#![doc(html_logo_url = "https://gunnar.rs/download/gunnar.webp", html_favicon_url = "https://gunnar.rs/download/favicon.svg")]
#[cfg(feature = "mimalloc")]
pub type ServerAllocator = mimalloc::MiMalloc;
#[cfg(feature = "mimalloc")]
pub const SERVER_ALLOCATOR: ServerAllocator = mimalloc::MiMalloc;
#[cfg(not(feature = "mimalloc"))]
pub type ServerAllocator = std::alloc::System;
#[cfg(not(feature = "mimalloc"))]
pub const SERVER_ALLOCATOR: ServerAllocator = std::alloc::System;
pub mod admin;
#[cfg(feature = "wg-client")]
pub mod appliance;
pub mod attest;
pub mod txlog_lfs;
pub mod twin;
pub mod twin_tls;
pub mod tunnel_addr;
pub mod budget;
pub mod capacity;
pub mod control_plane;
pub mod grow;
pub mod dashboard;
pub mod account_purge;
pub mod account_audit_sweep;
pub mod job_queue;
pub mod gc_queue;
pub mod purge_queue;
pub mod mirror_queue;
pub mod mirror_fetch;
#[cfg(feature = "mirror-http")]
pub mod mirror_http;
pub mod grpc_gate;
#[cfg(feature = "engine-znippy")]
pub use storage::encrypted_quarantine;
pub mod diff;
pub mod blame;
pub mod search;
#[cfg(test)]
#[cfg(test)]
mod testgit;
pub mod first_commit;
pub mod durable;
pub mod disktest;
pub mod glance;
pub mod topology;
#[cfg(feature = "metered")]
pub mod entitlement;
#[cfg(feature = "metered")]
pub mod actor_ticket;
pub mod limits;
pub mod oci;
pub mod boot;
pub mod narrate;
pub mod stop_record;
pub mod clock;
pub mod readiness;
pub mod registry;
pub mod runner;
pub mod sandbox;
pub mod scrape;
pub mod serve;
pub mod layout_config;
pub mod slow;
#[cfg(feature = "sso-egress")]
pub mod sso_egress;
pub mod storage;
pub mod push_space;
pub mod stores;
pub mod usage_ledger;
pub mod tls_preflight;
pub mod transport;
pub mod verify;
#[cfg(feature = "wg-client")]
pub mod wg;
#[cfg(feature = "wg-client")]
pub mod pairing;
#[cfg(feature = "wg-client")]
pub mod fed_at_rest;
#[cfg(feature = "wg-client")]
pub mod fed_principals;
#[cfg(feature = "wg-client")]
pub mod installer_listener;
pub mod live_update;
#[cfg(feature = "dev-narrate")]
pub mod dev_narrate;
pub mod feed;
#[cfg(feature = "http")]
pub use gunnar_http::Cleartext;
pub fn run(
data_root: std::path::PathBuf,
command: gunnar_common::cli::Command,
) -> anyhow::Result<()> {
let outcome = dispatch(data_root, command);
storage::shutdown();
outcome
}
fn dispatch(
data_root: std::path::PathBuf,
command: gunnar_common::cli::Command,
) -> anyhow::Result<()> {
use gunnar_common::cli::Command;
match command {
Command::Serve(args) => {
use std::sync::Arc;
let stores = Arc::new(stores::ServerStores::open(&data_root)?);
let wire = transport::WireConfig::from_serve_args(&args)?;
let fed_at_start = crate::feed::load_at_start(&data_root);
let fed_monetize = fed_at_start.as_ref().and_then(|f| f.monetize_pubkey.clone());
#[cfg(not(feature = "metered"))]
if let Some(key) = &fed_monetize {
anyhow::bail!(
"REFUSED [pairing-feed-monetize-open-build] the installer's door fed monetize key {key}, \
but this gunnar-server was built without the `metered` feature and contains no \
entitlement code at all; it would serve every namespace Free. Build the \
`gunnar-server-metered` variant, or stop the front feeding `monetize_pubkey`."
);
}
#[cfg(not(feature = "metered"))]
crate::feed::record_monetize_in_force(None);
#[cfg(not(feature = "metered"))]
if let Some(key) = &args.monetize_pubkey {
anyhow::bail!(
"--monetize-pubkey {key} was given, but this gunnar-server was built without \
the `metered` feature and contains no entitlement code at all; it would \
serve every namespace Free. Build the `gunnar-server-metered` variant."
);
}
#[cfg(feature = "metered")]
let metered = {
let awaits_feed = args.monetize_pubkey.as_ref().is_some_and(|k| k.is_fed());
let medium = match &args.monetize_pubkey {
Some(key) if key.is_fed() => None,
Some(key) => Some(entitlement::read_pubkey(key)?),
None => None,
};
let resolved = crate::feed::resolve_monetize(medium, fed_monetize.as_deref())
.map_err(|why| anyhow::anyhow!(why))?;
if medium.is_none() && resolved.is_some() {
tracing::info!("monetize key: from the installer's feed (<data>/pairing/joined.toml); the medium carries none");
}
crate::feed::record_monetize_in_force(resolved);
match resolved {
None if awaits_feed => {
tracing::warn!(
"monetize key: the medium says `{}` — the installer's door feeds it, and it has not yet \
(first boot, before the join). Enforcing NOTHING until it arrives; it then applies in \
place, with no restart",
gunnar_common::monetize_key::FED
);
let mut cfg = entitlement::MeteredConfig::awaiting_feed(args.billing_url.as_deref())?;
cfg.vital = args.monetize_vital;
Some(cfg)
}
Some(key) => {
let mut cfg = entitlement::MeteredConfig::load(
&gunnar_common::monetize_key::MonetizePubkey::Literal(key),
args.billing_url.as_deref(),
)?;
cfg.vital = args.monetize_vital;
Some(cfg)
}
None if args.billing_url.is_some() => anyhow::bail!(
"--billing-url (GUNNAR_BILLING_URL) was given, but no monetize key: neither \
--monetize-pubkey (GUNNAR_MONETIZE_PUBKEY) on the medium nor `monetize_pubkey` \
fed by the installer's door. A pay page with no key to verify facts with \
bills nobody."
),
None => None,
}
};
let disk_floor = gunnar_ssh::DiskFloor::parse(&args.disk_floor)
.map_err(|err| anyhow::anyhow!(err))?;
#[cfg(feature = "admit-share")]
let admit = admit_settings(args.admit_limit.as_deref(), args.admit_window_days)?;
let config = serve::ServeConfig {
ssh_bind: args.ssh_bind,
disk_floor,
grpc_bind: args.grpc_bind,
grpc_bind_device: None,
admin_key: args.admin_key.clone(),
restore_pubkeys: args.restore_pubkeys.clone(),
no_grpc: args.no_grpc,
wg_only: args.wg_only,
twin_role: args.role == gunnar_common::cli::ServeRole::Twin,
allow_anonymous: !args.no_anonymous,
anonymous: match args.anonymous {
gunnar_common::cli::AnonymousArg::Allow => gunnar_ssh::AnonymousPolicy::Allow,
gunnar_common::cli::AnonymousArg::Refuse => gunnar_ssh::AnonymousPolicy::Refuse {
browse_host: args.browse_host.clone(),
},
},
max_concurrent_transfers: args.max_concurrent_transfers,
memory_budget_bytes: args.git_memory_bytes,
admission_wait: args.admission_wait_ms.map(std::time::Duration::from_millis),
max_transfers_per_principal: args.max_transfers_per_principal,
max_anonymous_transfers: args.max_anonymous_transfers,
#[cfg(feature = "admit-share")]
admit,
per_principal_limit_is_hard: args.hard_per_principal_limit,
max_object_bytes: args.max_object_bytes,
metrics_bind: args.metrics_bind,
#[cfg(feature = "replication")]
standby_watch: args.watch_primary.map(|primary| {
crate::standby_watch::StandbyWatch {
primary,
probe_interval: std::time::Duration::from_millis(args.watch_probe_ms),
connect_timeout: std::time::Duration::from_millis(args.watch_connect_ms),
promote_after: args.watch_promote_after,
}
}),
#[cfg(feature = "replication")]
repair: Some(crate::repair::RepairConfig {
auto: args.repair_auto,
key: args.repair_key.clone(),
twin: args.repair_twin.clone(),
fleet: crate::repair::resolve_fleet(match (&args.fleet, &args.fleet_instance) {
(Some(url), Some(instance)) => Some(crate::repair::FleetConfig {
url: url.clone(),
ca: args.fleet_ca.clone(),
ca_pem: None,
at: None,
instance: instance.clone(),
role: args.fleet_role.clone().unwrap_or_default(),
monetize_pubkey: args.monetize_pubkey.as_ref().and_then(|k| k.literal()),
every: std::time::Duration::from_secs(args.fleet_every_s.max(1)),
}),
(Some(_), None) => anyhow::bail!("--fleet needs --fleet-instance (GUNNAR_FLEET_INSTANCE): the estate this box reports for"),
_ => None,
}, fed_at_start.as_ref(), std::time::Duration::from_secs(args.fleet_every_s.max(1)))
.map_err(|why| anyhow::anyhow!(why))?,
}),
#[cfg(feature = "http")]
http_bind: args.http_bind,
#[cfg(feature = "http")]
http_cleartext: if args.http_allow_cleartext_credentials {
Cleartext::Allowed
} else {
Cleartext::RequireProxyTls
},
#[cfg(feature = "http")]
http_tls: if args.http_tls_self {
Some(serve::HttpTls::SelfMinted)
} else if let (Some(cert), Some(key)) = (&args.http_tls_cert, &args.http_tls_key) {
Some(serve::HttpTls::Identity(gunnar_http::TlsIdentity {
cert_chain_pem: std::fs::read_to_string(cert)
.map_err(|e| anyhow::anyhow!("reading {}: {e}", cert.display()))?,
key_pem: std::fs::read_to_string(key)
.map_err(|e| anyhow::anyhow!("reading {}: {e}", key.display()))?,
}))
} else {
None
},
grpc_tls: if args.grpc_tls_self {
Some(serve::GrpcTls::SelfMinted)
} else {
None
},
web_relying_parties: args.web_relying_party.clone(),
web_require_user_verification: args.web_require_user_verification,
grpc_replication_bind: args.grpc_replication_bind,
runner: args.runner.then(|| runner::WatchConfig {
workspace: args
.runner_workspace
.clone()
.expect("--runner requires --runner-workspace"),
builder: args
.runner_builder
.clone()
.expect("--runner requires --runner-builder"),
allow_network: args.runner_allow_network,
refs: Vec::new(),
max_builds: None,
}),
wire,
#[cfg(feature = "metered")]
metered,
..serve::ServeConfig::default()
};
serve::runtime()?.block_on(serve::run(stores, config, args.check))
}
Command::Namespace(command) => {
let stores = stores::ServerStores::open(&data_root)?;
admin::namespace(&stores, command)
}
Command::Repo(command) => {
#[cfg(feature = "engine-znippy")]
if matches!(command, gunnar_common::cli::RepoCommand::ArchiveOut { .. }) {
return admin::repo_archive_out_without_stores(&data_root, command);
}
let stores = stores::ServerStores::open(&data_root)?;
admin::repo(&stores, command)
}
Command::Principal(command) => {
let stores = stores::ServerStores::open(&data_root)?;
admin::principal(&stores, command)
}
Command::Issuer(command) => {
let stores = stores::ServerStores::open(&data_root)?;
admin::issuer(&stores, command)
}
Command::Key(command) => {
let stores = stores::ServerStores::open(&data_root)?;
admin::key(&stores, command)
}
Command::Token(command) => {
let stores = stores::ServerStores::open(&data_root)?;
admin::token(&stores, command)
}
Command::Attest(command) => {
let stores = stores::ServerStores::open(&data_root)?;
admin::attest(&stores, command)
}
Command::Verify(args) => admin::verify(&data_root, args),
Command::Blame(args) => admin::blame(&data_root, args),
Command::Search(args) => admin::search(&data_root, args),
Command::Build(command) => {
let stores = stores::ServerStores::open(&data_root)?;
admin::build(&stores, command)
}
Command::Forge(command) => {
let stores = stores::ServerStores::open(&data_root)?;
admin::forge(&stores, command)
}
Command::Registry(command) => {
let stores = std::sync::Arc::new(stores::ServerStores::open(&data_root)?);
admin::registry(&stores, command)
}
Command::Grant(args) => {
let stores = stores::ServerStores::open(&data_root)?;
admin::grant(&stores, args)
}
Command::Revoke(args) => {
let stores = stores::ServerStores::open(&data_root)?;
admin::revoke(&stores, args)
}
Command::Gc(args) => {
let stores = stores::ServerStores::open(&data_root)?;
admin::gc(&stores, args)
}
Command::Reflog(args) => admin::reflog(&data_root, args),
#[cfg(feature = "wg-client")]
Command::Wg(command) => wg::keygen_verb(command),
#[cfg(not(feature = "wg-client"))]
Command::Wg(_) => anyhow::bail!(
"this binary was built WITHOUT the `wg-client` feature: it contains no \
WireGuard implementation, so `gunnar wg` has no keypair to mint. Rebuild \
with --features wg-client, or use a build that has it."
),
#[allow(unreachable_patterns)]
_ => anyhow::bail!("gunnar-server handles only server commands"),
}
}
#[cfg(feature = "replication")]
pub mod mirror;
#[cfg(feature = "replication")]
pub mod standby_watch;
#[cfg(feature = "replication")]
pub mod repair;
#[cfg(feature = "fleet-pull")]
pub mod postman;
#[cfg(feature = "replication")]
pub mod paid_growth;
#[cfg(feature = "replication")]
pub mod product_report;
pub mod replication_view;
#[cfg(feature = "webhooks")]
pub mod webhook;
#[cfg(feature = "replication")]
pub mod replication_wire;
#[inline]
pub fn functional_status(component: &str, check: &str, ok: bool, detail: &str) {
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(component, check, ok, detail);
#[cfg(not(feature = "testmatrix"))]
{
let _ = (component, check, ok, detail);
}
}
pub use attest::{AttestationLog, ATTESTATION_LOG};
pub use control_plane::CoreControlPlane;
pub use grpc_gate::{activity_ledger, control_audit_ledger, control_transfer_gate};
#[cfg(feature = "replication")]
pub use mirror::{
ReplicationConfig, ReplicationCounters, ReplicationScope, ReplicationTarget, Replicator,
TargetStatus, VisibilityScope,
};
pub use serve::{GrpcTls, ServeConfig, Server};
pub use stores::{ServerStores, CONTROL_DB, CONTROL_DIR};
pub use transport::{WireConfig, WireTransport};
#[cfg(feature = "admit-share")]
pub fn admit_settings(
limit: Option<&str>,
window_days: Option<u32>,
) -> anyhow::Result<gunnar_ssh::edda_admit::Settings> {
let mut settings = gunnar_ssh::edda_admit::Settings::default();
if let Some(limit) = limit {
settings.limit = gunnar_ssh::edda_admit::parse_bytes(limit)
.map_err(|err| anyhow::anyhow!("--admit-limit (GUNNAR_ADMIT_LIMIT): {err}"))?;
}
if let Some(days) = window_days {
anyhow::ensure!(days > 0, "--admit-window-days (GUNNAR_ADMIT_WINDOW_DAYS) must be at least 1");
settings.window_days = days;
}
Ok(settings)
}