feat(#569): wireguard inter-hive mesh option
Add opt-in WireGuard mesh support to services.hyperhive.swarm: - swarm.peers.<domain>.wireguardPublicKey — peer's wg public key - swarm.peers.<domain>.wireguardEndpoint — peer's UDP endpoint (optional) - swarm.peers.<domain>.wireguardAddress — peer's mesh IP with prefix - swarm.wireguard.enable — bring up wg-hive interface - swarm.wireguard.privateKeyFile — path to host's wg private key - swarm.wireguard.address — this host's mesh IP/prefix - swarm.wireguard.listenPort — UDP listen port (default 51820) - swarm.wireguard.persistentKeepalive — keepalive seconds (default 25) When enabled, generates networking.wireguard.interfaces.wg-hive with one peer entry per mesh-enabled swarm.peers entry. Opens listenPort UDP on the host firewall. Adds wireguard_address to HYPERHIVE_PEERS JSON so hive-c0re can use mesh IPs for intra-swarm routing. Assertions guard against enable=true without privateKeyFile or address. Also refactors networking.firewall.allowedTCPPortRanges from the nested attrset form (which conflicted with the new allowedUDPPorts line) to the per-attribute form. docs/swarm.md: adds WireGuard setup section with key generation commands, two-hive config example, NAT/keepalive notes.
This commit is contained in:
parent
0bff5cd5c3
commit
89609aaa6a
2 changed files with 256 additions and 17 deletions
|
|
@ -93,6 +93,79 @@ services.hyperhive.swarm.peers."pr1ma.example.com" = { certFingerprint = "sha256
|
|||
Mixed trust is fine: A trusts B via CA bundle (no fingerprint), B
|
||||
pins A's self-signed cert.
|
||||
|
||||
## WireGuard inter-hive mesh (optional)
|
||||
|
||||
The peer config above uses public HTTPS for all inter-hive traffic.
|
||||
For private deployments — or to reduce latency and TLS overhead on
|
||||
intra-swarm traffic — hive-c0re can configure a host-to-host
|
||||
WireGuard mesh.
|
||||
|
||||
### Generating keys
|
||||
|
||||
On each hive host:
|
||||
|
||||
```bash
|
||||
wg genkey | install -m 0400 /dev/stdin /etc/wireguard/hive.key
|
||||
wg pubkey < /etc/wireguard/hive.key # → share this with peer operators
|
||||
```
|
||||
|
||||
### Config example (two hives)
|
||||
|
||||
```nix
|
||||
# hive A (pr1ma.example.com, mesh IP 10.100.0.1)
|
||||
services.hyperhive = {
|
||||
swarm.wireguard = {
|
||||
enable = true;
|
||||
privateKeyFile = "/etc/wireguard/hive.key";
|
||||
address = "10.100.0.1/24";
|
||||
listenPort = 51820; # optional, default 51820
|
||||
};
|
||||
|
||||
swarm.peers."edge.corp" = {
|
||||
certFingerprint = "sha256:…"; # TLS trust (unchanged)
|
||||
wireguardPublicKey = "base64key="; # peer's wg pubkey
|
||||
wireguardEndpoint = "203.0.113.42:51820"; # peer's public IP:port
|
||||
wireguardAddress = "10.100.0.2/32"; # peer's mesh IP
|
||||
};
|
||||
};
|
||||
|
||||
# hive B (edge.corp, mesh IP 10.100.0.2)
|
||||
services.hyperhive = {
|
||||
swarm.wireguard = {
|
||||
enable = true;
|
||||
privateKeyFile = "/etc/wireguard/hive.key";
|
||||
address = "10.100.0.2/24";
|
||||
};
|
||||
|
||||
swarm.peers."pr1ma.example.com" = {
|
||||
wireguardPublicKey = "base64key="; # hive A's wg pubkey
|
||||
wireguardEndpoint = "198.51.100.1:51820";
|
||||
wireguardAddress = "10.100.0.1/32";
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### What the mesh does
|
||||
|
||||
- `networking.wireguard.interfaces.wg-hive` is configured on the host
|
||||
(not inside agent containers; containers reach peers via the host's
|
||||
routing table).
|
||||
- UDP port 51820 (or `listenPort`) is opened on the host firewall.
|
||||
- `HYPERHIVE_PEERS` gains a `wireguard_address` field for each mesh
|
||||
peer so hive-c0re can reach intra-swarm services without a public
|
||||
DNS round-trip.
|
||||
- `persistentKeepalive = 25` is set by default; override or null to
|
||||
disable (not needed when both sides have public IPs and no NAT).
|
||||
|
||||
### NAT / one-sided endpoints
|
||||
|
||||
If one host is behind NAT and can't accept incoming connections, only
|
||||
that host needs a null `wireguardEndpoint` on the peer config — the
|
||||
other side initiates. With keepalive on, the NAT hole stays open.
|
||||
|
||||
If both hosts are behind NAT, a STUN relay or a third host (exit node)
|
||||
is required. Out of scope for v0.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- `docs/conventions.md` § Hive identity — env vars, qualified labels
|
||||
|
|
|
|||
|
|
@ -127,6 +127,45 @@ in
|
|||
Encrypt peers). Set to pin a self-signed cert.
|
||||
'';
|
||||
};
|
||||
|
||||
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 when
|
||||
`wireguardPublicKey` is set.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
@ -141,10 +180,74 @@ in
|
|||
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.
|
||||
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, hive-c0re configures a `wg-hive` interface on the host
|
||||
# connecting 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;
|
||||
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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
options.services.hyperhive.c0re = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
|
|
@ -342,14 +445,70 @@ in
|
|||
# Open the per-agent web-port range when the gateway is *off* —
|
||||
# otherwise the gateway nginx is the sole external entry point.
|
||||
# See `docs/gateway.md::Firewall posture (host-level)`.
|
||||
networking.firewall = lib.mkIf (!config.services.hyperhive.gateway.enable) {
|
||||
allowedTCPPortRanges = [
|
||||
{
|
||||
from = 8100;
|
||||
to = 8999;
|
||||
}
|
||||
];
|
||||
};
|
||||
networking.firewall.allowedTCPPortRanges = lib.mkIf (!config.services.hyperhive.gateway.enable) [
|
||||
{
|
||||
from = 8100;
|
||||
to = 8999;
|
||||
}
|
||||
];
|
||||
|
||||
# WireGuard inter-hive mesh. Enabled when
|
||||
# `services.hyperhive.swarm.wireguard.enable = true`. Brings up a
|
||||
# `wg-hive` interface and connects to each peer that has
|
||||
# `wireguardPublicKey` set. Firewall opens the UDP listen port on
|
||||
# the host (not inside containers — this is host-level networking).
|
||||
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.
|
||||
networking.firewall.allowedUDPPorts = lib.mkIf config.services.hyperhive.swarm.wireguard.enable [
|
||||
config.services.hyperhive.swarm.wireguard.listenPort
|
||||
];
|
||||
|
||||
assertions = lib.mkIf 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").
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
systemd.services.hive-c0re = {
|
||||
description = "hyperhive coordinator daemon";
|
||||
|
|
@ -429,15 +588,22 @@ in
|
|||
HIVE_FORGE_PUBLIC_URL = "https://${config.services.hyperhive.forge.domain}";
|
||||
}
|
||||
// lib.optionalAttrs (config.services.hyperhive.swarm.peers != { }) {
|
||||
# Peer hives serialised as a JSON array of {domain, cert_fingerprint}
|
||||
# objects. Consumed by hive-ag3nt::identity::peers() + the dashboard's
|
||||
# peer_hives StateSnapshot field (P33RS tab). Domain is the attrset key;
|
||||
# cert_fingerprint is null for CA-trusted peers.
|
||||
# Peer hives serialised as a JSON array of {domain, cert_fingerprint,
|
||||
# wireguard_address?} objects. Consumed by hive-ag3nt::identity::peers()
|
||||
# + the dashboard's peer_hives StateSnapshot field (P33RS tab). Domain
|
||||
# is the attrset key; cert_fingerprint is null for CA-trusted peers;
|
||||
# wireguard_address is omitted when not part of the mesh.
|
||||
HYPERHIVE_PEERS = builtins.toJSON (
|
||||
lib.mapAttrsToList (domain: p: {
|
||||
inherit domain;
|
||||
cert_fingerprint = p.certFingerprint;
|
||||
}) config.services.hyperhive.swarm.peers
|
||||
lib.mapAttrsToList (
|
||||
domain: p:
|
||||
{
|
||||
inherit domain;
|
||||
cert_fingerprint = p.certFingerprint;
|
||||
}
|
||||
// lib.optionalAttrs (p.wireguardAddress != null) {
|
||||
wireguard_address = p.wireguardAddress;
|
||||
}
|
||||
) config.services.hyperhive.swarm.peers
|
||||
);
|
||||
};
|
||||
serviceConfig = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue