Rootless pings in Rust
Sending a ping by creating an ICMP socket normally requires root: you can’t create a raw socket to send ICMP packets without it. The ping command line tool works without root however, how is that possible? It turns out you can create a UDP socket with a protocol flag, which allows you to send the ping rootless. I couldn’t find any simple examples of this online and LLMs are surprisingly bad at this (probably because of the lack of examples). Therefore I posted an example on GitHub in Rust. The gist of it is this:
1. Create a UDP socket with ICMP protocol
Using the 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 the ping packet
Note that you don’t need to provide an IP header and that Linux and macOS behave differently here: the Linux kernel overrides the identifier and checksum fields, while macOS does use them and the checksum needs to 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 the response
Here macOS and Linux are different 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 pings etc. but that’s left as an exercise to the reader.
Nov 2025