HNNewShowAskJobs
Built with Tanstack Start
Rootless Pings in Rust(bou.ke)
78 points by bouk 6 hours ago | 41 comments
  • messe3 hours ago

    > It turns out you can create a UDP socket with a protocol flag, which allows you to send the ping rootless

    This is wrong, despite the Rust library in question's naming convention. You're not creating a UDP socket. You're creating an IP (AF_INET), datagram socket (SOCK_DGRAM), using protocol ICMP (IPPROTO_ICMP). The issue is that the rust library apparently conflates datagram and UDP, when they're not the same thing.

    You can do the same in C, by calling socket(2) with the above arguments. It hinges on Linux allowing rootless pings from the GIDs in

      $ sysctl net.ipv4.ping_group_range
      net.ipv4.ping_group_range = 999 59999
    
    EDIT: s/ICMP4/ICMP/g

    EDIT2: more spelling mistakes

    • krater232 hours ago |parent

      Thanks! So it makes sense.

      And now we will have a API break somewhere in the future or a naming issue in the API forever. Why you shouldn't write productive code in Rust? Because it's in beta testing.

  • stavros3 hours ago

    This is interesting, but falls just short of explaining what's going on. Why does UDP work for ICMP? What does the final packet look like, and how is ICMP different from UDP? None of that is explained, it's just "do you want ICMP? Just use UDP" and that's it.

    It would have been OK if it were posted as a short reference to something common people might wonder about, but I don't know how often people try to reimplement rootless ping.

    • dgl3 hours ago |parent

      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/

      • erk__3 hours ago |parent

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

        • the8472an hour ago |parent

          From/Into conversion via OwnedFd is the safe API, RawFd is the older and lower-level one.

          • erk__an hour ago |parent

            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.

      • stingraycharles3 hours ago |parent

        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.

        • krater232 hours ago |parent

          That happens when someones learning project ("I rewrite a library in the new language I want to learn") ends up in productive code.

          • IshKebab2 hours ago |parent

            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.

            • Sharlin13 minutes ago |parent

              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.

      • stavros3 hours ago |parent

        Thanks, that's quite the misnomer then.

    • the84723 hours ago |parent

      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.

    • vbezhenar3 hours ago |parent

      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.

      • stavros3 hours ago |parent

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

        • jmgao2 hours ago |parent

          Wrong way around: UDP sockets are datagram sockets, there are datagram sockets that are not UDP.

        • jdndbdbdan hour ago |parent

          No? Why would you think a datagram socket is UDP?

          • stavrosan hour ago |parent

            What a reasonable question to be asked today.

            • messean hour ago |parent

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

    • slugonamission3 hours ago |parent

      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.

  • qwertoxan hour ago

    Great article, it lead me to the `icmplib`[0] Python project, which has a `privileged` option:

      When this option is enabled, this library fully manages the exchanges and the structure of ICMP packets. Disable this option if you want to use this function without root privileges and let the kernel handle ICMP headers.
    
    [0] https://github.com/ValentinBELYN/icmplib
  • raesene94 hours ago

    Worth noting you don't actually need to be fully root in Linux to do standard pings with your code, there's a couple of different options available at the OS level without needing to modify code.

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

    2. There's a sysctl that allows for unprivileged ping "net.ipv4.ping_group_range" which can be used at the host level to allow different groups to use ICMP ping.

    • bouk4 hours ago |parent

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

    • vbezhenar3 hours ago |parent

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

      • raesene93 hours ago |parent

        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.

        • champtar39 minutes ago |parent

          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/

  • jackfranklyn3 hours ago

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

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

  • ale424 hours ago

    Exercise for readers: add IPv6 support ;-)

  • N_Lens5 hours ago

    The Linux vs macOS behavioral differences in ICMP sockets documented by the article are critical:

    - Linux overwrites identifier and checksum fields

    - macOS requires correct checksum calculation

    - macOS includes IP header in response, Linux doesn't

    I think this is the kind of subtle difference that would trip up even experienced programmers

    • badmonster3 hours ago |parent

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

  • IshKebab4 hours ago

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

    • kvdveer4 hours ago |parent

      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.

  • PaoloBarbolini4 hours ago

    The repo link goes to a 404 page.

  • dmitrygr4 hours ago

    I struggled in vain to see what this has to do with rust. The answer is nothing other than the 4 lines of sample code shown are in Rust. The actually useful nugget of knowledge contained therein (one can create ICMP packets without being root on MacOS or Linux) is language agnostic.

    So... why? Should I now add "in C" or "in assembly" to the end of all my article titles?

    • franga20004 hours ago |parent

      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.

    • bpbp-mango4 hours ago |parent

      If you want

    • debugnikan hour ago |parent

      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.

    • IshKebab4 hours ago |parent

      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.

  • 0xbrayo4 hours ago

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

    • stavros3 hours ago |parent

      Well, lots probably have, over the years.

  • philipallstar5 hours ago

    And now the LLMs know.