gunnar-server 3.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
//! **Gunnar's end of PID 1.** Everything an OS-less PID 1 owes the kernel —
//! the signals, the reaper, the netlink writer, the DHCPv4 client and its
//! renewal, the resolver file, the stop sources and the power-off, the install
//! record, the graphics-console mirror, the boot narration's secret door, the
//! panic as a fault — is edda's `appliance-pid1` (0.1.5, lanes G4 and G6), the one PID 1
//! Gunnar and Holger both run. Gunnar's own `pid1.rs` and `dhcp.rs` were moved
//! there whole and deleted here (lane L10).
//!
//! What stays Gunnar's is what Gunnar says about itself: the variable the
//! kernel command line names the network in ([`NET_VAR`]), the host-name
//! prefix, the name the resolver is proven on, the T1 test seam's variable,
//! where the boot narration and the install line go (the console log tap and
//! the dashboard's `install.mode` row), and the panic's code (`GN-99`) with
//! Gunnar's own crash file and console line. The narrator (the `Stage` story,
//! the ESP and `/data` log lines) is [`crate::narrate`].

use std::path::{Path, PathBuf};

use anyhow::Result;

pub use appliance_pid1::net::DhcpSession;

/// The variable the kernel command line declares the network in:
/// `GUNNAR_NET=eth0:dhcp,eth1:dhcp` or `GUNNAR_NET=eth0:10.0.2.15/24:10.0.2.2`.
pub const NET_VAR: &str = "GUNNAR_NET";
/// The DHCP T1 test seam's variable (read only behind the appliance gate).
pub const T1_OVERRIDE_VAR: &str = "GUNNAR_DHCP_T1_OVERRIDE_SECS";
/// A box with no lease name is called `gunnar-<last three MAC bytes>`.
pub const HOSTNAME_PREFIX: &str = "gunnar";
/// The name the published resolver is proven on, once, at boot.
pub const RESOLVER_PROBE: &str = "github.com";
/// **The name this build asks** (AIRGAP B3, owner rule 2026-10-25): a DNS question for a
/// public name is an outbound call, so only a build with `resolver-probe` (every appliance
/// row names it) proves the resolver; a default build asks nothing.
pub const RESOLVER_PROBE_IN_THIS_BUILD: Option<&str> = if cfg!(feature = "resolver-probe") {
    Some(RESOLVER_PROBE)
} else {
    None
};
/// Gunnar's panic code.
pub const PANIC_CODE: &str = "GN-99";

/// The plans `GUNNAR_NET` declares (empty when it declares none).
pub fn net_plans() -> Result<Vec<appliance_pid1::net::NetConfig>> {
    appliance_pid1::net::NetConfig::from_env(NET_VAR)
}

/// Pure: **a bind by ROLE becomes this box's NIC** (the lean estate, A19 row 17):
/// `utility:<port>` is the last interface the net spec `net` (the medium's [`NET_VAR`])
/// declares that this box has (`exists`). Any other spelling is returned unchanged.
pub fn role_bound(
    spec: gunnar_common::bindspec::BindSpec,
    net: Option<&str>,
    exists: impl Fn(&str) -> bool,
) -> Result<gunnar_common::bindspec::BindSpec> {
    if !matches!(spec, gunnar_common::bindspec::BindSpec::Role { .. }) {
        return Ok(spec);
    }
    let declared: Vec<String> = match net.map(str::trim).filter(|n| !n.is_empty()) {
        Some(n) => appliance_pid1::net::NetConfig::parse_list(n)?
            .iter()
            .map(|c| c.interface().to_string())
            .collect(),
        None => Vec::new(),
    };
    spec.for_this_box(&declared, exists)
        .map_err(|why| anyhow::anyhow!("{NET_VAR}: {why}"))
}

/// [`role_bound`] on this box: its [`NET_VAR`] and the NICs the kernel has
/// (`/sys/class/net/<name>`).
pub fn role_bound_here(
    spec: gunnar_common::bindspec::BindSpec,
) -> Result<gunnar_common::bindspec::BindSpec> {
    let net = std::env::var(NET_VAR).ok();
    role_bound(spec, net.as_deref(), |i| {
        std::path::Path::new("/sys/class/net").join(i).exists()
    })
}

