deploy: move the wireguard mesh out of the namespace hives read

`swarm.*` is what a hive needs to be a *client* of the swarm; the mesh is
none of it. A peer needs this host's `wireguardEndpoint` -- the roster entry
in swarm.nix, which stays -- and nothing about the interface this host
brings up. The module already said so: "plain host networking that a machine
which runs no hive at all still needs."

All five options move, so the namespace relocates rather than splitting.
`listenPort` is the one that reads the other way: it is what this host
*binds*, while the port a peer *dials* lives inside `wireguardEndpoint`.

Declared in swarm-wireguard.nix under the `deploy.*` path, following
swarm-victorialogs.nix; deploy.nix carries only the renames, per its own
"a single file to delete when the deprecation window closes". Deliberately
NOT added to deploy.nix's own options block: every entry there is a swarm
service this host deploys, and the mesh is host networking.

hivectl/src/wg.rs generates the config snippet an operator pastes, so it
moves too -- otherwise the tool's own output trips the deprecation warning.

module-eval gains a case that configures a host through the OLD path and
asserts the rendered wg-hive interface, because the new path evaluates
fine without the shim: dropping it reads as a clean tree.
This commit is contained in:
atlas 2026-09-07 00:54:45 +02:00 committed by mara
commit 368f5d82aa
10 changed files with 85 additions and 27 deletions

View file

@ -148,6 +148,34 @@ in
[ "services" "hyperhive" "tls" "leafValidityDays" ]
[ "services" "hyperhive" "deploy" "hive-controller" "tls" "leafValidityDays" ]
)
# The WireGuard mesh, whole. Unlike every rename above this one moves a
# namespace rather than a toggle: nothing under it is a fact another hive
# reads. A peer needs this host's `wireguardEndpoint` — the roster entry
# in ./swarm.nix, which stays — and nothing about the interface this host
# brings up. `listenPort` moves for the same reason and is easy to read
# the other way: it is what this host *binds*, while the port a peer
# *dials* is the one inside `wireguardEndpoint`.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "wireguard" "enable" ]
[ "services" "hyperhive" "deploy" "wireguard" "enable" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "wireguard" "privateKeyFile" ]
[ "services" "hyperhive" "deploy" "wireguard" "privateKeyFile" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "wireguard" "address" ]
[ "services" "hyperhive" "deploy" "wireguard" "address" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "wireguard" "listenPort" ]
[ "services" "hyperhive" "deploy" "wireguard" "listenPort" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "wireguard" "persistentKeepalive" ]
[ "services" "hyperhive" "deploy" "wireguard" "persistentKeepalive" ]
)
];
# ⚠️ `deploy.forgejo` is declared in ./hive-ci.nix, not here, and it is the

View file

@ -26,9 +26,9 @@
}:
let
cfg = config.services.hyperhive.snapshotStore;
wgCfg = config.services.hyperhive.swarm.wireguard;
wgCfg = config.services.hyperhive.deploy.wireguard;
# `swarm.wireguard.address` carries a prefix ("10.100.0.1/24") because
# `deploy.wireguard.address` carries a prefix ("10.100.0.1/24") because
# it feeds `networking.wireguard.interfaces.wg-hive.ips`. A listen
# address must be the bare IP, so strip it.
meshAddress = lib.head (lib.splitString "/" wgCfg.address);
@ -103,7 +103,7 @@ in
mesh. Off by default --- it is a distinct deployment role, not
part of a hive.
Requires `services.hyperhive.swarm.wireguard.enable`: the mesh
Requires `services.hyperhive.deploy.wireguard.enable`: the mesh
is both the transport and the authentication, so there is no
meaningful configuration without it.
'';
@ -138,7 +138,7 @@ in
assertion = wgCfg.enable;
message = ''
services.hyperhive.snapshotStore.enable requires
services.hyperhive.swarm.wireguard.enable --- the mesh is the
services.hyperhive.deploy.wireguard.enable --- the mesh is the
store's transport AND its authentication (cryptokey routing
binds a peer's source address to its public key). Without it
there is nothing to bind the listener to and no way to tell
@ -149,7 +149,7 @@ in
assertion = wgCfg.address != "";
message = ''
services.hyperhive.snapshotStore.enable requires
services.hyperhive.swarm.wireguard.address to be set --- the
services.hyperhive.deploy.wireguard.address to be set --- the
receiver binds to this host's mesh address, and refuses to
fall back to a wildcard.
'';

View file

