Watch
0
0
Fork
You've already forked hyperhive
0

the bridge resolver answers other record types for its own names locally instead of waiting on an upstream

This commit is contained in:
atlas 2026-11-05 16:04:40 +01:00
commit 89bbeb9757
3 changed files with 48 additions and 13 deletions

View file

@ -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 `<hive-domain>`: 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

View file

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

View file

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