/// **Everything PID 1 owes the system before the listeners bind**: edda's
/// `net::bring_up_network_with` over `GUNNAR_NET`, with Gunnar's host-name
/// prefix, its resolver probe and the boot narration handed to the console log
/// tap. A boot DHCP that failed and is asked again in the background lands the
/// same way ([`late_lease`], appliance-pid1 0.1.5): said through the boot
/// narration, renewed on Gunnar's T1 seam and its resolver proven.
pub fn bring_up_network() -> Result<Vec<DhcpSession>> {
    let narrate = |note: &str| ship_boot_narration(note);
    let how = appliance_pid1::net::Bringup {
        hostname_prefix: HOSTNAME_PREFIX,
        resolver_probe: RESOLVER_PROBE_IN_THIS_BUILD,
        narrate: &narrate,
    };
    appliance_pid1::net::bring_up_network_with(net_plans()?, &how, late_lease())
}

/// What a late DHCP lease (the boot's failed ask, retried in the background)
/// says and renews on: the boot narration's own door and Gunnar's T1 seam.
pub fn late_lease() -> appliance_pid1::net::LateLease {
    appliance_pid1::net::LateLease {
        narrate: ship_boot_narration,
        t1_override_var: Some(T1_OVERRIDE_VAR),
    }
}

/// The kernel's story to the boot log the installer collects (edda's
/// `narration::ship_boot_narration`, tapped into `gunnar_common::logqueue`).
pub fn ship_boot_narration(network_note: &str) {
    appliance_pid1::narration::ship_boot_narration(network_note, &|l: &str| {
        gunnar_common::logqueue::tap_line(l)
    });
}

/// Keep the boot's lease alive, forever (edda's `runtime::dhcp_renewal`, with
/// Gunnar's T1 test seam).
#[cfg(feature = "dhcp-client")]
pub async fn dhcp_renewal(session: DhcpSession) {
    appliance_pid1::runtime::dhcp_renewal(
        session,
        appliance_pid1::runtime::dhcp_t1_override(T1_OVERRIDE_VAR),
    )
    .await
}

/// With `dhcp-client` off there is no lease to renew: the session type is
/// uninhabited, so this cannot be called.
#[cfg(not(feature = "dhcp-client"))]
pub async fn dhcp_renewal(session: DhcpSession) {
    match session {}
}

/// **Say whether this /data is the one the box had before its last install**
/// (edda's `install::announce_install_mode`): the set's shape is read off the
/// md sysfs under `data_root` (`dashboard::probe::disk_kind`), and the line is
/// said as the structured `install.mode` event the dashboard's `install` row
/// keys on. Silent when the record is absent.
pub fn announce_install_mode(data_root: &Path) {
    if !Path::new(appliance_pid1::install::INSTALL_RECORD).exists() {
        return;
    }
    let set = crate::dashboard::probe::disk_kind(data_root)
        .and_then(|k| k.set)
        .map(|s| {
            format!(
                "{}\u{d7}{}",
                s.members,
                crate::dashboard::fmt_bytes(s.member_bytes)
            )
        });
    appliance_pid1::install::announce_install_mode(set.as_deref(), &install_mode_event);
}

/// One `install.mode` line (`install.mode mode=… kept=… …`) as the structured
/// event the dashboard reads: each `key=value` token a field, `-` when absent.
pub fn install_mode_event(line: &str) {
    let f = install_mode_fields(line);
    tracing::info!(
        event = "install.mode",
        mode = %f[0],
        kept = %f[1],
        kept_partno = %f[2],
        kept_gib = %f[3],
        kept_set = %f[4],
        "install record read off {}",
        appliance_pid1::install::INSTALL_RECORD
    );
}

/// `mode`, `kept`, `kept_partno`, `kept_gib`, `kept_set` off an edda
/// `install.mode` line, `-` for each one it does not name. Pure.
pub fn install_mode_fields(line: &str) -> [String; 5] {
    let get = |k: &str| {
        line.split_whitespace()
            .find_map(|t| t.strip_prefix(k).and_then(|r| r.strip_prefix('=')))
            .unwrap_or("-")
            .to_owned()
    };
    [
        get("mode"),
        get("kept"),
        get("kept_partno"),
        get("kept_gib"),
        get("kept_set"),
    ]
}

