diff --git a/docs/gotchas.md b/docs/gotchas.md index c50421be..4c106d3d 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -72,6 +72,32 @@ the path entry resolves to `/run/wrappers/bin/bin` instead. hive-c0re restarts. Without it, every restart wipes bind sources and existing containers can't be started. +### `RestrictAddressFamilies` fails as "Address family not supported by protocol" + +A unit whose `RestrictAddressFamilies` omits a family gets `EAFNOSUPPORT` +(errno 97) back from `socket()`. Clients surface that as *"tcp open error: +Address family not supported by protocol"* — the message names the +**protocol** and never the **sandbox**, so it reads like a dead network, a +missing route, or an IPv6 problem. + +⇒ On that error, read the unit before you touch the network. + +Two things to get right when a daemon needs outbound TCP: + +- list `AF_INET` **and** `AF_INET6` — omitting one leaves a client that + works until DNS hands back the other family; +- list `AF_NETLINK` too. glibc's `getaddrinfo` opens a netlink socket to + enumerate local addresses before it returns any, so name resolution + fails without it even when `AF_INET` is allowed. + +**The directive is a claim about what the program does, and nothing +re-checks it when the program changes.** A unit that only served a unix +socket when it was written is correct at `[ "AF_UNIX" ]` and silently wrong +the day someone adds an HTTP client. Check the unit in the same commit as +the client — and when narrowing it, prefer a test that derives the required +families from the code (which fails on the *next* client too) over one that +asserts today's list. + ### `register_agent` is idempotent Drops any prior socket task before rebinding. Required so a diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index 90c2ca67..9c8b1fca 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -508,8 +508,7 @@ in StateDirectoryMode = "0750"; # Nothing here needs a writable filesystem, real privileges, or a - # view of the rest of the machine; the daemon reads its socket path - # from config and serves. + # view of the rest of the machine. PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = true; @@ -518,8 +517,27 @@ in ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; + + # `AF_UNIX` for the socket this daemon serves on, plus what its + # outbound clients need: it mints authelia tokens and calls the forge + # over HTTPS (`auth.rs`, `forge.rs`) and reaches the queue over NATS + # (`main.rs`, `status.rs`). `AF_NETLINK` because glibc's + # `getaddrinfo` opens a netlink socket to enumerate local addresses + # before it will return one. + # + # ⚠️ This list is a CLAIM ABOUT WHAT THE DAEMON DOES, so it goes stale + # the moment the daemon grows a client — and it goes stale in the + # worst available way: a blocked family makes `socket()` return + # EAFNOSUPPORT, i.e. "Address family not supported by protocol", so + # the error names the protocol and never the sandbox that refused it. + # This was `AF_UNIX`-only while the daemon merely served its socket; + # all three clients above arrived later, and the restriction was not + # revisited. Add the family when you add the client. RestrictAddressFamilies = [ "AF_UNIX" + "AF_INET" + "AF_INET6" + "AF_NETLINK" ]; };