Skip to main content

gunnar_common/
lib.rs

1//! **The parts every gunnar binary shares, regardless of which half it is.**
2//!
3//! The command line ([`cli`]), the logging ladder ([`logging`]), the build
4//! identity ([`BUILD_REF`], [`ssh_server_id`]) and the one address the control
5//! plane defaults to ([`DEFAULT_CONTROL_ADDR`]) are needed by a pure-server
6//! build, a pure-client build and the all-in-one alike. They lived in
7//! `gunnar-cli` while it was the only binary crate; they moved here when the
8//! server implementation became its own crate, so that `gunnar-cli` and
9//! `gunnar-server` can both depend on this and neither has to depend on the
10//! other.
11//!
12//! Nothing here is an *implementation* of a verb. The `Command` enum names the
13//! verbs and parses their arguments; dispatching them into `gunnar-server`'s
14//! server code or `gunnar-cli`'s client code stays in those crates.
15
16#![warn(clippy::all)]
17#![doc(html_logo_url = "https://gunnar.rs/download/gunnar.webp", html_favicon_url = "https://gunnar.rs/download/favicon.svg")]
18
19/// Where a listener binds — an address, or an interface resolved at boot.
20pub mod bindspec;
21pub mod cli;
22/// A box's own background duty as named jera jobs (A32-14).
23#[cfg(feature = "jobs")]
24pub mod jobs;
25/// Where the metered appliance's verifying key comes from — a file, or the
26/// key itself, spelled inline on a kernel command line.
27pub mod monetize_key;
28pub mod logging;
29pub mod logqueue;
30/// Every INFO+ line as a value, for the appliance console dashboard.
31pub mod logtap;
32#[cfg(feature = "stderr-capture")]
33pub mod stderr_capture;
34
35/// `gunnar/<version>`, the string this binary identifies itself with.
36pub const AGENT: &str = concat!("gunnar/", env!("CARGO_PKG_VERSION"));
37
38/// The build this binary is, as `build.rs` stamped it: the builder's passed
39/// ref (`GUNNAR_BUILD_REF`, then the Build Thing's `NORDISK_BUILD_REF`), else
40/// `git describe --tags --always --dirty=-modified --abbrev=7`, else `unknown`.
41pub const BUILD_REF: &str = env!("GUNNAR_BUILD_REF");
42
43/// Where the control plane listens unless told otherwise.
44///
45/// **Loopback, and it should stay there.** `gunnar serve` binds
46/// `127.0.0.1:50051` by default for defence in depth, so the default target of
47/// `gunnar control …` is the server on this host. Reaching a remote one is an
48/// explicit `--at`.
49///
50/// It lives here, and not in `gunnar-cli`'s `control` module, because `cli.rs`
51/// names it as the default of every control verb's `--at` and `cli.rs` is here.
52pub const DEFAULT_CONTROL_ADDR: &str = "127.0.0.1:50051";
53
54/// **The SSH identification string, carrying the commit this server is.**
55///
56/// `SshConfig::default()` produces `SSH-2.0-gunnar_0.1.0`, which names a manifest
57/// field. That is enough on a machine where you can run `gunnar --version`, and
58/// it is *all there is* on the appliance: the OS-less ISO has no shell, no init
59/// but gunnar itself, and no way in but this listener. MEASURED 2026-08-05, the
60/// forge-built appliance ISO booting under QEMU/OVMF:
61///
62/// ```text
63/// BOOTED AND LISTENING in 5.911152673s — banner "SSH-2.0-gunnar_0.1.0"
64/// ```
65///
66/// Two ISOs built from different commits are indistinguishable over the wire
67/// there, and the only other identity on offer is the file's mtime. So the build
68/// reference goes on the banner, which is the first thing the appliance says and
69/// the one thing it says before authenticating.
70///
71/// # Where it goes, and why not in the version field
72///
73/// RFC 4253 §4.2 is `SSH-protoversion-softwareversion SP comments`, and
74/// *softwareversion* may contain no space and no minus — `git describe` output
75/// has both (`v1.0-4-g008d74b`). So it goes in the optional **comments** field,
76/// after the space, which is exactly where OpenSSH puts its distribution string
77/// (`SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.5`). Nothing parses the comments, and
78/// the kex hash covers the whole string either way, so this is a legal
79/// identification string for every client and a readable one for a person.
80///
81/// An unstamped build (`unknown`, i.e. no `.git` at compile time) adds nothing
82/// rather than advertising the word "unknown" to the network.
83pub fn ssh_server_id() -> String {
84    let base = concat!("SSH-2.0-gunnar_", env!("CARGO_PKG_VERSION"));
85    if BUILD_REF == "unknown" || BUILD_REF.is_empty() {
86        return base.to_string();
87    }
88    format!("{base} {BUILD_REF}")
89}
90
91/// Install the tracing subscriber once, honouring `RUST_LOG`.
92///
93/// `info` by default rather than `warn`. The things an operator most needs to
94/// see on a gunnar server are all logged at `info`: the bound addresses, the
95/// gRPC authentication mode, the `gunnar::audit` trail of who called what, and
96/// one line per connection and one per transfer with its
97/// applied outputs. A default of `warn` would hide every one of them.
98///
99/// # The targets, and how to filter them
100///
101/// Every line gunnar emits is on a `gunnar::` target, listed once in
102/// `gunnar_core::observe::TARGETS`:
103///
104/// ```text
105///   gunnar::audit     who called the control plane, and what was decided
106///   gunnar::ssh       connections: accepted, authenticated, refused
107///   gunnar::git       transfers: upload-pack, receive-pack, objects, bytes
108///   gunnar::policy    the push gate: which gate refused which reference
109///   gunnar::store     ref transactions, CAS contention, repack
110///   gunnar::metrics   the counter heartbeat
111/// ```
112///
113/// ```text
114///   RUST_LOG=warn,gunnar::audit=info,gunnar::git=info   # a quiet production box
115///   RUST_LOG=gunnar::ssh=debug,gunnar::policy=debug     # one incident, in detail
116/// ```
117///
118/// Each connection carries a request id on its span, so every line underneath,
119/// including the ones `gunnar-wire` and `gunnar-policy` emit from inside a
120/// transfer, is greppable by `conn=<id>` without any of them repeating the
121/// principal or the repository.
122pub fn init_tracing(verbose: bool) {
123    // ── the filter is RELOADABLE ────────────────────────────────────────────
124    //
125    // The body lives in [`logging::init`] so the level is changeable on a
126    // running server. This stays as the name everything already calls, so the
127    // change is one file and not thirty call sites (one implementation).
128    let _installed = crate::logging::init(verbose);
129}
130
131#[cfg(test)]
132mod identification_string {
133    use super::*;
134
135    /// **RFC 4253 §4.2, enforced.** `SSH-protoversion-softwareversion SP
136    /// comments`, where *softwareversion* may contain no space and no minus.
137    ///
138    /// This is not decoration. The obvious way to put a build reference on the
139    /// banner is to append it to the version — `SSH-2.0-gunnar_0.1.0-v1.0-4-g…`
140    /// — and `git describe` output is nothing but minuses, so that spelling
141    /// produces an identification string whose *softwareversion* field is
142    /// malformed for every client that parses one. The comments field, after the
143    /// space, is where OpenSSH puts its own distribution string and where this
144    /// belongs. Move the reference back into the version and this fires.
145    #[test]
146    fn the_banner_is_a_legal_identification_string_and_still_names_the_build() {
147        let id = ssh_server_id();
148        assert!(id.starts_with("SSH-2.0-"), "{id}");
149        assert!(
150            !id.contains('\r') && !id.contains('\n'),
151            "russh appends the CRLF; one in the string would end the line early: {id:?}"
152        );
153        assert!(
154            id.len() < 253,
155            "RFC 4253 caps the line at 255 with CRLF: {id}"
156        );
157
158        let body = &id["SSH-2.0-".len()..];
159        let software = body.split(' ').next().expect("a software version");
160        assert!(
161            software.starts_with("gunnar_"),
162            "the software version identifies gunnar: {id}"
163        );
164        assert!(
165            !software.contains('-'),
166            "a minus in the software version makes the identification string malformed — the \
167             build reference belongs in the COMMENTS field after the space: {id}"
168        );
169
170        // And it must actually carry the build, when there is one to carry. A
171        // build with no `.git` stamps `unknown`, and advertising the word
172        // "unknown" to the network is worse than saying nothing.
173        if BUILD_REF == "unknown" {
174            assert_eq!(id, format!("SSH-2.0-gunnar_{}", env!("CARGO_PKG_VERSION")));
175        } else {
176            let comments = body
177                .split_once(' ')
178                .map(|(_, c)| c)
179                .unwrap_or_else(|| panic!("a stamped build puts its reference in comments: {id}"));
180            assert_eq!(comments, BUILD_REF, "{id}");
181            assert!(
182                !comments.is_empty() && !comments.contains(' '),
183                "the comments field is one token: {id}"
184            );
185        }
186    }
187}