diff --git a/docs/networking/network.md b/docs/networking/network.md index bd7d7bb3..6235eade 100644 --- a/docs/networking/network.md +++ b/docs/networking/network.md @@ -127,7 +127,15 @@ when it runs authelia, and `hive-collector.hive` on every hive is reachable, and where the hive's own collector listens). Note forge and matrix are swarm-domain names, not sub-domains of ``: a swarm runs one forge and one homeserver, so their names belong to the swarm rather -than to whichever hive happens to host them. dnsmasq forwards everything else to the host's +than to whichever hive happens to host them. + +Every domain answered this way is also `local=` for dnsmasq, so the other +record types (AAAA, TXT, MX, HTTPS) for those names get an empty answer from +dnsmasq itself instead of waiting on the host's upstream resolver. Without it, +dnsmasq 2.86 and later send them upstream, and a host whose upstream doesn't +answer leaves each lookup to its client's own timeout. + +dnsmasq forwards everything else to the host's own resolvers: it runs on the host and reads the host's `/etc/resolv.conf` directly. Containers don't need to know the upstream — they query the bridge IP and dnsmasq does the right thing diff --git a/nix/host-modules/hive-gateway/dnsmasq.nix b/nix/host-modules/hive-gateway/dnsmasq.nix index 10dda986..017b05a9 100644 --- a/nix/host-modules/hive-gateway/dnsmasq.nix +++ b/nix/host-modules/hive-gateway/dnsmasq.nix @@ -11,6 +11,11 @@ networkCfg, hyperhiveDomain, }: +let + # The domains this resolver answers itself: the hive domain and every + # name a module contributed. + localDomains = lib.optional (hyperhiveDomain != null) hyperhiveDomain ++ lib.unique cfg.localNames; +in { enable = true; # ON for its *plumbing*, not for the address it publishes. @@ -52,18 +57,25 @@ # Hive authoritative records — answer queries for the hive domain # + its sub-domains with the bridge IP, where nginx is reachable # from every container netns. - address = - lib.optional (hyperhiveDomain != null) "/${hyperhiveDomain}/${networkCfg.bridgeIp}" - # Names contributed by the modules that own them - # (`gateway.localNames`). Same address as everything above — the - # bridge IP is the gateway's answer for anything it fronts, and a - # contributing module neither knows nor should know it. - # - # `unique` is not tidiness: two modules claiming one name would - # otherwise emit two `address=` rules for it, and dnsmasq resolves - # that by precedence rather than by complaining. An assertion in - # ./default.nix makes the collision loud instead. - ++ map (name: "/${name}/${networkCfg.bridgeIp}") (lib.unique cfg.localNames); + # + # Names contributed by the modules that own them + # (`gateway.localNames`) get the same address — the bridge IP is the + # gateway's answer for anything it fronts, and a contributing module + # neither knows nor should know it. + # + # `unique` is not tidiness: two modules claiming one name would + # otherwise emit two `address=` rules for it, and dnsmasq resolves + # that by precedence rather than by complaining. An assertion in + # ./default.nix makes the collision loud instead. + address = map (name: "/${name}/${networkCfg.bridgeIp}") localDomains; + # Every domain above is also `local=`. Since 2.86 an `address=` rule + # answers only the queries of its own address family and sends the rest + # upstream, so an AAAA query for a name answered with an IPv4 address + # waits on the host's upstream resolver, and a host with none that + # answers (no network) leaves the client to its own timeout. `local=` + # with the same domain keeps those queries here: the A answer is + # unchanged and the other types get an empty answer. + local = map (name: "/${name}/") localDomains; # DHCP pool covering all usable host addresses on the bridge # subnet — bounds computed by hive-network.nix from # bridgeIp/bridgePrefixLength. All containers (agents and service diff --git a/nix/module-eval/core-toggle.nix b/nix/module-eval/core-toggle.nix index 67398f34..b25685e9 100644 --- a/nix/module-eval/core-toggle.nix +++ b/nix/module-eval/core-toggle.nix @@ -364,6 +364,21 @@ let && allLocal.services.hyperhive.otel.collector.host == "hive-collector.hive" && lib.elem "/hive-collector.hive/${bridge}" allLocal.services.dnsmasq.settings.address; } + { + # An `address=` rule alone sends the other record types upstream + # (dnsmasq 2.86), so each of its domains is also `local=`, and no other + # domain is: `local=` on a domain with public records would hide them. + name = "the resolver keeps queries for every domain it answers itself, and only those, off the upstream"; + ok = + let + s = allLocal.services.dnsmasq.settings; + domain = rule: builtins.elemAt (lib.splitString "/" rule) 1; + answered = map domain s.address; + in + lib.elem "hive-collector.hive" answered + && map domain (s.local or [ ]) == answered + && lib.all (rule: lib.hasSuffix "/" rule) (s.local or [ ]); + } { # The absence arm, and what makes the agents' queue address cases above able # to fail.