23 comments

[ 2.8 ms ] story [ 48.5 ms ] thread
The Linux vs macOS behavioral differences in ICMP sockets documented by the article are critical:

- Linux overwrites identifier and checksum fields

- macOS requires correct checksum calculation

- macOS includes IP header in response, Linux doesn't

I think this is the kind of subtle difference that would trip up even experienced programmers

And now the LLMs know.
Python's ping3 package also encodes this knowledge in LLM-accessible form.
I struggled in vain to see what this has to do with rust. The answer is nothing other than the 4 lines of sample code shown are in Rust. The actually useful nugget of knowledge contained therein (one can create ICMP packets without being root on MacOS or Linux) is language agnostic.

So... why? Should I now add "in C" or "in assembly" to the end of all my article titles?

was so excited thinking it was a Kenyan who had made it to the frontpage of hackernews :(
Worth noting you don't actually need to be fully root in Linux to do standard pings with your code, there's a couple of different options available at the OS level without needing to modify code.

1. You can just add the capability CAP_NET_RAW to your process, at which point it can ping freely

2. There's a sysctl that allows for unprivileged ping "net.ipv4.ping_group_range" which can be used at the host level to allow different groups to use ICMP ping.

> There's a sysctl that allows for unprivileged ping "net.ipv4.ping_group_range"

What are the risks of enabling this for all groups (i.e. sysctl net.ipv4.ping_group_range='0 4294967294')?

Note this allows unprivileged ICMP sockets, not unprivileged RAW sockets.

Why does Linux require root for this if you can do it anyway?
(comment deleted)
It doesn't.

For users in the UID range in sysctl `net.ipv4.ping_group_range` the normal ping command uses this non-root way.

Sure, maybe your system still sets suid root on your ping binary, or shows it adding `cap_net_raw` according to `getcap`, but mine does not.

Exercise for readers: add IPv6 support ;-)
This is interesting, but falls just short of explaining what's going on. Why does UDP work for ICMP? What does the final packet look like, and how is ICMP different from UDP? None of that is explained, it's just "do you want ICMP? Just use UDP" and that's it.

It would have been OK if it were posted as a short reference to something common people might wonder about, but I don't know how often people try to reimplement rootless ping.

The rust API in use lets you feed an fd into a UdpSocket which calls the necessary send/recv/etc on it.

The socket itself is an ICMP socket, but the ICMP shaped API just happened to fit into the UDP shaped hole. I'm sure some advanced UDP socket options will break or have weird side effects if your code tries to apply them.

(comment deleted)
> It turns out you can create a UDP socket with a protocol flag, which allows you to send the ping rootless

This is wrong, despite the Rust library in question's naming convention. You're not creating a UDP socket. You're creating an IP (AF_INET), datagram socket (SOCK_DGRAM), using protocol ICMP (IPPROTO_ICMP). The issue is that the rust library apparently conflates datagram and UDP, when they're not the same thing.

You can do the same in C, by calling socket(2) with the above arguments. It hinges on Linux allowing rootless pings from the GIDs in

  $ sysctl net.ipv4.ping_group_range
  net.ipv4.ping_group_range = 999 59999
EDIT: s/ICMP4/ICMP/g

EDIT2: more spelling mistakes

Could you please explain me the difference? As UDP is the "User Datagram Protocol" when I read about datagrams I always think about UDP and though it was just a different way of saying the same thing. Maybe "datagram" is supposed to be the packet itself, but you're still sending it via UDP, right?
> The issue is that the rust library apparently conflates datagram and UDP, when they're not the same thing.

It comes down to these two lines (using full items paths for clarity):

    let socket = socket2::Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::ICMPV4))?;
    let socket: std::net::UdpSocket = socket.into();
The latter is using this impl: https://docs.rs/socket2/0.6.1/socket2/struct.Socket.html#imp...

Basically the `socket2` crate lets you convert the fd it produces into a `UdpSocket`. It doesn't verify it really is a UDP socket first; that's up to you. If you do it blindly, you can get something with the wrong name, but it's probably harmless. (At the very least, it doesn't violate memory safety guarantees, which is what Rust code tends to be very strict about.)

`UdpSocket` itself has a `From<OwnedFd>` impl that similarly doesn't check it really is a UDP socket; you could convert the `socket2::Socket` to an `OwnedFd` then that to a `UdpSocket`. https://doc.rust-lang.org/stable/std/net/struct.UdpSocket.ht... https://docs.rs/socket2/0.6.1/socket2/struct.Socket.html#imp...

Great article, it lead me to the `icmplib`[0] Python project, which has a `privileged` option:

  When this option is enabled, this library fully manages the exchanges and the structure of ICMP packets. Disable this option if you want to use this function without root privileges and let the kernel handle ICMP headers.
[0] https://github.com/ValentinBELYN/icmplib
ideal for ddos ;(
I was interested in a related topic a while back.

Historically, to receive ICMP packets, I think you had to open a RAW socket and snoop everything. Obviously, this required root or similar.

IPPROTO_ICMP allows you to send ICMP packets and receive responses from the same address, without root. But you can't use it for traceroute because it only accepts ICMP responses from the ultimate destination you sent to; not some TTL failure intermediary.

Finally, IP_RECVERR (Linux 2.2) on UDP sockets allows you to receive associated ICMP errors from any hop for a send. (This is useful for traceroute, but not ICMP ping.)

I think there are also some caveats on how you can monitor for these type of events in Rust in particular? IIRC, the mainstream async stuff only watches for read/write events, and these aren't those.

Since basically all the comments are about how both the author and many commenters are confused about what UDP and DGRAM sockets are, I have corrected the author's code to no longer miscommunicate what protocol is being used.

https://github.com/ThomasHabets/rust-ping-example-corrected

There is no UDP used anywhere in this example. ICMP is not UDP.

I'm not saying my fix is pretty (e.g. uses unwrap(), and ugly destination address parsing), but that's not the point.