{ lib, config, ... }: let cfg = config.services.hyperhive.network; in { # Hive-internal network — host-side bridge + per-agent DNS resolver. # Always active when hyperhive is enabled: agent containers run in # private netns behind the bridge. Full design: docs/network.md. options.services.hyperhive.network = { enable = lib.mkOption { type = lib.types.bool; default = config.services.hyperhive.enable; defaultText = lib.literalExpression "config.services.hyperhive.enable"; example = false; description = '' **DEPRECATED — ignored.** The hive network (bridge + dnsmasq resolver + private-netns isolation) is now always on whenever hyperhive is enabled; setting this to `false` warns and has no effect. Retained as a no-op so existing configs eval; will be removed in a future release. The network requires `services.hyperhive.domain` to be set — the dnsmasq resolver is authoritative for `` and its sub-domains. A bridge interface (`bridgeName`) appears on the host with `bridgeIp` assigned, the hive-gateway container runs a dnsmasq on that IP, and each agent container runs in a private netns with a veth pair on the bridge. ''; }; bridgeName = lib.mkOption { type = lib.types.str; default = "hive-br0"; example = "h0"; description = '' Name of the host-side bridge interface the hive uses for inter-container traffic. Kept short so it survives the IFNAMSIZ (15-char) cap, and prefixed so it's obviously hive-managed in `ip link` output. ''; }; bridgeIp = lib.mkOption { type = lib.types.str; default = "10.42.0.1"; example = "172.30.0.1"; description = '' IPv4 address assigned to the bridge interface on the host side. Agents use this address as their DNS server (dnsmasq in the gateway container binds here). Default `10.42.0.1` is in RFC 1918 space and unlikely to clash with operator's existing setup; override if a different range is already in use. ''; }; bridgePrefixLength = lib.mkOption { type = lib.types.int; default = 24; example = 16; description = '' Netmask prefix length for the bridge subnet. Default `/24` gives 254 usable per-agent addresses, enough for any single-host hive. Operator with a larger swarm or a tighter addressing scheme overrides. ''; }; upstreamDns = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "1.1.1.1" "9.9.9.9" ]; example = [ "192.168.1.1" "8.8.8.8" ]; description = '' Upstream DNS servers dnsmasq forwards non-hive queries to. Defaults to Cloudflare + Quad9. Override for operators on private networks who need a specific resolver (corporate DNS, pi-hole, etc.). The hive resolver itself stays authoritative for `` and its sub-domains regardless of upstream choice. ''; }; exposeHostPorts = lib.mkOption { type = lib.types.listOf lib.types.port; default = [ ]; example = [ 4318 ]; description = '' TCP ports on the host that agent containers may reach at the bridge IP (`bridgeIp`). Each listed port `P` is opened on the bridge-interface firewall, so an agent can connect to `''${bridgeIp}:P` (default `10.42.0.1:P`). Use this to let agents reach a host-local service — e.g. an OpenTelemetry collector for `services.hyperhive.otel.endpoint` (set `endpoint = "http://''${bridgeIp}:P"`). **The host service must bind an address reachable from the bridge** — `0.0.0.0` or the bridge IP (`bridgeIp`) — not loopback-only. The bridge→`127.0.0.0/8` DROP rule (defence-in-depth) is unchanged: this only opens the firewall, it does not bridge loopback. A service that binds `127.0.0.1` only is still unreachable; rebind it to `0.0.0.0`. The exposed port is reachable by EVERY agent on the bridge subnet (same as DNS/gateway), so only expose services safe for any agent to reach. ''; }; isolateContainers = lib.mkOption { type = lib.types.bool; default = true; example = true; description = '' **DEPRECATED — ignored.** Network isolation is now the only mode and is always on whenever hyperhive is enabled; the shared-netns path was removed. This option is retained as a no-op so existing configs eval; setting it to `false` warns and has no effect. It will be removed in a future release. ''; }; }; config = lib.mkMerge [ # The hive network + container isolation are unconditional whenever # hyperhive is enabled: the shared-netns mode was removed, so there is # one mode (private netns behind the bridge). `network.enable` and # `isolateContainers` are kept as deprecated no-op options (see the # warnings block below) so existing configs that set them still eval. (lib.mkIf config.services.hyperhive.enable { assertions = [ { assertion = config.services.hyperhive.domain != null; message = '' hyperhive requires services.hyperhive.domain to be set — the hive resolver is authoritative for `` and its sub-domains, and agents reach the forge/matrix through the gateway by that domain. Pin a hostname (`services.hyperhive.domain = "example.com";`). ''; } ]; # Virtual bridge — each agent container attaches a veth pair (isolation # is unconditional now). networking.bridges.${cfg.bridgeName}.interfaces = [ ]; # Bridge IP — dnsmasq (in the gateway container) binds here. networking.interfaces.${cfg.bridgeName}.ipv4.addresses = [ { address = cfg.bridgeIp; prefixLength = cfg.bridgePrefixLength; } ]; # DNS only on the bridge interface — no external amplification surface. networking.firewall.interfaces.${cfg.bridgeName} = { allowedUDPPorts = [ 53 ]; allowedTCPPorts = [ 53 ]; }; }) # Container isolation overlay — now unconditional (the shared-netns # mode was removed). See docs/network.md#container-isolation. (lib.mkIf config.services.hyperhive.enable { # Agents route internet traffic via the bridge; NAT masquerades their RFC-1918 IPs. boot.kernel.sysctl."net.ipv4.ip_forward" = 1; networking.nat = { enable = true; internalInterfaces = [ cfg.bridgeName ]; }; # Defence-in-depth: DROP bridge→loopback so compromised agents can't # reach host-loopback services even via routing table leaks. networking.firewall.extraInputRules = '' ip saddr ${cfg.bridgeIp}/${toString cfg.bridgePrefixLength} ip daddr 127.0.0.0/8 drop ''; # Allow isolated agents to reach the gateway (nginx on the host, shared # netns). Port 80 covers `http://forge.`, per-agent UI proxies, # and any other HTTP services the gateway fronts. Port 443 for HTTPS. # (`exposeHostPorts` opens its own ports in its dedicated block below, # co-located with the proxies so the firewall hole + listener can't drift.) networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = [ 80 443 ]; # Tells hive-c0re to pass PRIVATE_NETWORK + bridge settings to each # container. HIVE_NETWORK_SUBNET is host-bridge IP/prefix, not canonical # network address — the Rust side normalises before subnet arithmetic. systemd.services.hive-c0re.environment = { HIVE_NETWORK_ISOLATION = "1"; HIVE_NETWORK_BRIDGE = cfg.bridgeName; HIVE_NETWORK_SUBNET = "${cfg.bridgeIp}/${toString cfg.bridgePrefixLength}"; }; }) # Host port exposure: open each `exposeHostPorts` entry on the bridge # firewall so agents can reach a host service at `:P`. The host # service must bind `0.0.0.0` or the bridge IP (a loopback-only bind stays # unreachable — the bridge→127.0.0.0/8 DROP rule above is unchanged). This # is firewall-only by design: a host service that binds `0.0.0.0` already # serves the bridge IP, so an extra bridge-IP proxy would only collide # (EADDRINUSE) with it. Merges with the [ 80 443 ] gateway ports above. (lib.mkIf (config.services.hyperhive.enable && cfg.exposeHostPorts != [ ]) { networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = cfg.exposeHostPorts; }) # Deprecation surface for the removed toggles. Both options are kept so # existing configs that set them to `true` still eval cleanly; setting # either to `false` no longer does anything (network + isolation are # unconditional now), so warn rather than silently ignore. { # Only warn when hyperhive itself is enabled — otherwise `cfg.enable` # defaults to `false` (tracking `hyperhive.enable`) and we'd fire a # spurious deprecation warning on a host that doesn't run hyperhive. warnings = lib.optionals config.services.hyperhive.enable ( lib.optional (!cfg.enable) '' services.hyperhive.network.enable = false is deprecated and ignored — the hive network is now always on (private-netns isolation is the only mode). Remove the setting. '' ++ lib.optional (!cfg.isolateContainers) '' services.hyperhive.network.isolateContainers = false is deprecated and ignored — network isolation is now the only mode and is always on. Remove the setting. '' ); } ]; }