/// **A panic on the appliance ends as a fault** (`GN-99`): edda's
/// `fault::install_panic_fault`, armed only as PID 1 with the narrator
/// installed (never in a test, a laptop's `gunnar-solo` or a container), the
/// crash record under `data`. `shout` is the binary's every-console writer.
///
/// Gunnar's own half rides in `say`: the panic line on every console with
/// Gunnar's wording, its own `crash/panic-<ms>.txt` (`slow::record_crash`),
/// and every fault line through the narrator (console, ESP, `/data`'s boot
/// log) — `shout` when no narrator is installed.
pub fn install_panic_fault(data: Option<PathBuf>, shout: fn(&str)) {
    // The crash record (`crash/fault.txt`) holds the narrator's memory under
    // the fault line, as `narrate::fault`'s record does (appliance-pid1 0.1.5).
    appliance_pid1::fault::narration_from(crate::narrate::memory);
    let p = appliance_pid1::fault::PanicFault {
        code: PANIC_CODE,
        meaning: crate::narrate::code_meaning(PANIC_CODE).unwrap_or("the app panicked"),
        data,
    };
    appliance_pid1::fault::install_panic_fault(
        p,
        || appliance_pid1::net::is_init() && crate::narrate::narrator().is_some(),
        move |line: &str| say_panic_line(line, shout),
    );
}

/// What Gunnar does with each line edda's panic hook says. See [`install_panic_fault`].
fn say_panic_line(line: &str, shout: fn(&str)) {
    if let Some(text) = line.strip_prefix("PANIC: ") {
        let text = format!("\ngunnar PANIC (exit 101): {text}\n");
        shout(&text);
        // And written down, off the stripe: a console message is gone the moment
        // the VNC session closes. A no-op before `ServerStores::open` registered
        // the diagnostics directory.
        if let Some(path) = crate::slow::record_crash(&text) {
            shout(&format!(
                "gunnar: crash record written to {}\n",
                path.display()
            ));
        }
        return;
    }
    match crate::narrate::narrator() {
        Some(n) => n.say(line),
        None => shout(&format!("gunnar: {line}\n")),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// ★ **The control plane's `utility:<port>` becomes this box's utility NIC** (the lean
    /// estate, A19 row 17): read from the net spec the medium declares and the NICs the box
    /// has, before anything binds. A spelling that is not a role passes unchanged.
    #[test]
    fn the_utility_role_becomes_the_nic_this_box_has() {
        use gunnar_common::bindspec::BindSpec;
        let role: BindSpec = "utility:50051".parse().unwrap();
        let iface = |i: &str| BindSpec::Interface {
            interface: i.into(),
            port: 50051,
        };
        assert_eq!(
            role_bound(role.clone(), Some("eth0:dhcp,eth1:dhcp"), |_| true).unwrap(),
            iface("eth1")
        );
        assert_eq!(
            role_bound(role.clone(), Some("eth0:dhcp,eth1:dhcp"), |i| i == "eth0").unwrap(),
            iface("eth0")
        );
        assert!(role_bound(role.clone(), None, |_| true)
            .unwrap_err()
            .to_string()
            .contains(NET_VAR));
        let kept: BindSpec = "127.0.0.1:50051".parse().unwrap();
        assert_eq!(role_bound(kept.clone(), None, |_| false).unwrap(), kept);
    }

    /// The names Gunnar gives edda's PID 1 are the ones every medium, kit and
    /// test already spells.
    #[test]
    fn gunnar_names_its_network_variable_its_host_and_its_panic() {
        assert_eq!(NET_VAR, "GUNNAR_NET");
        assert_eq!(T1_OVERRIDE_VAR, "GUNNAR_DHCP_T1_OVERRIDE_SECS");
        assert_eq!(
            appliance_pid1::net::hostname_for(
                HOSTNAME_PREFIX,
                None,
                [0, 0x11, 0x22, 0xaa, 0xbb, 0xcc]
            ),
            "gunnar-aabbcc"
        );
        assert_eq!(
            appliance_pid1::net::hostname_for(HOSTNAME_PREFIX, Some("vm1.example"), [0; 6]),
            "vm1.example"
        );
        assert_eq!(RESOLVER_PROBE, "github.com");
        assert_eq!(PANIC_CODE, "GN-99");
        assert_eq!(
            crate::narrate::code_meaning(PANIC_CODE),
            Some("the app panicked")
        );
    }

    /// edda's `install.mode` line becomes the fields the dashboard's `install`
    /// row reads, for every shape the record can spell — never a fabricated value.
    #[test]
    fn the_install_line_becomes_the_dashboards_fields() {
        use appliance_pid1::install::InstallRecord;
        let p = InstallRecord::parse(
            "mode=upgrade\nkept=korp-data\nkept_partno=3\nkept_bytes=241591910400\n",
        )
        .line(None);
        assert_eq!(
            install_mode_fields(&p),
            ["upgrade", "korp-data", "3", "225.0", "-"].map(String::from)
        );
        let s = InstallRecord::parse("mode=upgrade\nkept=data-set\n").line(Some("4\u{d7}75G"));
        assert_eq!(
            install_mode_fields(&s),
            ["upgrade", "data-set", "-", "-", "4\u{d7}75G"].map(String::from)
        );
        let s = InstallRecord::parse("mode=upgrade\nkept=data-set\n").line(None);
        assert_eq!(
            install_mode_fields(&s),
            ["upgrade", "data-set", "-", "-", "-"].map(String::from)
        );
        let f = InstallRecord::parse("mode=fresh\n").line(None);
        assert_eq!(
            install_mode_fields(&format!("{f} (install record read off the ESP)")),
            ["fresh", "-", "-", "-", "-"].map(String::from)
        );
    }

    /// ★ **A late lease is said, renewed on Gunnar's T1 seam and its resolver
    /// proven** (lane L10, appliance-pid1 0.1.5): the boot hands edda a
    /// `LateLease` whose line goes through the same boot narration door as the
    /// boot lease's (into the log the installer collects) and whose renewal reads
    /// `GUNNAR_DHCP_T1_OVERRIDE_SECS`; the resolver probe rides on the `Bringup`
    /// edda copies onto the retry. edda's `late_lease_tests` prove what it does
    /// with these; this proves Gunnar hands them over.
    #[test]
    fn a_late_lease_is_narrated_into_the_installers_boot_log() {
        let late = late_lease();
        assert_eq!(late.t1_override_var, Some(T1_OVERRIDE_VAR));
        // The installer's boot log exists with the pairing door (`wg-client`).
        #[cfg(feature = "wg-client")]
        {
            crate::installer_listener::capture();
            (late.narrate)("network up: eth0 10.0.2.15/24 gw Some(10.0.2.2) via DHCP from 10.0.2.2 (lease 3600s) — after the boot's failure, 7 asks l10-late");
            let log = crate::installer_listener::BOOT_LOG
                .since(0, usize::MAX)
                .lines
                .join("\n");
            assert!(
                log.contains("after the boot's failure, 7 asks l10-late"),
                "the late line reaches the boot log: {log}"
            );
        }
        let src = include_str!("boot.rs");
        let up = &src[src.find("pub fn bring_up_network()").unwrap()..];
        let up = &up[..up.find("\n}\n").unwrap()];
        assert!(
            up.contains("bring_up_network_with(net_plans()?, &how, late_lease())"),
            "{up}"
        );
        assert!(
            up.contains("resolver_probe: RESOLVER_PROBE_IN_THIS_BUILD,"),
            "{up}"
        );
    }

    /// `GUNNAR_NET` is parsed by edda with Gunnar's variable: absent is no plan,
    /// and the estate's two-NIC declaration is two plans, eth0 first.
    #[test]
    fn gunnar_net_is_read_from_its_own_variable() {
        // Not set in a test process: no plans, and no interface touched.
        if std::env::var_os(NET_VAR).is_none() {
            assert!(net_plans().unwrap().is_empty());
        }
        let plans = appliance_pid1::net::NetConfig::parse_list(
            "eth0:81.27.107.63/23:81.27.106.1,eth1:10.13.8.78/22",
        )
        .unwrap();
        assert_eq!(
            plans.iter().map(|p| p.interface()).collect::<Vec<_>>(),
            ["eth0", "eth1"]
        );
    }
}

/// **AIRGAP B3** (owner rule 2026-10-25): the boot's resolver proof is a DNS question for
/// [`RESOLVER_PROBE`], a public name, so a default build asks none; the `resolver-probe`
/// feature (named by every appliance row) asks it as before.
#[cfg(test)]
mod airgap {
    #[test]
    fn the_bring_up_asks_the_name_this_build_names() {
        let src = include_str!("boot.rs");
        let up = &src[src.find("pub fn bring_up_network()").unwrap()..];
        let up = &up[..up.find("\n}\n").unwrap()];
        assert!(
            up.contains("resolver_probe: RESOLVER_PROBE_IN_THIS_BUILD,"),
            "{up}"
        );
    }

    #[cfg(not(feature = "resolver-probe"))]
    #[test]
    fn a_default_build_proves_no_resolver_and_asks_no_public_name() {
        assert_eq!(super::RESOLVER_PROBE_IN_THIS_BUILD, None);
    }

    #[cfg(feature = "resolver-probe")]
    #[test]
    fn a_resolver_probe_build_asks_github_com_as_before() {
        assert_eq!(super::RESOLVER_PROBE_IN_THIS_BUILD, Some("github.com"));
    }
}