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}