> 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
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.)
It may be memory safe but it's not using the type system to represent the domain very well.
One could imagine a more type-friendly design in which we could write that first line as follows:
let socket: Socket<IPv4, Datagram, IcmpV4> = Socket::new()?;
Now, the specifics of socket types will be statically checked.
Edit: I realized that the issue here is actually the conversion, and that UdpSocket on its own is actually a type-safe representation of a UDP socket, not a general datagram socket. But the fact that this dubiously-safe conversion is possible and even useful suggests that an improved design is possible. For example, a method like UdpSocket's `set_broadcast` can't work with a socket like the above, and from a type safety perspective, it shouldn't be possible to call it on such a socket.
One could, but one probably doesn't want to have separate types for TCP-over-IPv4 vs TCP-over-IPv6 for example, even if they accept/produce different forms of addresses. That'd force a lot of code bloat with monomorphization.
So now one is making one's own enumeration which is different than the OS one and mapping between them, which can get into a mess of all the various protocols Linux and other OSs support, and I'm not sure it's solving a major problem. Opinions vary, but I prefer to use complex types sparingly.
I think there are likely a bunch of other cases where it's useful to choose these values more dynamically too. Networking gets weird!
It's precisely because networking gets weird that a good representation at the type level could be useful. But I agree that it'd need to be done carefully to avoid creating usability issues.
> dubiously-safe
No, it's perfectly safe. Except if you expand the scope of "safe" by a lot.
OP turned the socket into an (almost) raw file descriptor, and created an UDP socket from it. Weird, yes, but since it's perfectly memory safe and invalid operations would correctly error, it's not "dubiously-safe". It's safe.
I mean, either your language has the ability to do raw (technically Owned in this case) file descriptors, or it doesn't.
Maybe you'd prefer Rust had a third mode? Safe, `unsafe {}`, and `are_you_sure_you_understand_this {}`, the last one also being 'safe', but just… odd.
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 do what you tell it to do, not what you intend it to do. Opening a file is way more dangerous than risking errors because "this syscall doesn't work on that fd".
There's also always risk that a syscall will fail at runtime, whether the type of fd is correct or not.
It sounds like you would prefer if UdpSocket From<OwnedFD> should run getsockname() or something to confirm it's of the expected type, but I would prefer not. 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.
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.
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<OwnedFD> 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.
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.
Why are you saying rust needs to be flooded with issues? Rust isn't conflating transport layers and protocols. OP is.
UdpSocket is just able to take ANY file descriptor and try to use it as if it's a UDP socket.
E.g. this compiles, and it's not a bug. It doesn't make any sense, but it's not a bug:
fn main() {
let f = std::fs::File::open("/dev/null").unwrap();
let f: std::os::fd::OwnedFd = f.into();
let socket: std::net::UdpSocket = f.into();
}
I think OP either banged on this until it compiled, maybe blindly copying from other examples, or it's vibe coded, and shows why AI needs supervision from someone who can actually understand what the code does.
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?
There's actually a lot of combinations of (domain, type, protocol) that are available. It is not always the case that the protocol implies the type.
In IP land (domains AF_INET and AF_INET6), we have the well known UDP and TCP protocols, of course. UDP is always datagram (SOCK_DGRAM) and TCP is always stream (SOCK_STREAM). Besides datagram-only ICMP, there's also SCTP, which lets you choose between stream and sequential-packet (SOCK_SEQPACKET) types. A sequential-packet socket provides in-order delivery of packet-sized messages, so it sits somewhere between datagram and stream in terms of features.
In AF_UNIX land, there are no protocols (the protocol field is always 0), but all 3 of the aforementioned types are available. You just have to pick the same type on both sides.
Footnotes: SCTP is not widely usable because Windows doesn't natively support it and many routers will drop or reject it instead of forwarding it. Also, AF_UNIX is now supported on Windows, but only with SOCK_STREAM type.
UDP and TCP are Layer 3 protocols, and so is ICMP. They all fill the same bits within network packets, like at the same level. So sending an ICMP packet (protocol 1) is not the same as sending a UDP packet (protocol 17).
Internet Protocols (v6 and v4) send packets via Ethernet (or WiFi or Bluetooth or anything else) from an IP address to an IP address. For structure see https://en.wikipedia.org/wiki/IPv6_packet or if for some reason you still need the legacy version see https://en.wikipedia.org/wiki/IPv4#Packet_structure (aside but notice how much complexity was removed from the legacy version). Notably, IP does not have any mechanism for reliability. It is essentially writing your address and a destination address on a brick and tossing over your fence to the neighbor’s yard and asking them to pass it along. If your neighbor isn’t home your brick is not moving along.
TCP and UDP send streams and datagrams respectively and use the concept of application ports. A TCP stream is what it sounds like: a continuous stream of bytes with no length or predefined stopping point. TCP takes your stream and chunks it into IP packets, the size of which is determined by the lowest Ethernet (or whatever data link protocol) data frame size. Typically this is 1500 but don’t forget to account for header sizes so useful payload size is smaller. TCP is complex because it guarantees that your stream will eventually be delivered in the exact order in which it was sent. Eventually here could mean at t = infinity. UDP simply has source and destination port numbers in its header (which follows the IP header in the data frame), and guarantees nothing: not ordering not guaranteed delivery, etc. If an IP packet is a brick with two house addresses, a UDP datagram is a brick with two house addresses and an ATTN: application X added. An address represents an computer (this is very approximate in the world where any machine can have N addresses and run M VMs or containers which themselves can have O addresses), and a port represents a specific process on that computer.
ICMP does not use ports. ICMP is meant for essentially network and host telemetry so you are still sending and receiving only at an IP address level. But it has a number of message types. You can see them here: https://en.wikipedia.org/wiki/ICMPv6. Note that ping and pong are just two of the types. Others are actually responsible for things like communicating what raw IP cannot. For example Packet Too Large type tells you that an IP packet you tried to send was hitting a part of its path where the datagram size did not allow it to fit and it’s used for IP MTU path discovery (you keep sending different size packets to find what is the largest that will go through).
There are other protocols that run directly on top of IP (6in4 for example, or SCTP). Most are way less popular than the three mentioned above. Some use datagrams (discrete “bricks” of data), some use streams (endless “tapes” of data), which is the difference in protocol family: datagrams vs stream. You can also go a level deeper and just craft raw IP packets directly but for that you typically must be the root user since you can for example send a packet with the source port set to 22 even though you are not the ssh daemon.
Since ICMP has no concept of a port, when you send a ping to a remote host and it returns a ping to you, how does your kernel know to hand the response to your process and not some other one? In the ICMP header there is an ICMP identifier (often the process PID) and when the reply comes back it has the same identifier (but with source and destination IPs swapped and type updated to echo reply). This is what the kernel uses to find the process to which it will deliver the ICMP packet.
I hope this clears up some of this.
[flagged]
You're using a single instance of poor API naming in a 3rd-party library (which is marked as beta) to dismiss the entire Rust language?
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.
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 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/
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.
Edit: It is possible in safe rust as well, see child comment.
From/Into conversion via OwnedFd is the safe API, RawFd is the older and lower-level one.
Ahh I guess that means that its possible in safe rust to cast a file descriptor to a different type. I was just looking at how socket2 did it and forgot to have a proper look.
Thanks, that's quite the misnomer then.
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.
That happens when someones learning project ("I rewrite a library in the new language I want to learn") ends up in productive code.
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.
Yes, I know, but the point is that the standard UdpSocket is correctly named as it doesn’t represent any other datagram socket. Uh, we’re pribably in agreement here actually.
Yeah exactly! :-D
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.
> 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.
Wrong way around: UDP sockets are datagram sockets, there are datagram sockets that are not UDP.
To give a more nuanced reply versus the "you're wrong" ones already here, the difference is that UDP adds send and receive ports, enabling most modern users (& uses) of UDP. Hence, it is the "User" datagram protocol.
(it also adds a checksum, which used to be more important than it is nowadays, but still well worth it imho.)
In related news, all rectangles are squares and all animals are dogs.
Not every cola is Coca-Cola, even though "Cola" stands for cola.
No? Why would you think a datagram socket is UDP?
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.
What networks are you using without ICMP?
Presumably you're also using systems that don't support Unix Domain Sockets which can be configured as SOCK_STREAM, SOCK_DGRAM, and even gasp SOCK_SEQPACKET (equivalent to SOCK_DGRAM in this case admittedly).
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.
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.
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.
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.
option 2 is what this blog is about, the example code creates a socket using that method
> 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?
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.
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.
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.
Exercise for readers: add IPv6 support ;-)
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.
Why does Linux require root for this if you can do it anyway?
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.
The repo link goes to a 404 page.
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?
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.
If you want
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.
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.
[deleted]
was so excited thinking it was a Kenyan who had made it to the frontpage of hackernews :(
Well, lots probably have, over the years.
ideal for ddos ;(
And now the LLMs know.
Python's ping3 package also encodes this knowledge in LLM-accessible form.
> 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
EDIT: s/ICMP4/ICMP/gEDIT2: more spelling mistakes
> 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):
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...
It may be memory safe but it's not using the type system to represent the domain very well.
One could imagine a more type-friendly design in which we could write that first line as follows:
Now, the specifics of socket types will be statically checked.Edit: I realized that the issue here is actually the conversion, and that UdpSocket on its own is actually a type-safe representation of a UDP socket, not a general datagram socket. But the fact that this dubiously-safe conversion is possible and even useful suggests that an improved design is possible. For example, a method like UdpSocket's `set_broadcast` can't work with a socket like the above, and from a type safety perspective, it shouldn't be possible to call it on such a socket.
One could, but one probably doesn't want to have separate types for TCP-over-IPv4 vs TCP-over-IPv6 for example, even if they accept/produce different forms of addresses. That'd force a lot of code bloat with monomorphization.
So now one is making one's own enumeration which is different than the OS one and mapping between them, which can get into a mess of all the various protocols Linux and other OSs support, and I'm not sure it's solving a major problem. Opinions vary, but I prefer to use complex types sparingly.
I think there are likely a bunch of other cases where it's useful to choose these values more dynamically too. Networking gets weird!
It's precisely because networking gets weird that a good representation at the type level could be useful. But I agree that it'd need to be done carefully to avoid creating usability issues.
> dubiously-safe
No, it's perfectly safe. Except if you expand the scope of "safe" by a lot.
OP turned the socket into an (almost) raw file descriptor, and created an UDP socket from it. Weird, yes, but since it's perfectly memory safe and invalid operations would correctly error, it's not "dubiously-safe". It's safe.
I mean, either your language has the ability to do raw (technically Owned in this case) file descriptors, or it doesn't.
Maybe you'd prefer Rust had a third mode? Safe, `unsafe {}`, and `are_you_sure_you_understand_this {}`, the last one also being 'safe', but just… odd.
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:
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 do what you tell it to do, not what you intend it to do. Opening a file is way more dangerous than risking errors because "this syscall doesn't work on that fd".
There's also always risk that a syscall will fail at runtime, whether the type of fd is correct or not.
It sounds like you would prefer if UdpSocket From<OwnedFD> should run getsockname() or something to confirm it's of the expected type, but I would prefer not. 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.
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.
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<OwnedFD> 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.
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.
Why are you saying rust needs to be flooded with issues? Rust isn't conflating transport layers and protocols. OP is.
UdpSocket is just able to take ANY file descriptor and try to use it as if it's a UDP socket.
E.g. this compiles, and it's not a bug. It doesn't make any sense, but it's not a bug:
OP is clearly confused, since there's no need to do this at all. socket2::Socket already has a `send_to()`: https://docs.rs/socket2/latest/socket2/struct.Socket.html#me...I think OP either banged on this until it compiled, maybe blindly copying from other examples, or it's vibe coded, and shows why AI needs supervision from someone who can actually understand what the code does.
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?
There's actually a lot of combinations of (domain, type, protocol) that are available. It is not always the case that the protocol implies the type.
In IP land (domains AF_INET and AF_INET6), we have the well known UDP and TCP protocols, of course. UDP is always datagram (SOCK_DGRAM) and TCP is always stream (SOCK_STREAM). Besides datagram-only ICMP, there's also SCTP, which lets you choose between stream and sequential-packet (SOCK_SEQPACKET) types. A sequential-packet socket provides in-order delivery of packet-sized messages, so it sits somewhere between datagram and stream in terms of features.
In AF_UNIX land, there are no protocols (the protocol field is always 0), but all 3 of the aforementioned types are available. You just have to pick the same type on both sides.
Footnotes: SCTP is not widely usable because Windows doesn't natively support it and many routers will drop or reject it instead of forwarding it. Also, AF_UNIX is now supported on Windows, but only with SOCK_STREAM type.
UDP and TCP are Layer 3 protocols, and so is ICMP. They all fill the same bits within network packets, like at the same level. So sending an ICMP packet (protocol 1) is not the same as sending a UDP packet (protocol 17).
You can see a list of network protocols in /etc/protocols actually, or here: https://www.iana.org/assignments/protocol-numbers/protocol-n...
Think of it as a protocol for sending custom ("user") datagrams. There are other datagram protocols too.
The kernel API defaults to UDP when you pass 0 as the protocol for a datagram socket, which may be the source of the confusion.
When I read about animals I always think about elephants and though it was just a different way of saying the same thing.
As the other commenter pointed out, UDP is transport protocol, not a packet level protocol.
Think of it like this:
Ethernet sends data frames to a MAC address. It has a sender and a receiver address. See here for structure: https://en.wikipedia.org/wiki/Ethernet_frame
Internet Protocols (v6 and v4) send packets via Ethernet (or WiFi or Bluetooth or anything else) from an IP address to an IP address. For structure see https://en.wikipedia.org/wiki/IPv6_packet or if for some reason you still need the legacy version see https://en.wikipedia.org/wiki/IPv4#Packet_structure (aside but notice how much complexity was removed from the legacy version). Notably, IP does not have any mechanism for reliability. It is essentially writing your address and a destination address on a brick and tossing over your fence to the neighbor’s yard and asking them to pass it along. If your neighbor isn’t home your brick is not moving along.
TCP and UDP send streams and datagrams respectively and use the concept of application ports. A TCP stream is what it sounds like: a continuous stream of bytes with no length or predefined stopping point. TCP takes your stream and chunks it into IP packets, the size of which is determined by the lowest Ethernet (or whatever data link protocol) data frame size. Typically this is 1500 but don’t forget to account for header sizes so useful payload size is smaller. TCP is complex because it guarantees that your stream will eventually be delivered in the exact order in which it was sent. Eventually here could mean at t = infinity. UDP simply has source and destination port numbers in its header (which follows the IP header in the data frame), and guarantees nothing: not ordering not guaranteed delivery, etc. If an IP packet is a brick with two house addresses, a UDP datagram is a brick with two house addresses and an ATTN: application X added. An address represents an computer (this is very approximate in the world where any machine can have N addresses and run M VMs or containers which themselves can have O addresses), and a port represents a specific process on that computer.
ICMP does not use ports. ICMP is meant for essentially network and host telemetry so you are still sending and receiving only at an IP address level. But it has a number of message types. You can see them here: https://en.wikipedia.org/wiki/ICMPv6. Note that ping and pong are just two of the types. Others are actually responsible for things like communicating what raw IP cannot. For example Packet Too Large type tells you that an IP packet you tried to send was hitting a part of its path where the datagram size did not allow it to fit and it’s used for IP MTU path discovery (you keep sending different size packets to find what is the largest that will go through).
There are other protocols that run directly on top of IP (6in4 for example, or SCTP). Most are way less popular than the three mentioned above. Some use datagrams (discrete “bricks” of data), some use streams (endless “tapes” of data), which is the difference in protocol family: datagrams vs stream. You can also go a level deeper and just craft raw IP packets directly but for that you typically must be the root user since you can for example send a packet with the source port set to 22 even though you are not the ssh daemon.
Since ICMP has no concept of a port, when you send a ping to a remote host and it returns a ping to you, how does your kernel know to hand the response to your process and not some other one? In the ICMP header there is an ICMP identifier (often the process PID) and when the reply comes back it has the same identifier (but with source and destination IPs swapped and type updated to echo reply). This is what the kernel uses to find the process to which it will deliver the ICMP packet.
I hope this clears up some of this.
[flagged]
You're using a single instance of poor API naming in a 3rd-party library (which is marked as beta) to dismiss the entire Rust language?
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.
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 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/
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.
Edit: It is possible in safe rust as well, see child comment.
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...
From/Into conversion via OwnedFd is the safe API, RawFd is the older and lower-level one.
Ahh I guess that means that its possible in safe rust to cast a file descriptor to a different type. I was just looking at how socket2 did it and forgot to have a proper look.
Thanks, that's quite the misnomer then.
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.
That happens when someones learning project ("I rewrite a library in the new language I want to learn") ends up in productive code.
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.
This is really no different. In this example it's not even a socket.We're talking about UdpSocket: https://doc.rust-lang.org/stable/std/net/struct.UdpSocket.ht...
Yes, I know, but the point is that the standard UdpSocket is correctly named as it doesn’t represent any other datagram socket. Uh, we’re pribably in agreement here actually.
Yeah exactly! :-D
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.
> 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.
Wrong way around: UDP sockets are datagram sockets, there are datagram sockets that are not UDP.
To give a more nuanced reply versus the "you're wrong" ones already here, the difference is that UDP adds send and receive ports, enabling most modern users (& uses) of UDP. Hence, it is the "User" datagram protocol.
(it also adds a checksum, which used to be more important than it is nowadays, but still well worth it imho.)
In related news, all rectangles are squares and all animals are dogs.
Not every cola is Coca-Cola, even though "Cola" stands for cola.
No? Why would you think a datagram socket is UDP?
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.
What networks are you using without ICMP?
Presumably you're also using systems that don't support Unix Domain Sockets which can be configured as SOCK_STREAM, SOCK_DGRAM, and even gasp SOCK_SEQPACKET (equivalent to SOCK_DGRAM in this case admittedly).
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.
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.
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.
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.
option 2 is what this blog is about, the example code creates a socket using that method
> 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?
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.
CAP_NET_RAW also allow to capture packets (tcpdump) so you really can have some fun like running a TCP stack in user space or MITM http connections: https://blog.champtar.fr/IPv6_RA_MITM/ / https://blog.champtar.fr/Metadata_MITM_root_EKS_GKE/
Great article, it lead me to the `icmplib`[0] Python project, which has a `privileged` option:
[0] https://github.com/ValentinBELYN/icmplibThe 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.
Exercise for readers: add IPv6 support ;-)
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.
Why does Linux require root for this if you can do it anyway?
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.
The repo link goes to a 404 page.
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?
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.
If you want
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.
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.
was so excited thinking it was a Kenyan who had made it to the frontpage of hackernews :(
Well, lots probably have, over the years.
ideal for ddos ;(
And now the LLMs know.
Python's ping3 package also encodes this knowledge in LLM-accessible form.