One attrset describing every hive in the swarm including this one,
identical on every host, with hiveName selecting which entry is us.
"My peers" is derived (swarm.peerHives) rather than declared.
Every field in the old per-host peer list was intrinsic to the hive it
described, never to the pair -- so the list was a directory each host
kept its own copy of. Beyond the deduplication it removes a bug class:
two hosts could hold different endpoints for the same third hive with
nothing to detect the disagreement.
Drops the per-hive caCert. Trust inside a swarm derives from the swarm
root, which every hive chains to. What that genuinely removes is
trusting a hive whose root this swarm does not own -- a cross-swarm
problem that wants a mechanism of its own, not a field that happened to
work.
The matrix container's certificateFiles block goes with it and could
NOT be migrated: that list is read at build time and the swarm root is
a runtime file (its key must never enter the store), so there is no
build-time name to put there. caCert being a nix path was precisely
what made it the build-time distribution channel. Agents are unaffected
-- hive-tls folds the root into the hive trust bundle and the meta
renderer embeds that one file. Tracked separately.
Migration is an assertion plus warnings, not a rename: hives is peers
union {self}, and the set gains a member no existing config has written
down. A rename migrates a name and a default can re-root a meaning;
neither can conjure a new member. The warning explains, the self-entry
assertion stops the build.
224 lines
9.1 KiB
Nix
224 lines
9.1 KiB
Nix
# The swarm's directory: one entry per hive, **including this one**,
|
|
# identical on every host in the swarm. `services.hyperhive.hiveName`
|
|
# says which entry is us, and `peerHives` below derives the rest.
|
|
#
|
|
# Why a directory rather than a per-host peer list: every field here is
|
|
# intrinsic to the hive it describes — none of them says anything about
|
|
# the *pair*. A list where every field is intrinsic is a directory each
|
|
# host was keeping its own copy of, which is O(n²) duplication that
|
|
# deduplicates without loss. It is also a correctness gain: two hosts
|
|
# could hold different endpoints for the same third hive and nothing
|
|
# detected it. One entry per hive makes that unrepresentable.
|
|
#
|
|
# Consumed by hive-c0re's environment (HYPERHIVE_PEERS — see
|
|
# ./hive-c0re), identity.rs + the dashboard's P33RS tab, and the mesh in
|
|
# ./swarm-wireguard.nix. The mesh lives there rather than here because
|
|
# bringing up an interface is host networking rather than swarm
|
|
# bookkeeping, and a host that runs no hive still needs it.
|
|
{
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.hyperhive;
|
|
swarmCfg = cfg.swarm;
|
|
in
|
|
{
|
|
options.services.hyperhive.swarm.hives = lib.mkOption {
|
|
type = lib.types.attrsOf (
|
|
lib.types.submodule {
|
|
options = {
|
|
domain = lib.mkOption {
|
|
type = lib.types.str;
|
|
example = "lab.example.com";
|
|
description = ''
|
|
Public DNS domain this hive occupies — used for dashboard
|
|
links, peer HTTPS checks and Matrix federation discovery.
|
|
|
|
Deliberately has no default. It is conventionally
|
|
`<name>.<swarm.domain>`, but defaulting to that would let
|
|
a typo'd swarm domain produce a name that resolves
|
|
somewhere real; an eval failure naming the hive is the
|
|
better outcome.
|
|
'';
|
|
};
|
|
|
|
certFingerprint = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "sha256:b1946ac92492d2347c6235b4d2611184a3f5b6cae6c19d6e3c2f0a8e7d4c9f12";
|
|
description = ''
|
|
Expected TLS certificate fingerprint for this hive's HTTPS
|
|
endpoint. Null = trust the CA bundle — which for a hive
|
|
inside the swarm CA hierarchy is the normal case, since
|
|
every hive under the swarm root already chains to it.
|
|
Set it to pin a leaf that no CA in the bundle vouches for.
|
|
|
|
Format: the literal `sha256:` followed by exactly 64
|
|
hex digits (case-insensitive, no colon separators) — the
|
|
SHA-256 digest of the 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/README.md for the full recipe.
|
|
|
|
Scopes only to hive-c0re's own peer HTTPS checks — it does
|
|
NOT help Matrix federation, which validates against the
|
|
container's trust bundle. There is no per-hive CA field to
|
|
cover that case any more: the swarm root is the trust path
|
|
(see ./swarm-ca.nix).
|
|
'';
|
|
};
|
|
|
|
wireguardPublicKey = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "base64pubkey=";
|
|
description = ''
|
|
WireGuard public key for this hive's host. Required when
|
|
`services.hyperhive.swarm.wireguard.enable = true` and
|
|
you want this hive 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 hive in `host:port` form.
|
|
Null = this hive has no reachable endpoint, so the tunnel
|
|
is initiated from the other side.
|
|
|
|
Reads like a fact about the relationship and is not: it
|
|
says whether *this* hive can be dialled, which every other
|
|
hive in the swarm needs the same answer to.
|
|
'';
|
|
};
|
|
|
|
wireguardAddress = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "10.100.0.2/32";
|
|
description = ''
|
|
IP address (with prefix) of this hive's host on the
|
|
WireGuard mesh. Used as the `allowedIPs` for its
|
|
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
|
|
a hive in the mesh (entries missing this field are
|
|
silently excluded from `wg-hive`).
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
);
|
|
default = { };
|
|
example = {
|
|
pr1ma = {
|
|
domain = "pr1ma.example.com";
|
|
wireguardAddress = "10.100.0.1/32";
|
|
wireguardEndpoint = "203.0.113.1:51820";
|
|
};
|
|
edge = {
|
|
domain = "edge.example.com";
|
|
wireguardAddress = "10.100.0.2/32";
|
|
};
|
|
};
|
|
description = ''
|
|
Every hive in this swarm, keyed by `hiveName` — **including this
|
|
host's own hive**. The same attrset is meant to be identical on
|
|
every host in the swarm, so it can be written once and shared;
|
|
`services.hyperhive.hiveName` is what makes a given host read it
|
|
as "me and four others" rather than "five peers".
|
|
|
|
Empty (the default) means this host is not part of a swarm. Once
|
|
non-empty it must contain an entry for `hiveName`, which is
|
|
asserted — a hive that lists everyone but itself would otherwise
|
|
derive its own peer set as *everything* and peer with itself.
|
|
'';
|
|
};
|
|
|
|
options.services.hyperhive.swarm.peerHives = lib.mkOption {
|
|
type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified);
|
|
readOnly = true;
|
|
internal = true;
|
|
description = ''
|
|
Read-only: `hives` minus this host's own entry. Derived once here
|
|
rather than in each consumer, because "everything that isn't me"
|
|
is a filter four different modules were re-implementing and only
|
|
one of them has to be wrong for a hive to peer with itself.
|
|
'';
|
|
};
|
|
|
|
config = {
|
|
services.hyperhive.swarm.peerHives = lib.filterAttrs (name: _: name != cfg.hiveName) swarmCfg.hives;
|
|
|
|
assertions = [
|
|
{
|
|
# Guarded on `hiveName != null` so the required-hiveName
|
|
# assertion in ./hyperhive.nix is what fires for that case —
|
|
# two assertions naming the same missing value is noise.
|
|
assertion = swarmCfg.hives == { } || cfg.hiveName == null || swarmCfg.hives ? ${cfg.hiveName};
|
|
message = ''
|
|
services.hyperhive.swarm.hives has no entry for this hive
|
|
(services.hyperhive.hiveName = "${toString cfg.hiveName}").
|
|
|
|
`hives` describes every hive in the swarm including this one,
|
|
so that every host can share one identical attrset. Add:
|
|
|
|
services.hyperhive.swarm.hives."${toString cfg.hiveName}" = {
|
|
domain = "${toString cfg.domain}";
|
|
};
|
|
|
|
Declared hives: ${lib.concatStringsSep ", " (lib.attrNames swarmCfg.hives)}
|
|
'';
|
|
}
|
|
];
|
|
};
|
|
|
|
# `enableRequiredServices` is declared in ./swarm-required-services.nix
|
|
# together with the per-service `enable`s it asserts — it is a
|
|
# deployment-shape switch rather than swarm bookkeeping, so it lives
|
|
# with its consequences instead of here.
|
|
|
|
options.services.hyperhive.swarm.snapshotStore = {
|
|
address = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "10.100.0.1";
|
|
description = ''
|
|
Mesh address of the swarm's snapshot store — the single
|
|
`btrfs receive` endpoint every hive in this swarm pushes agent
|
|
snapshots to. Bare IP, no prefix.
|
|
|
|
There is exactly **one** store per swarm, not one per peer: the
|
|
receiver keys destinations by *agent*, so an agent that migrates
|
|
between hives keeps a single unbroken incremental chain. Per-hive
|
|
stores would split that chain in two, which is the case the store
|
|
exists to serve.
|
|
|
|
Null means this swarm has no store configured, and pushing fails
|
|
saying so rather than guessing an address. Set it on every hive
|
|
that pushes; the receiving host separately sets
|
|
`services.hyperhive.snapshotStore.enable`.
|
|
'';
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 51821;
|
|
description = ''
|
|
TCP port the swarm's snapshot store listens on. Must match the
|
|
receiving host's `services.hyperhive.snapshotStore.port`.
|
|
|
|
Defaulted (unlike `address`) because it is a shared convention
|
|
both sides read from the same option docs — whereas an address
|
|
is deployment-specific and cannot be guessed.
|
|
'';
|
|
};
|
|
};
|
|
|
|
}
|