gunnar-common 2.0.0

Shared command line, logging, and build identity for the gunnar binaries
Documentation
//! **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;
/// 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;
pub mod logging;
pub mod logqueue;
/// Every INFO+ line as a value, for the appliance console dashboard.
pub mod logtap;
#[cfg(feature = "stderr-capture")]
pub mod stderr_capture;

/// `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 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}"
            );
        }
    }
}