@ -8,6 +8,14 @@
# 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.
#
# Everything declared here is `deploy.*`, not `swarm.*`: by ./deploy.nix's
# rule, `swarm.*` is what a hive needs to be a *client* of the swarm, and
# none of this is. A peer needs this host's `wireguardEndpoint` (the roster
# entry in ./swarm.nix); what interface this host brings up, on which
# address, with which key, is nobody else's business. `listenPort` included
# — it is what this host binds, while the port a peer dials is the one
# inside `wireguardEndpoint`.
{
lib,
config,
@ -19,7 +27,7 @@
# `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 = {
options.services.hyperhive.deploy.wireguard = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
@ -39,7 +47,7 @@
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`.
`deploy.wireguard.enable = true`.
'';
};
@ -83,21 +91,21 @@
# at all. Nothing below is c0re-specific; the peer data c0re consumes
# (HIVE_PEER_CA_PATHS) is rendered in ./hive-c0re and stays gated
# there.
config = lib.mkIf config.services.hyperhive.swarm.wireguard.enable {
config = lib.mkIf config.services.hyperhive.deploy.wireguard.enable {
assertions = [
{
assertion = config.services.hyperhive.swarm.wireguard.privateKeyFile != null;
assertion = config.services.hyperhive.deploy.wireguard.privateKeyFile != null;
message = ''
services.hyperhive.swarm.wireguard.enable requires
services.hyperhive.swarm.wireguard.privateKeyFile to be set.
services.hyperhive.deploy.wireguard.enable requires
services.hyperhive.deploy.wireguard.privateKeyFile to be set.
Generate a key: wg genkey > /etc/wireguard/hive.key
'';
}
{
assertion = config.services.hyperhive.swarm.wireguard.address != "";
assertion = config.services.hyperhive.deploy.wireguard.address != "";
message = ''
services.hyperhive.swarm.wireguard.enable requires
services.hyperhive.swarm.wireguard.address to be set
services.hyperhive.deploy.wireguard.enable requires
services.hyperhive.deploy.wireguard.address to be set
(e.g. "10.100.0.1/24").
'';
}
@ -107,7 +115,7 @@
# connects to each peer that has `wireguardPublicKey` set.
networking.wireguard.interfaces =
let
wgCfg = config.services.hyperhive.swarm.wireguard;
wgCfg = config.services.hyperhive.deploy.wireguard;
# `peerHives` is `swarm.hives` minus this hive (../swarm.nix) —
# a mesh that included our own entry would configure a tunnel to
# ourselves.
@ -139,7 +147,7 @@
# Open the WireGuard UDP port on the host firewall (host-level
# networking — not inside containers).
networking.firewall.allowedUDPPorts = [
config.services.hyperhive.swarm.wireguard.listenPort
config.services.hyperhive.deploy.wireguard.listenPort
];
};
}

View file

@ -144,7 +144,7 @@ in
example = "base64pubkey=";
description = ''
WireGuard public key for this hive's host. Required when
`services.hyperhive.swarm.wireguard.enable = true` and
`services.hyperhive.deploy.wireguard.enable = true` and
you want this hive reachable over the mesh. Null = TLS-
only peering (public internet, no mesh tunnel).
'';

View file

@ -62,6 +62,16 @@ let
bare = hive { };
withCi = hive { deploy.forgejo.ci.enable = true; };
# A host configured against the pre-rename option path. `mkRenamedOptionModule`
# is the only thing carrying it, and nothing else in this suite would notice
# if it were dropped: the new path evaluates fine on its own, so a missing
# shim reads as a clean tree and breaks every existing operator config.
wireguardOldPath = hive {
swarm.wireguard.enable = true;
swarm.wireguard.address = "10.100.0.1/24";
swarm.wireguard.privateKeyFile = "/etc/wireguard/hive.key";
};
baoPkcs11 = hive {
deploy.bao.enable = true;
deploy.bao.seal = "pkcs11";
@ -183,6 +193,18 @@ let
name = "the all-local mode turns the swarm controller on";
ok = allLocal.services.hyperhive.deploy.swarm-controller.enable;
}
{
# The mesh moved namespace wholesale, so an existing config sets paths
# that no longer exist. Reading the *rendered interface* rather than the
# option: a rename that resolved but stopped reaching the module would
# satisfy an option-level check and still bring up no tunnel.
name = "a config written against the pre-rename wireguard path still configures the interface";
ok =
let
wg = wireguardOldPath.networking.wireguard.interfaces.wg-hive;
in
wg.ips == [ "10.100.0.1/24" ] && wg.privateKeyFile == "/etc/wireguard/hive.key";
}
{
# The gateway's per-name issuer choice. If this ever collapses to a
# constant, every swarm-service vhost serves a certificate its CA

View file

@ -93,7 +93,7 @@ let
# Completions come from the binary's own `completions <shell>` verb
# (the single source of truth, so they never drift from the actual
# verbs). The wireguard-tools wrap makes `hivectl wg` subcommands
# work before `swarm.wireguard.enable` is set (wg init is the very
# work before `deploy.wireguard.enable` is set (wg init is the very
# first setup step). Completion generation runs before wrapProgram
# since wrapProgram renames the real binary.
hivectlPkg =