hyperhive/docs/network.md
atlas ed3b9d853e nix/hive-network: bridge + dnsmasq resolver, opt-in (#805 v1)
Stand up the host-side bridge interface + per-agent DNS resolver
ahead of #14 (netns isolation). Mara on #805#11541: "we need it
before #14 so nothing breaks when we switch over". v1 ships the
endpoint live but containers stay on shared host netns — when #14
flips them to private netns the DNS contract is already there.

Shape:

- new `nix/modules/hive-network.nix` with `services.hyperhive.network.*`
  options (enable + bridgeName + bridgeIp + bridgePrefixLength +
  upstreamDns). Default off. Imported from `hive-c0re.nix`.
- bridge interface via `networking.bridges` (no slave NICs at v1;
  per-agent veth pairs attach once #14 lands).
- bridge IP assigned via `networking.interfaces`.
- `networking.firewall.interfaces.<bridge>.allowed{UDP,TCP}Ports =
  [ 53 ]` opens the resolver on the bridge interface only —
  other interfaces stay closed.
- dnsmasq config added to the existing `hive-gateway` container
  (mara on #805:10957: "put the resolver into the gateway container").
  Listens only on `bridgeName` + `lo`; authoritative for
  `<hive-domain>`, `forge.<hive>`, `matrix.<hive>` answering with
  the bridge IP; forwards everything else to upstream.
  `resolveLocalQueries = false` keeps the gateway container's own
  resolver untouched.

Asserts `services.hyperhive.domain != null` + `gateway.enable =
true` — both required for the resolver to be meaningful.

Docs: new `docs/network.md` covering v1 vs v2 split, container shape
rationale, default addressing, resolver behaviour, firewall posture.

`nix flake check` clean.
2026-05-31 16:56:59 +02:00

4.5 KiB

hive-network

Host-side bridge + per-agent DNS resolver — the foundation that makes #14 container netns isolation safe to land. Configured via services.hyperhive.network.*; off by default during rollout.

Why ship before #14

Mara on #805#issuecomment-11541: "we need it before #14 so nothing breaks when we switch over". If netns isolation lands first, agent containers lose /etc/resolv.conf propagation from the host and DNS breaks until a separate resolver is up. Inverting the sequence — bridge + dnsmasq first, netns flip second — makes the flag day boring: the resolver endpoint is already live, agents just discover it via veth instead of shared netns.

v1 vs v2

feature v1 (this PR) v2 (after #14)
bridge interface created on host, no slave NICs per-agent veth pairs attach
dnsmasq binding bridge IP (reachable via host loopback in shared netns) bridge IP (reachable via veth in private netns)
agent container netns shared host private
agent /etc/resolv.conf unchanged (host DNS) nameserver <bridge-ip>
address rules target <bridge-ip> (works in both modes) unchanged from v1

The address rules ship pointing at the bridge IP from v1 so the DNS contract is fixed before any container actually depends on it — minimises the things that flip on #14 day.

Container shape (where dnsmasq lives)

Co-located in the existing hive-gateway container per mara on #805:10957 — single front-door for both DNS and HTTP, saves a sibling container, single systemd-unit / state surface to monitor. The gateway shares host netns (privateNetwork = false) so dnsmasq's bind-interfaces listener on bridgeIp works without any veth gymnastics today; when #14 flips agent containers to private netns the binding doesn't change (it's still on the host's bridge interface).

Configuration

{
  services.hyperhive = {
    enable = true;
    domain = "darkest.space";
    network.enable = true;            # opt in to bridge + DNS
    network.bridgeIp = "10.42.0.1";   # default
    network.upstreamDns = [            # default Cloudflare + Quad9
      "1.1.1.1"
      "9.9.9.9"
    ];
  };
}

Asserts services.hyperhive.domain != null (resolver needs a domain to be authoritative for) + services.hyperhive.gateway.enable = true (resolver lives in the gateway container).

Bridge addressing

Default subnet is 10.42.0.0/24, host-side gateway at 10.42.0.1. RFC 1918 space, unlikely to clash with operator's existing setup; override bridgeIp + bridgePrefixLength if a different range is already in use. /24 gives 254 usable per-agent addresses — enough for any single-host hive; bigger swarms or tighter addressing schemes pick their own.

Resolver behaviour

dnsmasq is authoritative for the hive's own zones — answers <hive-domain>, forge.<hive-domain>, matrix.<hive-domain> queries with the bridge IP (where nginx is reachable). Everything else gets forwarded to upstreamDns. Containers don't need to know the upstream — they query the bridge IP and dnsmasq does the right thing per-name.

bind-interfaces + interface = [ bridgeName "lo" ] means the listener only accepts queries from the bridge interface (plus lo for container health-checks). External hosts can't reach it — no DNS-amplification surface even when the operator opens port 80 for gateway HTTP.

resolveLocalQueries = false keeps dnsmasq out of the host's own resolution stack — the host's resolver (systemd-resolved, plain glibc nss, dnscrypt-proxy, etc.) keeps doing whatever the operator configured. The hive resolver is purely for inbound queries from agent containers.

Firewall posture

networking.firewall.interfaces.<bridge>.allowedUDPPorts = [ 53 ]

  • allowedTCPPorts = [ 53 ] opens the resolver on the bridge interface only. Other interfaces stay closed. The hive resolver isn't an external-facing service.

Sequencing history

  • mara on #805 (comment-10957): "can we put the resolver into the gateway container?" — yes, this v1 does that.
  • mara on #805 (comment-11541): "we need it before #14 so nothing breaks when we switch over" — flipped the dependency direction; v1 ships now, #14 flips containers later.

Cross-references

  • Issue #805 — DNS resolver tracking
  • Issue #14 — netns isolation (downstream consumer)
  • docs/gateway.md — vhost map + the gateway container's other duties