crates/gunnar-common/src/lib.rs
//! **The parts every gunnar binary shares, regardless of which half it is.**
//!
//! The command line ([`cli`]), the logging ladder ([`logging`]), the build
//! identity ([`BUILD_REF`], [`ssh_server_id`]) and the one address the control
//! plane defaults to ([`DEFAULT_CONTROL_ADDR`]) are needed by a pure-server
//! build, a pure-client build and the all-in-one alike. They lived in
//! `gunnar-cli` while it was the only binary crate; they moved here when the
//! server implementation became its own crate, so that `gunnar-cli` and
//! `gunnar-server` can both depend on this and neither has to depend on the
//! other.
//!
//! Nothing here is an *implementation* of a verb. The `Command` enum names the
//! verbs and parses their arguments; dispatching them into `gunnar-server`'s
//! server code or `gunnar-cli`'s client code stays in those crates.
#![warn(clippy::all)]
#![doc(
html_logo_url = "https://gunnar.rs/download/gunnar.webp",
html_favicon_url = "https://gunnar.rs/download/favicon.svg"
)]
/// Where a listener binds — an address, or an interface resolved at boot.
pub mod bindspec;
pub mod cli;
/// A box's own background duty as named jera jobs (A32-14).
#[cfg(feature = "jobs")]
pub mod jobs;
pub mod logging;
pub mod logqueue;
/// Every INFO+ line as a value, for the appliance console dashboard.
pub mod logtap;
/// Where the metered appliance's verifying key comes from — a file, or the
/// key itself, spelled inline on a kernel command line.
pub mod monetize_key;
#[cfg(feature = "stderr-capture")]
pub mod stderr_capture;
/// ★ ACCT-2 (2026-10-29): **the Gunnar release this build is** — the `gunnar` crate's
/// version, which the release tag (`v1.7.0`) names. Not a library crate's own semver: since the R5C crate versions (2026-10-26) gunnar-grpc and
/// gunnar-common were 2.0.0 on a 1.5.1 release (3.0.0 on 1.6.0, 4.0.0 on 1.7.0), and `Admin.Health` answered that to the console's
/// header. A test in gunnar-common holds it to the gunnar crate's version at every bump.
pub const RELEASE: &str = "1.7.0";
/// `gunnar/<version>`, the string this binary identifies itself with.
pub const AGENT: &str = concat!("gunnar/", env!("CARGO_PKG_VERSION"));
/// The build this binary is, as `build.rs` stamped it: the builder's passed
/// ref (`GUNNAR_BUILD_REF`, then the Build Thing's `NORDISK_BUILD_REF`), else
/// `git describe --tags --always --dirty=-modified --abbrev=7`, else `unknown`.
pub const BUILD_REF: &str = env!("GUNNAR_BUILD_REF");
/// Where the control plane listens unless told otherwise.
///
/// **Loopback, and it should stay there.** `gunnar serve` binds
/// `127.0.0.1:50051` by default for defence in depth, so the default target of
/// `gunnar control …` is the server on this host. Reaching a remote one is an
/// explicit `--at`.
///
/// It lives here, and not in `gunnar-cli`'s `control` module, because `cli.rs`
/// names it as the default of every control verb's `--at` and `cli.rs` is here.
pub const DEFAULT_CONTROL_ADDR: &str = "127.0.0.1:50051";
/// **The SSH identification string, carrying the commit this server is.**
///
/// `SshConfig::default()` produces `SSH-2.0-gunnar_0.1.0`, which names a manifest
/// field. That is enough on a machine where you can run `gunnar --version`, and
/// it is *all there is* on the appliance: the OS-less ISO has no shell, no init
/// but gunnar itself, and no way in but this listener. MEASURED 2026-08-05, the
/// forge-built appliance ISO booting under QEMU/OVMF:
///
/// ```text
/// BOOTED AND LISTENING in 5.911152673s — banner "SSH-2.0-gunnar_0.1.0"
/// ```
///
/// Two ISOs built from different commits are indistinguishable over the wire
/// there, and the only other identity on offer is the file's mtime. So the build
/// reference goes on the banner, which is the first thing the appliance says and
/// the one thing it says before authenticating.
///
/// # Where it goes, and why not in the version field
///
/// RFC 4253 §4.2 is `SSH-protoversion-softwareversion SP comments`, and
/// *softwareversion* may contain no space and no minus — `git describe` output
/// has both (`v1.0-4-g008d74b`). So it goes in the optional **comments** field,
/// after the space, which is exactly where OpenSSH puts its distribution string
/// (`SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.5`). Nothing parses the comments, and
/// the kex hash covers the whole string either way, so this is a legal
/// identification string for every client and a readable one for a person.
///
/// An unstamped build (`unknown`, i.e. no `.git` at compile time) adds nothing
/// rather than advertising the word "unknown" to the network.
pub fn ssh_server_id() -> String {
let base = concat!("SSH-2.0-gunnar_", env!("CARGO_PKG_VERSION"));
if BUILD_REF == "unknown" || BUILD_REF.is_empty() {
return base.to_string();
}
format!("{base} {BUILD_REF}")
}
/// Install the tracing subscriber once, honouring `RUST_LOG`.
///
/// `info` by default rather than `warn`. The things an operator most needs to
/// see on a gunnar server are all logged at `info`: the bound addresses, the
/// gRPC authentication mode, the `gunnar::audit` trail of who called what, and
/// one line per connection and one per transfer with its
/// applied outputs. A default of `warn` would hide every one of them.
///
/// # The targets, and how to filter them
///
/// Every line gunnar emits is on a `gunnar::` target, listed once in
/// `gunnar_core::observe::TARGETS`:
///
/// ```text
/// gunnar::audit who called the control plane, and what was decided
/// gunnar::ssh connections: accepted, authenticated, refused
/// gunnar::git transfers: upload-pack, receive-pack, objects, bytes
/// gunnar::policy the push gate: which gate refused which reference
/// gunnar::store ref transactions, CAS contention, repack
/// gunnar::metrics the counter heartbeat
/// ```
///
/// ```text
/// RUST_LOG=warn,gunnar::audit=info,gunnar::git=info # a quiet production box
/// RUST_LOG=gunnar::ssh=debug,gunnar::policy=debug # one incident, in detail
/// ```
///
/// Each connection carries a request id on its span, so every line underneath,
/// including the ones `gunnar-wire` and `gunnar-policy` emit from inside a
/// transfer, is greppable by `conn=<id>` without any of them repeating the
/// principal or the repository.
pub fn init_tracing(verbose: bool) {
// ── the filter is RELOADABLE ────────────────────────────────────────────
//
// The body lives in [`logging::init`] so the level is changeable on a
// running server. This stays as the name everything already calls, so the
// change is one file and not thirty call sites (one implementation).
let _installed = crate::logging::init(verbose);
}
#[cfg(test)]
mod release {
/// ★ ACCT-2: [`super::RELEASE`] is the `gunnar` crate's version — the product crate the
/// release tag names — checked where the workspace is (a published crate has none beside
/// it, and says nothing).
#[test]
fn the_release_is_the_gunnar_crates_version() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../gunnar/Cargo.toml");
let Ok(m) = std::fs::read_to_string(&path) else {
return;
};
let Some(at) = m.find("[package]") else {
return;
};
let version = m[at..]
.lines()
.find_map(|l| l.trim().strip_prefix("version = \""))
.and_then(|v| v.split('"').next())
.expect("the gunnar crate names its version");
assert_eq!(
super::RELEASE,
version,
"bump gunnar_common::RELEASE with the gunnar crate's version"
);
}
}
#[cfg(test)]
mod identification_string {
use super::*;
/// **RFC 4253 §4.2, enforced.** `SSH-protoversion-softwareversion SP
/// comments`, where *softwareversion* may contain no space and no minus.
///
/// This is not decoration. The obvious way to put a build reference on the
/// banner is to append it to the version — `SSH-2.0-gunnar_0.1.0-v1.0-4-g…`
/// — and `git describe` output is nothing but minuses, so that spelling
/// produces an identification string whose *softwareversion* field is
/// malformed for every client that parses one. The comments field, after the
/// space, is where OpenSSH puts its own distribution string and where this
/// belongs. Move the reference back into the version and this fires.
#[test]
fn the_banner_is_a_legal_identification_string_and_still_names_the_build() {
let id = ssh_server_id();
assert!(id.starts_with("SSH-2.0-"), "{id}");
assert!(
!id.contains('\r') && !id.contains('\n'),
"russh appends the CRLF; one in the string would end the line early: {id:?}"
);
assert!(
id.len() < 253,
"RFC 4253 caps the line at 255 with CRLF: {id}"
);
let body = &id["SSH-2.0-".len()..];
let software = body.split(' ').next().expect("a software version");
assert!(
software.starts_with("gunnar_"),
"the software version identifies gunnar: {id}"
);
assert!(
!software.contains('-'),
"a minus in the software version makes the identification string malformed — the \
build reference belongs in the COMMENTS field after the space: {id}"
);
// And it must actually carry the build, when there is one to carry. A
// build with no `.git` stamps `unknown`, and advertising the word
// "unknown" to the network is worse than saying nothing.
if BUILD_REF == "unknown" {
assert_eq!(id, format!("SSH-2.0-gunnar_{}", env!("CARGO_PKG_VERSION")));
} else {
let comments = body
.split_once(' ')
.map(|(_, c)| c)
.unwrap_or_else(|| panic!("a stamped build puts its reference in comments: {id}"));
assert_eq!(comments, BUILD_REF, "{id}");
assert!(
!comments.is_empty() && !comments.contains(' '),
"the comments field is one token: {id}"
);
}
}
}