From 57459cb6d8aa9af4626aa998e92de6266195978d Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 31 Jul 2026 18:42:37 +0200 Subject: [PATCH] refactor(#2862): split the wireguard mesh out of swarm.nix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mara asked, and the file had already stopped being one thing: after the gate moved off c0re.enable, swarm.nix held two concerns with different audiences and different gates. swarm.nix now declares WHO the peers are — data hive-c0re serialises into HYPERHIVE_PEERS and the dashboard renders. Declaration only, no config block. swarm-wireguard.nix owns the mesh: assertions, the wg-hive interface, the firewall port. That is plain host networking, and a machine which runs no hive at all — the snapshot store — still needs it. Under the old layout a reader could not tell which half of swarm.nix applied to a non-hive host. The two stay coupled by data, not by structure: the per-peer wireguard* fields stay on the peer submodule, because that is where a peer is described, and the mesh module reads them. No behaviour change — same options, same gate, same rendered config. --- nix/host-modules/default.nix | 1 + nix/host-modules/swarm-wireguard.nix | 142 +++++++++++++++++++++++++++ nix/host-modules/swarm.nix | 136 ++----------------------- 3 files changed, 150 insertions(+), 129 deletions(-) create mode 100644 nix/host-modules/swarm-wireguard.nix diff --git a/nix/host-modules/default.nix b/nix/host-modules/default.nix index 79647d04..ebd10e31 100644 --- a/nix/host-modules/default.nix +++ b/nix/host-modules/default.nix @@ -22,6 +22,7 @@ ./hive-snapshot-store.nix ./hive-tls.nix ./otel.nix + ./swarm-wireguard.nix ./swarm.nix ]; } diff --git a/nix/host-modules/swarm-wireguard.nix b/nix/host-modules/swarm-wireguard.nix new file mode 100644 index 00000000..4433de13 --- /dev/null +++ b/nix/host-modules/swarm-wireguard.nix @@ -0,0 +1,142 @@ +# The WireGuard inter-hive mesh for the local host. Split out of +# ./swarm.nix because the two are different concerns with different +# audiences: that file declares WHO the peers are (data hive-c0re +# serialises into HYPERHIVE_PEERS and the dashboard renders), while +# this one is plain host networking that a machine which runs no hive +# at all --- the snapshot store, for one --- still needs. +# +# The two stay coupled by data, not by structure: the per-peer +# `wireguard*` fields live on the peer submodule in ./swarm.nix, since +# that is where a peer is described, and this module reads them. +{ + lib, + config, + ... +}: +{ + # WireGuard mesh config for the local host. + # When enabled, a `wg-hive` interface connects to all peers that have + # `wireguardPublicKey` declared. Peers reachable over the mesh are + # preferred for inter-hive traffic (no public TLS round-trip needed); + # peers without a public key still work via normal HTTPS. + options.services.hyperhive.swarm.wireguard = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Enable the WireGuard inter-hive mesh. When true, a `wg-hive` + interface is brought up connecting to all swarm peers that + declare a `wireguardPublicKey`. Requires + `privateKeyFile` to be set. + ''; + }; + + privateKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/etc/wireguard/hive.key"; + description = '' + Path to the host's WireGuard private key file. The file must + be readable by root and should have mode 0400. Generate with + `wg genkey > /etc/wireguard/hive.key`. Required when + `swarm.wireguard.enable = true`. + ''; + }; + + address = lib.mkOption { + type = lib.types.str; + default = ""; + example = "10.100.0.1/24"; + description = '' + IP address (with prefix) of this host on the WireGuard mesh. + Use a /24 (or broader) prefix so the routing table covers all + peer /32 routes. Example: `"10.100.0.1/24"` for a 256-host mesh. + ''; + }; + + listenPort = lib.mkOption { + type = lib.types.port; + default = 51820; + description = '' + UDP port the local WireGuard interface listens on. Must be + reachable from peer hosts when they initiate the tunnel. + Default: 51820 (standard WireGuard port). + ''; + }; + + persistentKeepalive = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = 25; + example = 25; + description = '' + Seconds between keepalive packets sent to each peer. Useful + when this host (or a peer) is behind NAT — keeps the UDP hole + open. Set to null to disable. Default: 25 seconds. + ''; + }; + }; + + # Gated on the mesh itself, NOT on the c0re daemon. The mesh is host + # networking, not a c0re feature: a swarm host that runs no hive — + # the snapshot store, for one — still has to join the mesh, and under + # the old `c0re.enable` gate it silently got no `wg-hive` interface + # at all. Nothing below is c0re-specific; the peer data + # c0re consumes (HYPERHIVE_PEERS / HIVE_PEER_CA_PATHS) is rendered in + # ./hive-c0re and stays gated there. + config = lib.mkIf config.services.hyperhive.swarm.wireguard.enable { + assertions = [ + { + assertion = config.services.hyperhive.swarm.wireguard.privateKeyFile != null; + message = '' + services.hyperhive.swarm.wireguard.enable requires + services.hyperhive.swarm.wireguard.privateKeyFile to be set. + Generate a key: wg genkey > /etc/wireguard/hive.key + ''; + } + { + assertion = config.services.hyperhive.swarm.wireguard.address != ""; + message = '' + services.hyperhive.swarm.wireguard.enable requires + services.hyperhive.swarm.wireguard.address to be set + (e.g. "10.100.0.1/24"). + ''; + } + ]; + + # WireGuard inter-hive mesh. Brings up a `wg-hive` interface and + # connects to each peer that has `wireguardPublicKey` set. + networking.wireguard.interfaces = + let + wgCfg = config.services.hyperhive.swarm.wireguard; + meshPeers = lib.filterAttrs ( + _: p: p.wireguardPublicKey != null && p.wireguardAddress != null + ) config.services.hyperhive.swarm.peers; + in + { + wg-hive = { + ips = [ wgCfg.address ]; + listenPort = wgCfg.listenPort; + privateKeyFile = wgCfg.privateKeyFile; + peers = lib.mapAttrsToList ( + _domain: p: + { + publicKey = p.wireguardPublicKey; + allowedIPs = [ p.wireguardAddress ]; + } + // lib.optionalAttrs (p.wireguardEndpoint != null) { + endpoint = p.wireguardEndpoint; + } + // lib.optionalAttrs (wgCfg.persistentKeepalive != null) { + persistentKeepalive = wgCfg.persistentKeepalive; + } + ) meshPeers; + }; + }; + + # Open the WireGuard UDP port on the host firewall (host-level + # networking — not inside containers). + networking.firewall.allowedUDPPorts = [ + config.services.hyperhive.swarm.wireguard.listenPort + ]; + }; +} diff --git a/nix/host-modules/swarm.nix b/nix/host-modules/swarm.nix index 36152c4c..00465c78 100644 --- a/nix/host-modules/swarm.nix +++ b/nix/host-modules/swarm.nix @@ -1,8 +1,11 @@ -# Swarm peering: the peer-hive declarations and the optional -# WireGuard inter-hive mesh. The peers are serialised into hive-c0re's +# Swarm peering: who the peer hives are. Serialised into hive-c0re's # environment (HYPERHIVE_PEERS / HIVE_PEER_CA_PATHS — see ./hive-c0re) -# and consumed by identity.rs + the dashboard's P33RS tab; the mesh -# config below is host-level networking. +# and consumed by identity.rs + the dashboard's P33RS tab. +# +# Declaration only — this module has no `config` block. The mesh that +# uses the `wireguard*` fields below lives in ./swarm-wireguard.nix, +# because bringing up an interface is host networking rather than swarm +# bookkeeping, and a host that runs no hive still needs it. { lib, config, @@ -122,129 +125,4 @@ ''; }; - # WireGuard mesh config for the local host. - # When enabled, a `wg-hive` interface connects to all peers that have - # `wireguardPublicKey` declared. Peers reachable over the mesh are - # preferred for inter-hive traffic (no public TLS round-trip needed); - # peers without a public key still work via normal HTTPS. - options.services.hyperhive.swarm.wireguard = { - enable = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Enable the WireGuard inter-hive mesh. When true, a `wg-hive` - interface is brought up connecting to all swarm peers that - declare a `wireguardPublicKey`. Requires - `privateKeyFile` to be set. - ''; - }; - - privateKeyFile = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; - example = "/etc/wireguard/hive.key"; - description = '' - Path to the host's WireGuard private key file. The file must - be readable by root and should have mode 0400. Generate with - `wg genkey > /etc/wireguard/hive.key`. Required when - `swarm.wireguard.enable = true`. - ''; - }; - - address = lib.mkOption { - type = lib.types.str; - default = ""; - example = "10.100.0.1/24"; - description = '' - IP address (with prefix) of this host on the WireGuard mesh. - Use a /24 (or broader) prefix so the routing table covers all - peer /32 routes. Example: `"10.100.0.1/24"` for a 256-host mesh. - ''; - }; - - listenPort = lib.mkOption { - type = lib.types.port; - default = 51820; - description = '' - UDP port the local WireGuard interface listens on. Must be - reachable from peer hosts when they initiate the tunnel. - Default: 51820 (standard WireGuard port). - ''; - }; - - persistentKeepalive = lib.mkOption { - type = lib.types.nullOr lib.types.int; - default = 25; - example = 25; - description = '' - Seconds between keepalive packets sent to each peer. Useful - when this host (or a peer) is behind NAT — keeps the UDP hole - open. Set to null to disable. Default: 25 seconds. - ''; - }; - }; - - # Gated on the mesh itself, NOT on the c0re daemon. The mesh is host - # networking, not a c0re feature: a swarm host that runs no hive — - # the snapshot store, for one — still has to join the mesh, and under - # the old `c0re.enable` gate it silently got no `wg-hive` interface - # at all. Nothing below is c0re-specific; the peer data - # c0re consumes (HYPERHIVE_PEERS / HIVE_PEER_CA_PATHS) is rendered in - # ./hive-c0re and stays gated there. - config = lib.mkIf config.services.hyperhive.swarm.wireguard.enable { - assertions = [ - { - assertion = config.services.hyperhive.swarm.wireguard.privateKeyFile != null; - message = '' - services.hyperhive.swarm.wireguard.enable requires - services.hyperhive.swarm.wireguard.privateKeyFile to be set. - Generate a key: wg genkey > /etc/wireguard/hive.key - ''; - } - { - assertion = config.services.hyperhive.swarm.wireguard.address != ""; - message = '' - services.hyperhive.swarm.wireguard.enable requires - services.hyperhive.swarm.wireguard.address to be set - (e.g. "10.100.0.1/24"). - ''; - } - ]; - - # WireGuard inter-hive mesh. Brings up a `wg-hive` interface and - # connects to each peer that has `wireguardPublicKey` set. - networking.wireguard.interfaces = - let - wgCfg = config.services.hyperhive.swarm.wireguard; - meshPeers = lib.filterAttrs ( - _: p: p.wireguardPublicKey != null && p.wireguardAddress != null - ) config.services.hyperhive.swarm.peers; - in - { - wg-hive = { - ips = [ wgCfg.address ]; - listenPort = wgCfg.listenPort; - privateKeyFile = wgCfg.privateKeyFile; - peers = lib.mapAttrsToList ( - _domain: p: - { - publicKey = p.wireguardPublicKey; - allowedIPs = [ p.wireguardAddress ]; - } - // lib.optionalAttrs (p.wireguardEndpoint != null) { - endpoint = p.wireguardEndpoint; - } - // lib.optionalAttrs (wgCfg.persistentKeepalive != null) { - persistentKeepalive = wgCfg.persistentKeepalive; - } - ) meshPeers; - }; - }; - - # Open the WireGuard UDP port on the host firewall (host-level - # networking — not inside containers). - networking.firewall.allowedUDPPorts = [ - config.services.hyperhive.swarm.wireguard.listenPort - ]; - }; }