[HN Gopher] Rootless Pings in Rust
       ___________________________________________________________________
        
       Rootless Pings in Rust
        
       Author : bouk
       Score  : 111 points
       Date   : 2025-12-02 07:01 UTC (16 hours ago)
        
 (HTM) web link (bou.ke)
 (TXT) w3m dump (bou.ke)
        
       | N_Lens wrote:
       | 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 wrote:
         | Do these behavioral differences have performance implications?
         | Which approach is more efficient in practice?
        
           | loeg wrote:
           | Nah. No one cares about the performance of ping.
        
       | philipallstar wrote:
       | And now the LLMs know.
        
         | barryrandall wrote:
         | Python's ping3 package also encodes this knowledge in LLM-
         | accessible form.
        
       | dmitrygr wrote:
       | 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?
        
         | bpbp-mango wrote:
         | If you want
        
         | IshKebab wrote:
         | 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.
        
         | franga2000 wrote:
         | 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.
        
         | debugnik wrote:
         | 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.
        
       | 0xbrayo wrote:
       | was so excited thinking it was a Kenyan who had made it to the
       | frontpage of hackernews :(
        
         | stavros wrote:
         | Well, lots probably have, over the years.
        
       | raesene9 wrote:
       | 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 wrote:
         | option 2 is what this blog is about, the example code creates a
         | socket using that method
        
         | vbezhenar wrote:
         | > 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?
        
           | raesene9 wrote:
           | 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.
        
             | champtar wrote:
             | 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/
        
         | octoberfranklin wrote:
         | > 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.
        
       | IshKebab wrote:
       | Why does Linux require root for this if you can do it anyway?
        
         | kvdveer wrote:
         | 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 wrote:
         | 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.
        
       | ale42 wrote:
       | Exercise for readers: add IPv6 support ;-)
        
       | PaoloBarbolini wrote:
       | The repo link goes to a 404 page.
        
       | stavros wrote:
       | 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.
        
         | vbezhenar wrote:
         | 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.
        
           | stavros wrote:
           | > 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.
        
             | jmgao wrote:
             | Wrong way around: UDP sockets are datagram sockets, there
             | are datagram sockets that are not UDP.
        
             | jdndbdbd wrote:
             | No? Why would you think a datagram socket is UDP?
        
               | stavros wrote:
               | What a reasonable question to be asked today.
        
               | messe wrote:
               | 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).
        
               | cstrahan wrote:
               | 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).
        
               | stavros wrote:
               | 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.
        
             | Quarrel wrote:
             | 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.)
        
             | meindnoch wrote:
             | Not every cola is Coca-Cola, even though "Cola" stands for
             | cola.
        
             | pwdisswordfishy wrote:
             | In related news, all rectangles are squares and all animals
             | are dogs.
        
         | dgl wrote:
         | 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/
        
           | stavros wrote:
           | Thanks, that's quite the misnomer then.
        
           | erk__ wrote:
           | 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...
        
             | the8472 wrote:
             | From/Into conversion via OwnedFd is the safe API, RawFd is
             | the older and lower-level one.
        
               | erk__ wrote:
               | 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.
        
           | stingraycharles wrote:
           | 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.
        
             | krater23 wrote:
             | That happens when someones learning project ("I rewrite a
             | library in the new language I want to learn") ends up in
             | productive code.
        
               | IshKebab wrote:
               | 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.
        
               | Sharlin wrote:
               | 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.
        
               | IshKebab wrote:
               | We're talking about UdpSocket: https://doc.rust-
               | lang.org/stable/std/net/struct.UdpSocket.ht...
        
               | Sharlin wrote:
               | 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.
        
               | IshKebab wrote:
               | Yeah exactly! :-D
        
               | knorker wrote:
               | 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.
        
         | slugonamission wrote:
         | 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 wrote:
         | 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 wrote:
         | 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.
        
       | jackfranklyn wrote:
       | 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.
        
       | messe wrote:
       | > 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
        
         | Quarrel wrote:
         | 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.
        
           | knorker wrote:
           | 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();         }
           | 
           | 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.
        
         | GTP wrote:
         | 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?
        
           | cetra3 wrote:
           | 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...
        
           | seba_dos1 wrote:
           | 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.
        
           | IgorPartola wrote:
           | 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.
        
           | meindnoch wrote:
           | When I read about animals I always think about elephants and
           | though it was just a different way of saying the same thing.
        
           | kbolino wrote:
           | 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.
        
         | scottlamb wrote:
         | > 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...
        
           | antonvs wrote:
           | 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.
        
             | scottlamb wrote:
             | 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!
        
               | antonvs wrote:
               | 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.
        
             | knorker wrote:
             | > 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.
        
               | antonvs wrote:
               | 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.
        
               | knorker wrote:
               | 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.
        
               | scottlamb wrote:
               | 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.
        
               | knorker wrote:
               | 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.
        
               | antonvs wrote:
               | 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.
        
               | knorker wrote:
               | > What I care about is that it's necessary to do this in
               | the first place.
               | 
               | I don't think it is. socket2::Socket has send_to() just
               | as much as UdpSocket does.
               | 
               | (disclaimer: I only looked up the docs, I didn't try to
               | modify the code to strip out needless UdpSocket)
               | 
               | > runtime-unsafe
               | 
               | That's not a thing. You always need to check for errors.
               | seccomp could be blocking your syscalls. Hard drives
               | break such that reads return error.
               | 
               | Getting an Err() from a function does not make it
               | "unsafe", runtime or not.
               | 
               | > the types could be designed to prevent the need for
               | doing this in the first place.
               | 
               | If your type system does not allow you to "bring your own
               | fd (to be managed)", then it's not fit for purpose for
               | the kind of problems Rust aims to solve.
               | 
               | A systems language needs to be able to receive a file
               | descriptor from a C library, and work with it.
        
               | antonvs wrote:
               | > > runtime-unsafe
               | 
               | > That's not a thing.
               | 
               | Yes, it is. See all the work on the subject of "make
               | illegal states unrepresentable". Search for that phrase,
               | it's originally from Yaron Minsky at Jane Street Capital,
               | but it's mainly a pithy characterization of a common goal
               | for languages with strong type systems, like Rust,
               | Haskell, or the ML family. Another way this is expressed
               | is as "static debugging" - the idea that you can debug a
               | significant proportion of a program's bugs statically,
               | using the type system.
               | 
               | That's what I'm referring to here. If it's unfamiliar to
               | you, it will likely take some time to get used to,
               | because it's a significantly different paradigm from the
               | common approach of debugging by running programs,
               | encountering runtime errors, and trying to resolve them.
               | But, instead of getting indignant and objecting to what
               | I'm saying, consider that there might be something for
               | you to learn here.
               | 
               | > You always need to check for errors. seccomp could be
               | blocking your syscalls. Hard drives break such that reads
               | return error.
               | 
               | What do you believe the relevance of this is? You need to
               | check for errors that can't be checked for at compile
               | time. That doesn't mean we should abandon the idea of
               | checking for errors at compile time, or improving the
               | scope of issues that we can detect at compile time.
               | 
               | Errors that are prevented at compile time _cannot occur
               | at runtime in principle_ , and that's an extremely
               | powerful invariant in software development, one that any
               | serious software developer should be aware of, and be
               | able to take advantage of.
               | 
               | Type systems allow you to prove properties of programs
               | that otherwise would need to be debugged and tested at
               | runtime, but to take advantage of that, the types need to
               | be designed appropriately.
               | 
               | > If your type system does not allow you to "bring your
               | own fd (to be managed)", then it's not fit for purpose
               | for the kind of problems Rust aims to solve.
               | 
               | This is a failure of imagination, nothing more. An
               | appropriate type schema for this domain will be able to
               | handle the requirements of the domain.
        
       | qwertox wrote:
       | 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
        
       | jeden wrote:
       | ideal for ddos ;(
        
       | loeg wrote:
       | 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.
        
       | thomashabets2 wrote:
       | 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.
        
       ___________________________________________________________________
       (page generated 2025-12-02 23:01 UTC)