Rootless pings in Rust

Creating an ICMP socket and sending a ping usually requires root: you cannot create a raw socket to send ICMP packets without it. ping Although the command line tool works without root, how is this possible? This shows that you can create a UDP socket with protocol flags, allowing you to send pings rootless. I couldn’t find any simple examples of this online and LLMs are surprisingly bad at it (probably due to lack of examples). So I posted an example on GitHub in Rust. Its gist is this:

1. Create a UDP socket with ICMP protocol

Using socket2 crate.

use socket2::{Domain, Protocol, Socket, Type};
use std::net::UdpSocket;

let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::ICMPV4))?;
let socket: UdpSocket = socket.into();

2. Create and send a ping packet

Note that you do not need to provide an IP header and that Linux and macOS behave differently here: Linux kernel overrides the identifier and checksum fields, while macOS does Use them and the checksum should be correct.

let sequence: u16 = 1;
let mut packet: Vec<u8> = vec![
    8, // type: echo request
    0, // code: always 0 for echo request
    0, 0, // checksum: calculated by kernel on Linux, required on macOS
    0, 1, // identifier: overwritten by kernel on Linux, not on macOS
    (sequence >> 8) as u8, (sequence & 0xff) as u8,
    b'h', b'e', b'l', b'l', b'o', // payload (can be anything)
];

// Checksum is determined by the kernel on Linux, but it's needed on macOS
let checksum = calculate_checksum(&packet);
packet[2] = (checksum >> 8) as u8;
packet[3] = (checksum & 0xff) as u8;

// Port can be anything, doesn't matter
socket.send_to(&packet, "1.1.1.1:0")?;

3. Receive and interpret feedback

Here macOS and Linux differ again: macOS includes the IP header in the response, Linux does not.

let mut buffer = vec![0u8; 64];
let (size, from_addr) = socket.recv_from(&mut buffer)?;

// On macOS, the IP header is included in the received packet, strip it
#[cfg(target_os = "macos")]
const IP_HEADER_LEN: usize = 20;

// On Linux, the IP header is not included
#[cfg(not(target_os = "macos"))]
const IP_HEADER_LEN: usize = 0;

let data = &buffer[IP_HEADER_LEN..size];
let reply_type = data[0]; // should be 0
let reply_sequence = ((data[6] as u16) << 8) | (data[7] as u16); // should equal 'sequence'
let payload = &data[8..]; // should be b"hello"

Of course you can implement latency, loss, periodic ping etc but that is left as an exercise for the reader.

November 2025



<a href

Leave a Comment