Live data from Hacker News

Rootless Pings in Rust

bou.ke

71–80 of 87 posts

Re: Rootless Pings in Rust

#71
post #16

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…

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.

Re: Rootless Pings in Rust

#72
post #44

Earlier quoted context omitted.

This is in the standard library; it's not a learning project. And it also isn't even incorrect - see erk__'s comment. Rust is an excellent language and fully capable of production use.

It's not, it's the `socket2` library. The standard sockets don't allow (ab)using actual `UdpSocket`s as a different kind of datagram socket.

Sure it does.

    let f = std::fs::File::open("/dev/null").unwrap();
    let f: std::os::fd::OwnedFd = f.into();
    let socket: std::net::UdpSocket = f.into();
This is really no different. In this example it's not even a socket.

Re: Rootless Pings in Rust

#73
post #70
post #68

Earlier quoted context omitted.

It's dubiously safe because it allows invalid combinations, i.e. calling UDP-related methods on non-UDP sockets. I'm using "safe" in the general English sense here, "protected from or not exposed to danger or risk." > invalid operations would correctly error At runtime, yes. I'm pointing out that Rust makes it possible to do better, and catch such issues at compile time.

Of course it allows invalid combinations. This also compiles: let f = std::fs::File::open("/dev/null").unwrap(); let f: std::os::fd::OwnedFd = f.into(); let socket: std::net::UdpSocket = f.into(); If you convert a high level object into a low level one, and then back up as another type, then what exactly do you expect the language to do about that? > "protected from or not exposed to danger or risk." A computer will…

I agree with everything you wrote except for this:

> Indeed, in the general case some perfectly coded `unsafe` code could `dup2()` over the fd, so any checking at UdpSocket creation time is moot; you still don't get the safety you are asking for.

If `unsafe` code breaks safe code's soundness guarantees (let's assume for a second an alternate world in which "fd is of the correct type" is a soundness guarantee Rust makes), the bug is in the `unsafe` code.

Re: Rootless Pings in Rust

#74
post #70
post #68

Earlier quoted context omitted.

It's dubiously safe because it allows invalid combinations, i.e. calling UDP-related methods on non-UDP sockets. I'm using "safe" in the general English sense here, "protected from or not exposed to danger or risk." > invalid operations would correctly error At runtime, yes. I'm pointing out that Rust makes it possible to do better, and catch such issues at compile time.

Of course it allows invalid combinations. This also compiles: let f = std::fs::File::open("/dev/null").unwrap(); let f: std::os::fd::OwnedFd = f.into(); let socket: std::net::UdpSocket = f.into(); If you convert a high level object into a low level one, and then back up as another type, then what exactly do you expect the language to do about that? > "protected from or not exposed to danger or risk." A computer will…

There are two issues here, and you're talking about a different one from the one I'm interested in. Your main issue seems to be this:

> If you convert a high level object into a low level one, and then back up as another type, then what exactly do you expect the language to do about that?

One answer to this would be "prevent it entirely". That's probably not practical for a language like Rust today, though, and I don't really care about that.

What I care about is that it's necessary to do this in the first place. The fact that doing this can be useful and necessary in a case like this suggests that it would be possible to design the types involved so that you don't need these low-level and runtime-unsafe conversions to get the job done.

> It sounds like you would prefer if UdpSocket From should run getsockname() or something to confirm it's of the expected type

No, I'm saying the types could be designed to prevent the need for doing this in the first place.

Re: Rootless Pings in Rust

#75
post #39

Earlier quoted context omitted.

What a reasonable question to be asked today.

Let me rephrase GP into (I hope) a more useful analogy. — actually, here’s the whole analogous exchange: “A rectangle is an equal-sided rectangle (i.e. “square”) though. That’s what the R stands for.” “No? Why would you think a rectangle is a square?” Just as not all rectangles are squares (squares are a specific subset of rectangles), not all datagram protocols are UDP (UDP is just one particular datagram protocol).

The obvious answer is "I didn't know datagrams were a superset of UDP". I don't really understand how "how do you not know this" is a reasonable or useful question to ask.

Re: Rootless Pings in Rust

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

Re: Rootless Pings in Rust

#77
post #2

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

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

Nah. No one cares about the performance of ping.

Re: Rootless Pings in Rust

#78
post #48
post #25

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

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?

[deleted]

Re: Rootless Pings in Rust

#79
post #9

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…

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

Re: Rootless Pings in Rust

#80
post #70

Earlier quoted context omitted.

Of course it allows invalid combinations. This also compiles: let f = std::fs::File::open("/dev/null").unwrap(); let f: std::os::fd::OwnedFd = f.into(); let socket: std::net::UdpSocket = f.into(); If you convert a high level object into a low level one, and then back up as another type, then what exactly do you expect the language to do about that? > "protected from or not exposed to danger or risk." A computer will…

I agree with everything you wrote except for this: > Indeed, in the general case some perfectly coded `unsafe` code could `dup2()` over the fd, so any checking at UdpSocket creation time is moot; you still don't get the safety you are asking for. If `unsafe` code breaks safe code's soundness guarantees (let's assume for a second an alternate world in which "fd is of the correct type" is a soundness guarantee Rust mak…

Sure, I would strongly recommend against doing something like that. But I would expect it to work in the obvious way, and not be undefined behavior.

E.g. if UdpSocket were to dup() internally its fd A into a fd B, and as_fd() returned B, but all actual recv/send is on fd A, then that would cause worse problems than this.

But say an OS has a sockopt that turns an IPv4 UDP socket into a IPv6 UDP socket. Would it be OK for me to call that on UdpSocket's underlying fd? I'd say yes.

Now if I closed the fd for a UdpSocket from underneath it, I would expect that to be basically UB, if not by spec, then in practice impossible to reason about.

As for unsound, sure. I could be convinced of calling the dup2 thing unsound. Not sure unsound is well enough defined, but basically: don't do it.

Is it unsound to create a UdpSocket from a non-UDP file descriptor? Not in a way that can trigger unsafe, no.

Post reply on HN