server/cli/src/sync_cli.rs
//! `holger-server sync` — the sync protocol by hand (Holger v2.0 H4,
//! `server_lib::sync`): the USB half of every verb the server's doors and its
//! `sync.tick` job run.
//!
//! | verb | side | what |
//! |---|---|---|
//! | `key` | both | this Holger's sync name and public key (minted on first use) |
//! | `invalidate` | upstream | issue an invalidation; the package is held here at once |
//! | `manifest --out <stick>` | upstream | the signed manifest for a stick |
//! | `apply --manifest <file> --ack-out <stick>` | downstream | verify, hold, sign the ack onto the stick |
//! | `ack --ack <file>` | upstream | take a downstream's ack from the stick |
//! | `board` | upstream | the board's sync data (JSON), the alarms due raised first |
//! | `pull` | downstream (`online`) | one pull from every upstream with a `url`, and its ack |
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use server_lib::sync::SyncHub;
#[derive(Args)]
pub struct SyncArgs {
#[command(subcommand)]
pub verb: SyncVerb,
}
#[derive(Subcommand)]
pub enum SyncVerb {
/// Print this Holger's sync name and public key (minted on first use).
Key {
#[arg(short, long)]
config: PathBuf,
},
/// Print this downstream's WireGuard public key (minted on first use): what its upstream
/// declares as `via: Wireguard(key: ..)` (lane HOL-WGDS).
#[cfg(feature = "downstream-wg")]
WgKey {
#[arg(short, long)]
config: PathBuf,
},
/// Issue an invalidation (upstream): every downstream holds the package at its next sync.
Invalidate {
#[arg(short, long)]
config: PathBuf,
/// The coordinate (`name@version`, a file path) as this Holger's intake record names it.
#[arg(long)]
coordinate: String,
/// The bytes it names; absent: the bytes this Holger holds at the coordinate.
#[arg(long)]
sha256: Option<String>,
/// cve-after, malware, pulled or policy.
#[arg(long)]
reason: String,
/// One sentence: the CVE, the scanner hit, the publisher's notice.
#[arg(long, default_value = "")]
detail: String,
},
/// Write the signed manifest for a stick (upstream).
Manifest {
#[arg(short, long)]
config: PathBuf,
/// The stick's directory.
#[arg(long)]
out: PathBuf,
/// The packages added after this generation (0: all).
#[arg(long, default_value_t = 0)]
since: u64,
},
/// Apply a signed manifest from the stick (downstream) and write the signed ack.
Apply {
#[arg(short, long)]
config: PathBuf,
#[arg(long)]
manifest: PathBuf,
/// Where the ack goes (the stick); it is always kept in the outbox too.
#[arg(long)]
ack_out: Option<PathBuf>,
},
/// Take a downstream's signed ack from the stick (upstream).
Ack {
#[arg(short, long)]
config: PathBuf,
#[arg(long)]
ack: PathBuf,
},
/// Print the board's sync data (upstream), JSON.
Board {
#[arg(short, long)]
config: PathBuf,
},
/// Pull once from every upstream with a `url`, and post the acks (downstream).
#[cfg(all(feature = "online", feature = "fetch"))]
Pull {
#[arg(short, long)]
config: PathBuf,
},
}
fn os_user() -> String {
std::env::var("USER")
.or_else(|_| std::env::var("LOGNAME"))
.unwrap_or_else(|_| "local".into())
}
/// The hub over `config`'s intake chain (the same one the server builds).
/// The chain is returned too: the hub holds it weakly, so the caller keeps it alive.
fn hub(config: &PathBuf) -> Result<(Arc<server_lib::promote::intake::IntakeChain>, Arc<SyncHub>)> {
let mut holger = server_lib::read_ron_config(config)
.with_context(|| format!("load config {}", config.display()))?;
if holger.resolved_network() == server_lib::egress::NetworkMode::Offline {
server_lib::egress::set_offline();
}
holger.instantiate_backends()?;
let audit = holger.audit_log()?;
let chain = holger.intake_chain(audit)?.ok_or_else(|| {
anyhow::anyhow!(
"config {} declares no intake chain (`promotion.intake`)",
config.display()
)
})?;
let hub = chain.sync().ok_or_else(|| {
anyhow::anyhow!(
"{}",
server_lib::errcode::SYNC_NOT_CONFIGURED.tag(format_args!(
"config {} has no `sync` block",
config.display()
))
)
})?;
Ok((chain, hub))
}
fn tagged(e: server_lib::sync::SyncFailure) -> anyhow::Error {
anyhow::anyhow!("{}", e.tagged())
}
/// `holger-server sync`.
pub fn run(a: SyncArgs) -> Result<()> {
match a.verb {
SyncVerb::Key { config } => {
let (_chain, h) = hub(&config)?;
println!("{} {}", h.name(), h.public_key());
}
#[cfg(feature = "downstream-wg")]
SyncVerb::WgKey { config } => {
let holger = server_lib::read_ron_config(&config)?;
let id = server_lib::sync::tunnel::identity(&holger.data_root()).map_err(tagged)?;
println!("{}", id.public_b64);
}
SyncVerb::Invalidate {
config,
coordinate,
sha256,
reason,
detail,
} => {
let (_chain, h) = hub(&config)?;
let reason = server_lib::sync::protocol::Reason::parse(&reason).ok_or_else(|| {
anyhow::anyhow!(
"{}",
server_lib::errcode::SYNC_MALFORMED
.tag("--reason is one of cve-after, malware, pulled, policy")
)
})?;
let i = h
.invalidate(
&os_user(),
"local",
&coordinate,
sha256.as_deref(),
reason,
&detail,
)
.map_err(tagged)?;
println!(
"{} {} generation {}: {} ({}) sha256 {}",
if i.new {
"INVALIDATED"
} else {
"ALREADY INVALIDATED"
},
i.invalidation.id,
i.invalidation.generation,
i.invalidation.coordinate,
i.invalidation.reason,
i.invalidation.sha256.as_deref().unwrap_or("any")
);
for c in &i.held_here {
println!(" HELD here {c}");
}
}
SyncVerb::Manifest { config, out, since } => {
let (_chain, h) = hub(&config)?;
let m = h.usb_manifest(since).map_err(tagged)?;
let path = h.write_manifest(&m, &out).map_err(tagged)?;
println!("MANIFEST {}", path.display());
}
SyncVerb::Apply {
config,
manifest,
ack_out,
} => {
let (_chain, h) = hub(&config)?;
let bytes =
std::fs::read(&manifest).with_context(|| format!("read {}", manifest.display()))?;
let a = h
.apply(&bytes, "usb", &os_user(), "local")
.map_err(tagged)?;
println!(
"APPLIED ({}) '{}' generation {}: {} package(s) added",
a.admission, a.upstream, a.generation, a.added
);
for c in &a.quarantined {
println!(" HELD {c}");
}
for c in &a.already_held {
println!(" ALREADY HELD {c}");
}
#[cfg(feature = "fetch")]
{
println!(" ack: {}", a.ack_path.display());
if let Some(dir) = ack_out {
let p = h.write_ack(&a.ack, &dir).map_err(tagged)?;
println!(" ACK {}", p.display());
}
}
// A one-way Holger signs no receipt: nothing goes back over the diode.
#[cfg(not(feature = "fetch"))]
{
let _ = ack_out;
println!(" no receipt: this Holger is one-way (a build without `fetch`), so it signs no ack");
}
}
SyncVerb::Ack { config, ack } => {
let (_chain, h) = hub(&config)?;
let bytes = std::fs::read(&ack).with_context(|| format!("read {}", ack.display()))?;
let out = h.import_ack(&bytes, "usb", "local").map_err(tagged)?;
println!(
"ACK '{}' generation {}: {} invalidation(s) reached now",
out.downstream,
out.generation,
out.reached_now.len()
);
for id in &out.cleared {
println!(" ALARM CLEARED {id}");
}
}
SyncVerb::Board { config } => {
let (_chain, h) = hub(&config)?;
let b = h
.board(server_lib::sync::protocol::now_secs())
.map_err(tagged)?;
println!("{}", serde_json::to_string_pretty(&b)?);
}
#[cfg(all(feature = "online", feature = "fetch"))]
SyncVerb::Pull { config } => {
let (_chain, h) = hub(&config)?;
let link = server_lib::sync::HttpLink::new();
for u in h.config().upstreams.iter().filter(|u| u.url.is_some()) {
let a = h.pull_once(&link, u).map_err(tagged)?;
println!(
"PULLED ({}) '{}' generation {}: {} held, ack {}",
a.admission,
a.upstream,
a.generation,
a.quarantined.len(),
if a.ack_sent {
"sent"
} else {
"kept in the outbox"
}
);
}
}
}
Ok(())
}