reshape swarm peers: domain-as-key, certFingerprint field

This commit is contained in:
damocles 2026-05-31 23:29:30 +02:00 committed by mara
commit 52cfc3ea1c
3 changed files with 45 additions and 66 deletions

View file

@ -51,14 +51,14 @@ pub fn swarm_name() -> Option<String> {
/// One peer hive in the same swarm. Parsed from `HYPERHIVE_PEERS`. /// One peer hive in the same swarm. Parsed from `HYPERHIVE_PEERS`.
#[derive(Debug, Clone, serde::Deserialize)] #[derive(Debug, Clone, serde::Deserialize)]
pub struct PeerHive { pub struct PeerHive {
pub label: String,
pub domain: String, pub domain: String,
pub cert_fingerprint: Option<String>,
} }
/// Peer hives in the same swarm, parsed from `HYPERHIVE_PEERS` env var /// Peer hives in the same swarm, parsed from `HYPERHIVE_PEERS` env var
/// (JSON array of `{label,domain}` objects, emitted by the c0re NixOS /// (JSON array of `{domain, cert_fingerprint}` objects, emitted by the
/// module from `services.hyperhive.peers`). Returns empty vec on /// c0re NixOS module from `services.hyperhive.swarm.peers`). Returns
/// single-hive deploys (env var absent). /// empty vec on single-hive deploys (env var absent).
#[must_use] #[must_use]
pub fn peers() -> Vec<PeerHive> { pub fn peers() -> Vec<PeerHive> {
env::var("HYPERHIVE_PEERS") env::var("HYPERHIVE_PEERS")

View file

@ -267,19 +267,14 @@ struct StateSnapshot {
/// unset — chrome omits the swarm segment of the breadcrumb. /// unset — chrome omits the swarm segment of the breadcrumb.
swarm_name: Option<String>, swarm_name: Option<String>,
/// Peer hives in the same swarm. Parsed from `HYPERHIVE_PEERS` /// Peer hives in the same swarm. Parsed from `HYPERHIVE_PEERS`
/// (JSON array of `{label,domain}` objects, emitted by the c0re /// (JSON array of `{domain,cert_fingerprint}` objects, emitted by
/// NixOS module from `services.hyperhive.peers`). Empty on /// the c0re NixOS module from `services.hyperhive.swarm.peers`).
/// single-hive deploys. Feeds the P33RS dashboard tab. Each entry /// Empty on single-hive deploys. Feeds the P33RS dashboard tab.
/// exposes `name` (the operator-chosen label) and `url` (dashboard
/// link derived as `http://<domain>/`; HTTP-only until the gateway
/// has TLS and the scheme is threaded through).
peer_hives: Vec<PeerHiveView>, peer_hives: Vec<PeerHiveView>,
} }
/// One peer hive for the P33RS dashboard tab. Derived from /// One peer hive for the P33RS dashboard tab. Derived from
/// `HYPERHIVE_PEERS` env; `url` is the peer's dashboard root so the /// `HYPERHIVE_PEERS` env; `url` is the peer's HTTPS dashboard root.
/// tab can render a clickable card without knowing the remote port.
/// HTTP-only until per-peer TLS is wired through the gateway layer.
#[derive(Serialize)] #[derive(Serialize)]
struct PeerHiveView { struct PeerHiveView {
name: String, name: String,
@ -498,15 +493,16 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
} }
/// Parse `HYPERHIVE_PEERS` env var into dashboard-ready `PeerHiveView` /// Parse `HYPERHIVE_PEERS` env var into dashboard-ready `PeerHiveView`
/// entries. The env var is a JSON array of `{label, domain}` objects /// entries. The env var is a JSON array of `{domain, cert_fingerprint}`
/// emitted by the c0re NixOS module from `services.hyperhive.peers`. /// objects emitted by the c0re NixOS module from
/// Each entry becomes `{ name: label, url: "http://domain/" }` for the /// `services.hyperhive.swarm.peers`. Each entry becomes
/// P33RS tab. Returns empty vec when unset (single-hive deploy). /// `{ name: domain, url: "https://domain/" }` for the P33RS tab.
/// Returns empty vec when unset (single-hive deploy).
fn parse_peer_hives() -> Vec<PeerHiveView> { fn parse_peer_hives() -> Vec<PeerHiveView> {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct Raw { struct Raw {
label: String,
domain: String, domain: String,
cert_fingerprint: Option<String>,
} }
let Ok(json) = std::env::var("HYPERHIVE_PEERS") else { let Ok(json) = std::env::var("HYPERHIVE_PEERS") else {
return Vec::new(); return Vec::new();
@ -517,8 +513,8 @@ fn parse_peer_hives() -> Vec<PeerHiveView> {
}; };
raw.into_iter() raw.into_iter()
.map(|r| PeerHiveView { .map(|r| PeerHiveView {
name: r.label, name: r.domain.clone(),
url: format!("http://{}/", r.domain), url: format!("https://{}/", r.domain),
}) })
.collect() .collect()
} }

View file

@ -99,48 +99,31 @@ in
# containers via `meta.rs::FORWARDED_VARS`. Consumed by # containers via `meta.rs::FORWARDED_VARS`. Consumed by
# `identity.rs::peers()` + the dashboard's `peer_hives` state field # `identity.rs::peers()` + the dashboard's `peer_hives` state field
# (feeds the P33RS dashboard tab). # (feeds the P33RS dashboard tab).
options.services.hyperhive.peers = lib.mkOption { options.services.hyperhive.swarm.peers = lib.mkOption {
type = lib.types.attrsOf ( type = lib.types.attrsOf (lib.types.submodule {
lib.types.submodule { options = {
options = { certFingerprint = lib.mkOption {
domain = lib.mkOption { type = lib.types.nullOr lib.types.str;
type = lib.types.str; default = null;
example = "lab.example.com"; example = "sha256:abc123...";
description = '' description = ''
DNS domain of the peer hive. Used to construct the peer's Expected TLS certificate fingerprint for this peer's HTTPS
dashboard URL (`http://''${domain}/`) and for Matrix endpoint. Null = trust the system CA bundle (for Let's
federation auto-discovery (`matrix.''${domain}`). Encrypt peers). Set to pin a self-signed cert.
Must be reachable from this host. '';
'';
};
tlsCertFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Optional path to a PEM cert/bundle to trust for this peer's
TLS. Null = system CA bundle (for Let's Encrypt peers). Set
to the peer's self-signed cert for `selfSignedTls = true`
peers. Forward-compat slot; not yet used in v0.
'';
};
}; };
} };
); });
default = { }; default = { };
example = { example = {
lab = { "lab.example.com" = { certFingerprint = "sha256:abc123"; };
domain = "lab.example.com"; "edge.corp" = { };
};
edge = {
domain = "edge.corp";
};
}; };
description = '' description = ''
Peer hives in the same swarm. The attrset key is a short label Peer hives in the same swarm. The attrset key is the peer's DNS
used in dashboard links and log messages -- it does not need to domain used for dashboard links and Matrix federation discovery.
match the remote hive's `hiveName`. Null `tlsCertFile` uses the Null `certFingerprint` trusts the system CA bundle; set it to pin
system CA bundle; set it for self-signed TLS peers (forward-compat a self-signed TLS cert.
slot, not yet used in v0).
''; '';
}; };
@ -365,16 +348,16 @@ in
# falls back to `<hostname>:3000`. # falls back to `<hostname>:3000`.
HIVE_FORGE_PUBLIC_URL = "https://${config.services.hyperhive.forge.domain}"; HIVE_FORGE_PUBLIC_URL = "https://${config.services.hyperhive.forge.domain}";
} }
// lib.optionalAttrs (config.services.hyperhive.peers != { }) { // lib.optionalAttrs (config.services.hyperhive.swarm.peers != { }) {
# Peer hives serialised as a JSON array of {label, domain} objects. # Peer hives serialised as a JSON array of {domain, cert_fingerprint}
# Consumed by hive-ag3nt::identity::peers() + the dashboard's # objects. Consumed by hive-ag3nt::identity::peers() + the dashboard's
# peer_hives StateSnapshot field (P33RS tab). tlsCertFile is # peer_hives StateSnapshot field (P33RS tab). Domain is the attrset key;
# nix-side-only (host nginx/trust config); rust never needs the path. # cert_fingerprint is null for CA-trusted peers.
HYPERHIVE_PEERS = builtins.toJSON ( HYPERHIVE_PEERS = builtins.toJSON (
lib.mapAttrsToList (label: p: { lib.mapAttrsToList (domain: p: {
inherit label; inherit domain;
inherit (p) domain; cert_fingerprint = p.certFingerprint;
}) config.services.hyperhive.peers }) config.services.hyperhive.swarm.peers
); );
}; };
serviceConfig = { serviceConfig = {