top | item 46118432

Rootless Pings in Rust

127 points| bouk | 3 months ago |bou.ke

80 comments

order

messe|3 months ago

> 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

scottlamb|3 months ago

> 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...

Quarrel|3 months ago

Thank you.

I assumed this was what was happening, but conflating network layer protocols with transport layer ones isn't great.

I'm surprised that pedantic zealots, like me in my youth, haven't risen up and flooded rust with issues over it way before this though.

GTP|3 months ago

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?

krater23|3 months ago

[deleted]

stavros|3 months ago

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.

dgl|3 months ago

The BSD socket API has 3 parameters when creating a socket with socket(), the family (e.g. inet) the kind (datagram in this case) and the protocol (often 0, but IPPROTO_ICMP in this case).

Because when the protocol is 0 it means a UDP socket Rust has called its API for creating any(?) datagram sockets UdpSocket, partly resulting in this confusion.

The kernel patch introducing the API also explains it was partly based on the UDP code, due to obviously sharing a lot of properties with it. https://lwn.net/Articles/420800/

vbezhenar|3 months ago

ICMP is just different protocol from UDP. There's field "Protocol" in IP packet. 0x01 = ICMP, 0x06 = TCP, 0x11 = UDP.

I think that this article gets terminology wrong. It's not UDP socket that gets created here, but Datagram socket. Seems to be bad API naming in Rust library.

slugonamission|3 months ago

So in fairness, this doesn't actually use UDP at all (SOCK_DGRAM does not mean UDP!).

The actual protocol in use, and what's supported, it matched by all of the address family (IPV4), the socket type (DGRAM), and the protocol (ICMP). The match structure for IPV4 is here in Linux at least: https://elixir.bootlin.com/linux/v6.18/source/net/ipv4/af_in...

So ultimately, it's not even UDP, it's just creating a standard ICMP socket.

the8472|3 months ago

The semantic wrappers around file descriptors (File, UdpSocket, PidFd, PipeReader, etc.) are advisory and generally interconvertible. Since there's no dedicated IcmpSocket they're using UdpSocket which happens to provide the right functions to invoke the syscalls they need.

jeroenhd|3 months ago

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.

thomashabets2|3 months ago

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.

loeg|3 months ago

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.

raesene9|3 months ago

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.

bouk|3 months ago

option 2 is what this blog is about, the example code creates a socket using that method

octoberfranklin|3 months ago

> 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.

vbezhenar|3 months ago

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

What are consequences of this capability? Seems like restricting this to root was done for a reason?

N_Lens|3 months ago

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

badmonster|3 months ago

Do these behavioral differences have performance implications? Which approach is more efficient in practice?

ale42|3 months ago

Exercise for readers: add IPv6 support ;-)

qwertox|3 months ago

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

jackfranklyn|3 months ago

The unprivileged DGRAM approach is a lifesaver for container environments. Ran into this building a health check service - spent ages wondering why my ping code needed --privileged when the system ping worked fine as a normal user. Turns out the default ping binary has setuid, which isn't an option in a minimal container image.

The cross-platform checksum difference is a pain though. Linux handling it for you is convenient until you test on macOS and everything breaks silently.

dmitrygr|3 months ago

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?

franga2000|3 months ago

It's a lot more than 4 lines of sample code, in fact on my screen, it looks like it's more code than text. This is closer to a Rust tutorial then a low-level networking explainer, so yeah, it makes sense to say "in Rust". If I wanted to do this in C, this would not be the best resource.

IshKebab|3 months ago

Yeah it would definitely be a good idea for the assembly ones. Maybe not C since C has kind of been the de facto language for this stuff for decades so it's implied.

debugnik|3 months ago

Agreed. I don't dislike Rust as a language, but it annoys me how its practitioners add the "[written] in Rust" tagline to every single thing they do that's otherwise unrelated to Rust. Specially when their code or dependencies are full of unverified unsafe blocks, which defeats the selling point.

IshKebab|3 months ago

Why does Linux require root for this if you can do it anyway?

kvdveer|3 months ago

Linux requires root for raw sockets, which _can_ be used to send pings, but also numerous other things.

The trick used here only allows pings. This trick is gated behind other ACLs.

thomashabets2|3 months ago

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.

0xbrayo|3 months ago

was so excited thinking it was a Kenyan who had made it to the frontpage of hackernews :(

stavros|3 months ago

Well, lots probably have, over the years.

jeden|3 months ago

ideal for ddos ;(

philipallstar|3 months ago

And now the LLMs know.

barryrandall|3 months ago

Python's ping3 package also encodes this knowledge in LLM-accessible form.