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