# Swarm peering: the peer-hive declarations and the optional # WireGuard inter-hive mesh. The peers 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. { lib, config, ... }: { # Peer hives in the same swarm. Each entry declares a remote hive # reachable from this host. options.services.hyperhive.swarm.peers = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule { options = { certFingerprint = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "sha256:b1946ac92492d2347c6235b4d2611184a3f5b6cae6c19d6e3c2f0a8e7d4c9f12"; description = '' Expected TLS certificate fingerprint for this peer's HTTPS endpoint. Null = trust the system CA bundle (for Let's Encrypt peers). Set to pin a self-signed cert. Format: the literal `sha256:` followed by exactly 64 hex digits (case-insensitive, no colon separators) — the SHA-256 digest of the peer's DER-encoded leaf certificate. Generate with `openssl x509 -noout -fingerprint -sha256`, then strip the colons and prepend `sha256:`. A malformed value is ignored with a warning rather than weakening trust. See docs/swarm.md for the full recipe. Scopes only to hive-c0re's own peer HTTPS checks — it does NOT help Matrix federation (tuwunel validates against its container trust bundle). For a self-signed peer whose root CA you want trusted hive-wide (every agent + Matrix federation), set `caCert` below. ''; }; caCert = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; example = "./peers/edge-ca.pem"; description = '' Path to this peer hive's root CA certificate (PEM). When set, the CA is embedded (at build time, into the nix store — no runtime file on the host) and trusted **everywhere the hive's own internal CA is**: it rides alongside `hive-ca.pem` in each agent's `security.pki.certificateFiles` (via the meta-flake renderer), and is added to the Matrix homeserver container's trust bundle so tuwunel validates *federation* TLS from a self-signed peer hive whose cert chains to it. This is the CA-trust path that `certFingerprint` (leaf-pinning, c0re-only) can't cover, and is what unblocks Matrix federation with a self-signed peer hive. Trust stays inside the hive (agents + the Matrix container), never the host system trust store. Mutually complementary with `certFingerprint`; set `caCert` for the federation case. See docs/swarm.md. ''; }; wireguardPublicKey = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "base64pubkey="; description = '' WireGuard public key for this peer host. Required when `services.hyperhive.swarm.wireguard.enable = true` and you want this peer reachable over the mesh. Null = TLS- only peering (public internet, no mesh tunnel). ''; }; wireguardEndpoint = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "203.0.113.1:51820"; description = '' WireGuard endpoint for this peer in `host:port` form. Required when the peer host is behind a firewall and this host needs to initiate the tunnel. Null = this host waits for the peer to connect (peer-initiates; peer must have an endpoint pointing back at this host). ''; }; wireguardAddress = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "10.100.0.2/32"; description = '' IP address (with prefix) of the peer host on the WireGuard mesh. Used as the `allowedIPs` for the peer's WireGuard config entry and injected into `HYPERHIVE_PEERS` so hive-c0re can route intra-swarm traffic to the mesh address rather than the public domain. Required to include the peer in the WireGuard mesh (peers missing this field are silently excluded from `wg-hive`). ''; }; }; } ); default = { }; example = { "lab.example.com" = { certFingerprint = "sha256:b1946ac92492d2347c6235b4d2611184a3f5b6cae6c19d6e3c2f0a8e7d4c9f12"; }; "edge.corp" = { }; }; description = '' Peer hives in the same swarm. The attrset key is the peer's DNS domain — used for dashboard links and Matrix federation discovery. Null `certFingerprint` trusts the system CA bundle; set it to pin a self-signed TLS cert. Add `wireguardPublicKey` + `wireguardAddress` (and optionally `wireguardEndpoint`) to include the peer in the WireGuard mesh when `swarm.wireguard.enable = true`. ''; }; # 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 c0re daemon being enabled (the historical shape — the # mesh is part of the coordinator host's networking). config = lib.mkIf config.services.hyperhive.c0re.enable { assertions = lib.optionals config.services.hyperhive.swarm.wireguard.enable [ { 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 = lib.mkIf config.services.hyperhive.swarm.wireguard.enable ( 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 when the mesh is # on (host-level networking — not inside containers). networking.firewall.allowedUDPPorts = lib.mkIf config.services.hyperhive.swarm.wireguard.enable [ config.services.hyperhive.swarm.wireguard.listenPort ]; }; }