feat(#2862): push a snapshot to a peer hive's store over the mesh

Adds the caller the fd-passing machinery existed for: hivectl agent
<name> subvol snapshot push --peer <hive> resolves the peer, connects
to its snapshot store, writes the agent header, and hands the connected
socket to hive-priv, which runs btrfs send straight into it.

The split keeps the root helper ignorant. Everything that involves
knowing where a peer is, what the wire protocol looks like, and which
hive to trust happens in the unprivileged daemon; hive-priv only ever
receives an already-open descriptor. Once btrfs send starts, neither
process is in the data path, so a multi-gigabyte transfer costs no
per-byte work and survives a hive-c0re restart.

call_with_fd takes the descriptor by value and closes it as soon as the
kernel has it. A socket stays open until every copy closes, so holding
one back would leave the receiver waiting for an EOF that never comes:
btrfs receive blocks and this side reports success for a transfer the
peer never committed. Ownership makes that unrepresentable.

The peer's store port is a new swarm.peers.<domain>.snapshotStorePort
option rather than a constant matching the module default. A pushing
hive cannot read the receiver's configuration, so assuming 51821 would
push at a port nobody promised to listen on; absent, the push fails
naming the option. swarm_peers parses the mesh address the host module
has always rendered into HYPERHIVE_PEERS but nothing read.
This commit is contained in:
atlas 2026-07-31 21:40:34 +02:00 committed by mara
commit 282bbc3709
10 changed files with 546 additions and 9 deletions

View file

@ -151,10 +151,13 @@ in
}
// lib.optionalAttrs (config.services.hyperhive.swarm.peers != { }) {
# Peer hives serialised as a JSON array of {domain, cert_fingerprint,
# wireguard_address?} objects. Consumed by hive-agent::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.
# wireguard_address?, snapshot_store_port?} objects. Consumed by
# hive-agent::identity::peers(), the dashboard's peer_hives
# StateSnapshot field (P33RS tab), and snapshot pushes
# (hive-c0re::swarm_peers). Domain is the attrset key;
# cert_fingerprint is null for CA-trusted peers; wireguard_address is
# omitted when not part of the mesh; snapshot_store_port is omitted
# when the peer runs no snapshot store.
HYPERHIVE_PEERS = builtins.toJSON (
lib.mapAttrsToList (
domain: p:
@ -165,6 +168,9 @@ in
// lib.optionalAttrs (p.wireguardAddress != null) {
wireguard_address = p.wireguardAddress;
}
// lib.optionalAttrs (p.snapshotStorePort != null) {
snapshot_store_port = p.snapshotStorePort;
}
) config.services.hyperhive.swarm.peers
);
}

View file

@ -105,6 +105,25 @@
are silently excluded from `wg-hive`).
'';
};
snapshotStorePort = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = null;
example = 51821;
description = ''
TCP port this peer's snapshot store listens on, when it
runs one (`services.hyperhive.snapshotStore`). Injected
into `HYPERHIVE_PEERS` so a pushing hive can reach the
receiver at `wireguardAddress:snapshotStorePort`.
Null means this peer hosts no snapshot store, and pushing
to it fails with that message rather than guessing a port.
The port lives here on the peer, alongside the mesh
address it pairs with because it describes *that host's*
deployment, and a pushing hive cannot read the receiver's
own configuration.
'';
};
};
}
);