refactor(3202): the gateway takes contributed dns names

Adds `services.hyperhive.gateway.localNames` (internal): hostnames the
hive resolver answers with the bridge IP, contributed by the modules
that own them. The service says which name, the gateway says where it
points — the same split `lib.tlsFor` already makes.

No behaviour change yet: the list is empty until the service modules
contribute in the following commits.

The assertion is not defensive padding. Duplicate `address=` rules do
not make dnsmasq complain; it resolves them by precedence, so a name
claimed twice silently stops being served by one of its claimants. That
failure mode only becomes reachable because contribution is now open, so
it gets closed in the same commit that opens it.
This commit is contained in:
atlas 2026-08-13 12:42:33 +02:00
commit 991cd24fc8
3 changed files with 55 additions and 1 deletions

View file

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

View file

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

View file

@ -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);