Rootless Pings in Rust
56 points
by bouk
3 hours ago
| 11 comments
| bou.ke
| HN
messe
54 minutes 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 hings 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
reply
stavros
1 hour 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.

reply
dgl
59 minutes 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/

reply
erk__
41 minutes ago
[-]
The std api can only create UdpSockets, the trick here is that you use Socket2 which allows more kinds of sockets and then you tell UdpSocket that some raw file descriptor is a upd socket through a unsafe api with no checks and I guess it works because they use the same api on posix.

The macro used by socket2: https://docs.rs/socket2/0.6.1/src/socket2/lib.rs.html#108

The FromRawFd trait: https://doc.rust-lang.org/stable/std/os/fd/trait.FromRawFd.h...

reply
stingraycharles
38 minutes ago
[-]
So UdpSocket should really be called DatagramSocket, UDP being the protocol that operates on these datagrams?

Surprising that they got such a fundamental thing wrong.

reply
stavros
47 minutes ago
[-]
Thanks, that's quite the misnomer then.
reply
vbezhenar
1 hour 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.

reply
stavros
48 minutes ago
[-]
> It's not UDP socket that gets created here, but Datagram socket

A datagram socket is a UDP socket, though. That's what the D stands for.

reply
jmgao
20 minutes ago
[-]
Wrong way around: UDP sockets are datagram sockets, there are datagram sockets that are not UDP.
reply
slugonamission
59 minutes 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.

reply
the8472
52 minutes 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.
reply
jackfranklyn
1 hour 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.

reply
ale42
2 hours ago
[-]
Exercise for readers: add IPv6 support ;-)
reply
raesene9
2 hours 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.

reply
bouk
2 hours ago
[-]
option 2 is what this blog is about, the example code creates a socket using that method
reply
vbezhenar
54 minutes 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?

reply
raesene9
47 minutes ago
[-]
It lets you send raw sockets, and has some dangers (e.g. packet forgery). It's included in pretty much every container in existence (if you're running as root in the container or have ambient capabilities setup).

The goal of the capabilities system was to allow processes and users to gain a small portion of root privileges without giving them all.

In the "old days" ping on a Linux host would be setuid root, so it essentially had all of root's rights. In more modern setups it either has CAP_NET_RAW or the ping_group sysctl is used to allow non-root users to use it.

reply
N_Lens
2 hours 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

reply
badmonster
1 hour ago
[-]
Do these behavioral differences have performance implications? Which approach is more efficient in practice?
reply
IshKebab
2 hours ago
[-]
Why does Linux require root for this if you can do it anyway?
reply
kvdveer
1 hour 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.

reply
PaoloBarbolini
1 hour ago
[-]
The repo link goes to a 404 page.
reply
dmitrygr
2 hours 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?

reply
franga2000
2 hours 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.
reply
bpbp-mango
2 hours ago
[-]
If you want
reply
IshKebab
2 hours 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.
reply
0xbrayo
2 hours ago
[-]
was so excited thinking it was a Kenyan who had made it to the frontpage of hackernews :(
reply
stavros
1 hour ago
[-]
Well, lots probably have, over the years.
reply
philipallstar
2 hours ago
[-]
And now the LLMs know.
reply