diff --git a/nix/host-modules/hive-gateway/default.nix b/nix/host-modules/hive-gateway/default.nix index 469ca341..db4188b9 100644 --- a/nix/host-modules/hive-gateway/default.nix +++ b/nix/host-modules/hive-gateway/default.nix @@ -125,6 +125,25 @@ in Let's Encrypt needs a contact address for the ACME account. ''; } + { + # Two modules claiming one hostname is a real possibility now + # that each service contributes its own name, and dnsmasq would + # not complain: duplicate `address=` rules resolve by precedence, + # so the loser simply stops being served with no error anywhere. + # Fail the build instead — a name is owned by exactly one module. + assertion = lib.length (lib.unique cfg.localNames) == lib.length cfg.localNames; + message = '' + services.hyperhive.gateway.localNames contains a duplicate: + ${lib.concatStringsSep ", " ( + lib.unique (lib.filter (n: lib.count (m: m == n) cfg.localNames > 1) cfg.localNames) + )} + + Each hostname the hive resolver answers for is contributed by + exactly one module. Two modules claiming the same name means + two services believe they serve it — resolve which one does + rather than letting dnsmasq pick. + ''; + } ]; # Ensure the gateway state dirs exist at host boot, before anything @@ -353,6 +372,7 @@ in services.dnsmasq = import ./dnsmasq.nix { inherit lib + cfg networkCfg forgeCfg matrixCfg diff --git a/nix/host-modules/hive-gateway/dnsmasq.nix b/nix/host-modules/hive-gateway/dnsmasq.nix index e790cf27..2cfba396 100644 --- a/nix/host-modules/hive-gateway/dnsmasq.nix +++ b/nix/host-modules/hive-gateway/dnsmasq.nix @@ -7,6 +7,7 @@ # are computed by hive-network. { lib, + cfg, # services.hyperhive.gateway networkCfg, forgeCfg, matrixCfg, @@ -79,7 +80,17 @@ # Reachability is not the access control here: the vhost's # `auth_request` + authelia's `group:operators` rule are, and an # agent that resolves the name still cannot open the page. - ++ lib.optional uiCfg.enable "/${uiCfg.domain}/${networkCfg.bridgeIp}"; + ++ lib.optional uiCfg.enable "/${uiCfg.domain}/${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); # 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/host-modules/hive-gateway/options.nix b/nix/host-modules/hive-gateway/options.nix index f0945a6b..48de8a1f 100644 --- a/nix/host-modules/hive-gateway/options.nix +++ b/nix/host-modules/hive-gateway/options.nix @@ -94,6 +94,29 @@ in ''; }; + localNames = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + internal = true; + description = '' + Extra hostnames the hive's resolver answers with the bridge IP, + contributed by the modules that own those names. + + A service module says **which name**; the gateway decides + **where it points** — the same split as `lib.tlsFor`. A service + that hardcoded the bridge IP would be one more place to fix when + the network layout changes, and it has no business knowing it. + + ⚠️ Contribute a name only when THIS host actually serves it. The + list is not "names the swarm has" — + `services.hyperhive.swarm.serviceDomains` is that, and it is + deliberately broader (it drives certificate issuance, so it + includes names this hive may only be a client of). Publishing an + address record for a service you do not run points every agent + on the bridge at a door that isn't there. + ''; + }; + lib = { listen = lib.mkOption { type = lib.types.listOf (lib.types.attrsOf lib.types.raw);