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 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 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 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 own resolvers: it runs on the host and reads the host's
`/etc/resolv.conf` directly. Containers don't need to know the `/etc/resolv.conf` directly. Containers don't need to know the
upstream — they query the bridge IP and dnsmasq does the right thing upstream — they query the bridge IP and dnsmasq does the right thing

View file

@ -11,6 +11,11 @@
networkCfg, networkCfg,
hyperhiveDomain, 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; enable = true;
# ON for its *plumbing*, not for the address it publishes. # ON for its *plumbing*, not for the address it publishes.
@ -52,18 +57,25 @@
# Hive authoritative records — answer queries for the hive domain # Hive authoritative records — answer queries for the hive domain
# + its sub-domains with the bridge IP, where nginx is reachable # + its sub-domains with the bridge IP, where nginx is reachable
# from every container netns. # from every container netns.
address = #
lib.optional (hyperhiveDomain != null) "/${hyperhiveDomain}/${networkCfg.bridgeIp}" # Names contributed by the modules that own them
# Names contributed by the modules that own them # (`gateway.localNames`) get the same address — the bridge IP is the
# (`gateway.localNames`). Same address as everything above — the # gateway's answer for anything it fronts, and a contributing module
# bridge IP is the gateway's answer for anything it fronts, and a # neither knows nor should know it.
# contributing module neither knows nor should know it. #
# # `unique` is not tidiness: two modules claiming one name would
# `unique` is not tidiness: two modules claiming one name would # otherwise emit two `address=` rules for it, and dnsmasq resolves
# otherwise emit two `address=` rules for it, and dnsmasq resolves # that by precedence rather than by complaining. An assertion in
# that by precedence rather than by complaining. An assertion in # ./default.nix makes the collision loud instead.
# ./default.nix makes the collision loud instead. address = map (name: "/${name}/${networkCfg.bridgeIp}") localDomains;
++ map (name: "/${name}/${networkCfg.bridgeIp}") (lib.unique cfg.localNames); # 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 # DHCP pool covering all usable host addresses on the bridge
# subnet — bounds computed by hive-network.nix from # subnet — bounds computed by hive-network.nix from
# bridgeIp/bridgePrefixLength. All containers (agents and service # bridgeIp/bridgePrefixLength. All containers (agents and service

View file

@ -364,6 +364,21 @@ let
&& allLocal.services.hyperhive.otel.collector.host == "hive-collector.hive" && allLocal.services.hyperhive.otel.collector.host == "hive-collector.hive"
&& lib.elem "/hive-collector.hive/${bridge}" allLocal.services.dnsmasq.settings.address; && 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 # The absence arm, and what makes the agents' queue address cases above able
# to fail. # to fail.