gunnar-common 3.0.0

Shared command line, logging, and build identity for the gunnar binaries
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
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
//! **Where a listener binds, when the address is not known until boot.**
//!
//! An appliance on a cloud takes its addresses from DHCP. `203.0.113.152` is
//! UpCloud's answer for ONE server on ONE day; the next box, and the same box
//! after a reinstall, get another. So a medium that is mastered once and
//! installed many times cannot carry a bind ADDRESS — and the alternative
//! everything reached for instead, `0.0.0.0`, is how gunnar's gRPC control
//! plane ended up on the public internet (MEASURED 2026-08-31: `repo list`,
//! `user list` and `repo create` all answered an unrelated caller with no
//! credential at all).
//!
//! The value a medium CAN carry is the **interface**: `eth1` is the utility
//! NIC on every UpCloud server that has one, on every box, forever.
//! [`BindSpec::Interface`] is that spelling, and it is resolved to an address
//! once — after `GUNNAR_NET` has brought the link up and before any listener
//! binds.
//!
//! Since the lean estate (owner 2026-10-23) a box may be laid with its utility NIC alone, and
//! then that NIC is `eth0`. A medium is the same for every box, so it names the ROLE:
//! [`BindSpec::Role`] (`utility:50051`) becomes the box's utility NIC
//! ([`BindSpec::for_this_box`]) and from there is the interface spelling.
//!
//! There is deliberately **no fallback**. An interface with no address is a
//! named refusal that stops the boot, not a quiet slide back to `0.0.0.0`:
//! falling back is what would put the control plane back on the public
//! interface at exactly the moment nobody is watching.

use std::fmt;
use std::net::{Ipv4Addr, SocketAddr};
use std::str::FromStr;

/// What a [`BindSpec`] resolved to.
///
/// Two arms and no third, and neither of them is a wildcard: an address to
/// bind, or a named interface that has none. "Bind everything instead" is not
/// an outcome this type can express, which is the strongest form the guarantee
/// can take.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resolved {
    /// Bind here.
    Bound(SocketAddr),
    /// The interface is present and holds no IPv4 address. Nothing to bind.
    NoAddress {
        /// The interface that was asked about.
        interface: String,
        /// The sentence to log, naming the interface and how to fix it.
        why: String,
    },
}

/// Where a listener binds: a literal socket address, or an interface whose
/// address is learned at boot.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BindSpec {
    /// `127.0.0.1:50051`, `0.0.0.0:50051`, `[::]:50051` — verbatim.
    Addr(SocketAddr),
    /// `eth1:50051` — the IPv4 address `interface` holds when the listener
    /// binds. Refused by name if it holds none.
    Interface {
        /// The interface name, e.g. `eth1`.
        interface: String,
        /// The port to bind on it.
        port: u16,
    },
    /// `utility:50051` — the NIC that plays `role` on THIS box (the lean estate, A19 row 17):
    /// the medium is the same for a box with two NICs and a box laid behind its front with its
    /// utility NIC alone, so it names the role and the box names the NIC
    /// ([`BindSpec::for_this_box`]). Never bound as it is.
    Role {
        /// The role, e.g. [`BindRole::Utility`].
        role: BindRole,
        /// The port to bind on its NIC.
        port: u16,
    },
}

/// The role a NIC plays on a box.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindRole {
    /// The provider's utility (private) network: the LAST NIC the net spec declares that the
    /// box has (`eth0:dhcp,eth1:dhcp`: `eth1` on a box with both, `eth0` on a box with one).
    Utility,
}

impl BindRole {
    /// Its spelling in a bind (`utility`).
    pub fn word(self) -> &'static str {
        match self {
            BindRole::Utility => "utility",
        }
    }
}

/// Pure: **the box's utility NIC**: the last interface the net spec `declared` that this box
/// has (`exists`). A box laid behind its front (the lean estate) has its utility NIC alone, and
/// it boots the same two-NIC spec, so its one NIC is the utility one. No NIC is a named refusal,
/// never a wildcard.
pub fn utility_interface(
    declared: &[String],
    exists: impl Fn(&str) -> bool,
) -> Result<String, String> {
    if declared.is_empty() {
        return Err(
            "`utility:<port>` names the box's utility NIC, and GUNNAR_NET declares no \
             interface to find it among (e.g. GUNNAR_NET=eth0:dhcp,eth1:dhcp). gunnar will NOT \
             fall back to 0.0.0.0."
                .to_string(),
        );
    }
    declared
        .iter()
        .rev()
        .find(|i| exists(i))
        .cloned()
        .ok_or_else(|| {
            format!(
            "`utility:<port>` names the box's utility NIC, and none of the interfaces GUNNAR_NET \
             declares ({}) is on this box. gunnar will NOT fall back to 0.0.0.0.",
            declared.join(", ")
        )
        })
}

impl BindSpec {
    /// Is this an address, and is it a loopback one? An interface spec is
    /// neither — its address is not known yet, and answering "no" for it would
    /// be a guess dressed as a fact.
    #[must_use]
    pub fn is_loopback_addr(&self) -> bool {
        match self {
            BindSpec::Addr(a) => a.ip().is_loopback(),
            BindSpec::Interface { .. } | BindSpec::Role { .. } => false,
        }
    }

    /// The port, whichever spelling this is.
    #[must_use]
    pub fn port(&self) -> u16 {
        match self {
            BindSpec::Addr(a) => a.port(),
            BindSpec::Interface { port, .. } | BindSpec::Role { port, .. } => *port,
        }
    }

    /// **A role becomes this box's NIC** ([`utility_interface`] over the interfaces the net
    /// spec `declared` and the ones the box has); every other spelling is returned unchanged.
    /// Called once, before anything binds.
    pub fn for_this_box(
        self,
        declared: &[String],
        exists: impl Fn(&str) -> bool,
    ) -> Result<BindSpec, String> {
        match self {
            BindSpec::Role {
                role: BindRole::Utility,
                port,
            } => Ok(BindSpec::Interface {
                interface: utility_interface(declared, exists)?,
                port,
            }),
            other => Ok(other),
        }
    }

    /// **Resolve to the address a listener can actually bind.**
    ///
    /// `lookup` answers "what IPv4 does this interface hold" — injected rather
    /// than called directly so this function, which owns every sentence it
    /// produces, is testable without a network stack. `gunnar-server` passes
    /// `pid1::interface_ipv4`.
    ///
    /// **Three outcomes, and the grading is the same law `bring_up_network`
    /// already applies to `GUNNAR_NET` — because it is the same distinction:**
    ///
    /// * an address, or an interface holding one — [`Resolved::Bound`];
    /// * the interface EXISTS and holds no IPv4 — [`Resolved::NoAddress`]. The
    ///   DECLARATION did not lie; the world declined to answer, exactly like a
    ///   DHCP timeout. `Ok`, and the caller decides. On the appliance that is
    ///   "serve git, refuse to serve a control plane, and say so", because
    ///   killing PID 1 there is a reboot loop that takes the git listener with
    ///   it — and a box in a reboot loop is not more secure than a box with no
    ///   control plane, it is only less reachable.
    /// * the interface is not there at all, or the kernel refused the question
    ///   — `Err`. The declaration named a NIC this machine does not have, which
    ///   is a lie a medium told, and it stops the boot the way every other
    ///   lying declaration does.
    ///
    /// No arm anywhere returns a wildcard. That is the whole point.
    pub fn resolve_with<E: fmt::Display>(
        &self,
        lookup: impl FnOnce(&str) -> Result<Option<Ipv4Addr>, E>,
    ) -> Result<Resolved, String> {
        let (interface, port) = match self {
            BindSpec::Addr(addr) => return Ok(Resolved::Bound(*addr)),
            BindSpec::Role { role, port } => {
                return Err(format!(
                    "--grpc-bind {}:{port} names a role, which becomes this box's NIC first \
                     (`BindSpec::for_this_box`); it is never bound as it is, and gunnar will NOT \
                     fall back to 0.0.0.0.",
                    role.word()
                ))
            }
            BindSpec::Interface { interface, port } => (interface, port),
        };
        match lookup(interface) {
            Ok(Some(ip)) => Ok(Resolved::Bound(SocketAddr::from((ip, *port)))),
            Ok(None) => Ok(Resolved::NoAddress {
                interface: interface.clone(),
                why: format!(
                    "--grpc-bind {interface}:{port} names interface {interface:?}, which exists \
                     but holds NO IPv4 address, so there is nothing to bind on it. On an \
                     appliance that means the DHCP handshake on {interface} did not complete — \
                     check that GUNNAR_NET names it (e.g. \
                     GUNNAR_NET=eth0:dhcp,{interface}:dhcp) and that the network answered. \
                     gunnar will NOT fall back to 0.0.0.0: an unauthenticated control plane on \
                     every interface is the defect this spelling exists to close."
                ),
            }),
            Err(err) => Err(format!(
                "--grpc-bind {interface}:{port} names interface {interface:?}, and this host \
                 could not be asked about it: {err}. A medium that names a NIC this machine \
                 does not have is a declaration that lies, and gunnar stops rather than \
                 falling back to 0.0.0.0."
            )),
        }
    }
}

impl From<SocketAddr> for BindSpec {
    fn from(addr: SocketAddr) -> Self {
        BindSpec::Addr(addr)
    }
}

impl fmt::Display for BindSpec {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            BindSpec::Addr(a) => write!(f, "{a}"),
            BindSpec::Interface { interface, port } => write!(f, "{interface}:{port}"),
            BindSpec::Role { role, port } => write!(f, "{}:{port}", role.word()),
        }
    }
}

/// A legal Linux interface name: non-empty, at most `IFNAMSIZ - 1` = 15 bytes,
/// no `/`, no whitespace, no `:` (the separator itself), and not `.`/`..`.
///
/// Checked HERE rather than at resolution, so `--grpc-bind bogus name:50051`
/// is refused by clap with the argument in hand instead of at boot on a box
/// with no console.
fn is_interface_name(s: &str) -> bool {
    !s.is_empty()
        && s.len() <= 15
        && s != "."
        && s != ".."
        && !s
            .bytes()
            .any(|b| b == b'/' || b == b':' || b.is_ascii_whitespace() || b == 0)
}

impl FromStr for BindSpec {
    type Err = String;

    /// `<addr>:<port>` or `<iface>:<port>`.
    ///
    /// The address form is tried FIRST and verbatim, so every value that
    /// parsed before this type existed still parses to exactly what it used to
    /// — `[::1]:50051` included, whose brackets contain the colons that would
    /// otherwise make the split ambiguous.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if let Ok(addr) = s.parse::<SocketAddr>() {
            return Ok(BindSpec::Addr(addr));
        }
        let (host, port) = s.rsplit_once(':').ok_or_else(|| {
            format!("{s:?} is not <addr>:<port> or <interface>:<port> — there is no `:` in it")
        })?;
        let port: u16 = port
            .parse()
            .map_err(|_| format!("{s:?}: {port:?} is not a port number"))?;
        // **Address-SHAPED and not an address.** `203.0.113.999` passes every
        // interface-name rule below — no `/`, no `:`, no whitespace, under
        // IFNAMSIZ — so without this arm a mistyped address is silently accepted
        // as the name of a NIC that will never exist, and the refusal arrives
        // at boot on a box with no console instead of here with the argument
        // in hand. Caught by `malformed_specs_are_refused_rather_than_guessed`,
        // which was RED over exactly this value.
        //
        // The shape test is "digits and dots only", or a leading `[`. No
        // interface name Linux hands out looks like that; every one of them
        // (`eth0`, `ens3`, `enp1s0`, `lo`) carries a letter.
        let address_shaped = host.starts_with('[')
            || (!host.is_empty() && host.bytes().all(|b| b.is_ascii_digit() || b == b'.'));
        if address_shaped {
            // It looked like an address and was not one. Saying "not an
            // interface name" here would send the reader looking for a NIC
            // they never meant to name.
            return Err(format!("{s:?}: {host:?} is not a valid IP address"));
        }
        if !is_interface_name(host) {
            return Err(format!(
                "{s:?}: {host:?} is neither an IP address nor a legal interface name (1..=15 \
                 bytes, no `/`, no `:`, no whitespace)"
            ));
        }
        // **A role, not a NIC** (`utility`): no Linux interface carries that name, and the
        // lean estate needs one medium for a box with two NICs and a box with one.
        if host == BindRole::Utility.word() {
            return Ok(BindSpec::Role {
                role: BindRole::Utility,
                port,
            });
        }
        Ok(BindSpec::Interface {
            interface: host.to_owned(),
            port,
        })
    }
}

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

    /// Every spelling that parsed before this type existed must still parse to
    /// the same socket address. A widening that quietly re-reads an old value
    /// as something else is worse than no widening.
    #[test]
    fn every_address_spelling_still_parses_to_an_address() {
        for (text, want) in [
            ("127.0.0.1:50051", SocketAddr::from(([127, 0, 0, 1], 50051))),
            ("0.0.0.0:50051", SocketAddr::from(([0, 0, 0, 0], 50051))),
            (
                "203.0.113.152:50051",
                SocketAddr::from(([203, 0, 113, 152], 50051)),
            ),
            ("[::1]:50051", "[::1]:50051".parse().unwrap()),
            ("[::]:50051", "[::]:50051".parse().unwrap()),
        ] {
            assert_eq!(
                text.parse::<BindSpec>().expect(text),
                BindSpec::Addr(want),
                "{text} must still parse as an ADDRESS"
            );
            assert_eq!(text.parse::<BindSpec>().unwrap().to_string(), text);
        }
    }

    #[test]
    fn an_interface_name_parses_as_one_and_round_trips() {
        assert_eq!(
            "eth1:50051".parse::<BindSpec>().expect("eth1:50051"),
            BindSpec::Interface {
                interface: "eth1".into(),
                port: 50051
            }
        );
        assert_eq!(
            "eth1:50051".parse::<BindSpec>().unwrap().to_string(),
            "eth1:50051"
        );
    }

    /// The refusals. Each of these was accepted by an earlier draft of
    /// `from_str` that split on the last colon and asked no further questions.
    #[test]
    fn malformed_specs_are_refused_rather_than_guessed() {
        for bad in [
            "eth1",                   // no port
            "eth1:",                  // empty port
            "eth1:notaport",          // not a number
            "eth1:70000",             // out of range
            "203.0.113.999:50051",    // address-shaped and not an address
            "[::1:50051",             // bracket-shaped and not an address
            "eth 1:50051",            // whitespace in a name
            "a/b:50051",              // `/` in a name
            "abcdefghijklmnop:50051", // 16 bytes, IFNAMSIZ is 16 INCLUDING the NUL
            ":50051",                 // no name at all
        ] {
            assert!(
                bad.parse::<BindSpec>().is_err(),
                "{bad:?} was ACCEPTED by BindSpec::from_str"
            );
        }
    }

    /// Measured on the resolver: an address resolves to itself without consulting
    /// the lookup at all, and the proof is that the lookup PANICS if called.
    #[test]
    fn an_address_resolves_to_itself_and_never_asks_the_kernel() {
        let spec: BindSpec = "203.0.113.152:50051".parse().unwrap();
        let got = spec
            .resolve_with(|_| -> Result<Option<Ipv4Addr>, String> {
                panic!("an address must not be looked up")
            })
            .expect("an address resolves");
        assert_eq!(
            got,
            Resolved::Bound(SocketAddr::from(([203, 0, 113, 152], 50051)))
        );
    }

    #[test]
    fn an_interface_resolves_to_the_address_it_holds() {
        let spec: BindSpec = "eth1:50051".parse().unwrap();
        let got = spec
            .resolve_with(|name| -> Result<Option<Ipv4Addr>, String> {
                assert_eq!(name, "eth1");
                Ok(Some(Ipv4Addr::new(203, 0, 113, 152)))
            })
            .expect("eth1 holds an address");
        assert_eq!(
            got,
            Resolved::Bound(SocketAddr::from(([203, 0, 113, 152], 50051)))
        );
    }

    /// **The half that matters.** An interface with no address must REFUSE,
    /// and the refusal must not be a wildcard bind wearing an apology.
    /// **An interface with no address yields NO ADDRESS — never a wildcard,
    /// and never an address belonging to something else.**
    ///
    /// It is `Ok` rather than `Err` because the DECLARATION did not lie: the
    /// NIC is there, the network declined to answer, and that is the grade
    /// `bring_up_network` already gives a DHCP timeout. The caller turns it
    /// into "no control plane", which on the appliance keeps the git listener
    /// alive instead of killing PID 1 into a reboot loop.
    #[test]
    fn an_interface_with_no_address_yields_no_address_and_never_widens() {
        let spec: BindSpec = "eth1:50051".parse().unwrap();
        let got = spec
            .resolve_with(|_| -> Result<Option<Ipv4Addr>, String> { Ok(None) })
            .expect("an interface with no address is not a lying declaration");
        let Resolved::NoAddress { interface, why } = got else {
            panic!("an interface with no address must NOT resolve to a bind: {got:?}");
        };
        assert_eq!(interface, "eth1");
        assert!(
            why.contains("eth1"),
            "the line must name the interface: {why}"
        );
        assert!(
            why.contains("GUNNAR_NET"),
            "the line must say how to fix it: {why}"
        );
        assert!(
            why.contains("0.0.0.0"),
            "the line must say out loud that it does NOT fall back: {why}"
        );
    }

    /// A NIC that is not there at all IS a lying declaration — a medium that
    /// names `eth1` on a one-NIC machine — and that stops the boot. The two
    /// cases are graded differently on purpose; conflating them would either
    /// brick a box whose lease was slow, or quietly serve nothing on a box
    /// whose medium was wrong for it.
    #[test]
    fn a_kernel_that_cannot_be_asked_is_an_error_and_not_merely_addressless() {
        let spec: BindSpec = "eth1:50051".parse().unwrap();
        let err = spec
            .resolve_with(|_| -> Result<Option<Ipv4Addr>, String> {
                Err("no interface named \"eth1\"".into())
            })
            .expect_err("a NIC that does not exist must stop the boot");
        assert!(
            err.contains("eth1"),
            "the refusal must carry the cause: {err}"
        );
        assert!(
            err.contains("0.0.0.0"),
            "and must say it is not widening instead: {err}"
        );
    }

    #[test]
    fn loopback_is_only_claimed_for_an_address_that_is_one() {
        assert!("127.0.0.1:50051"
            .parse::<BindSpec>()
            .unwrap()
            .is_loopback_addr());
        assert!("[::1]:50051"
            .parse::<BindSpec>()
            .unwrap()
            .is_loopback_addr());
        assert!(!"0.0.0.0:50051"
            .parse::<BindSpec>()
            .unwrap()
            .is_loopback_addr());
        assert!(
            !"eth1:50051".parse::<BindSpec>().unwrap().is_loopback_addr(),
            "an unresolved interface must not be claimed as loopback — its address is not known"
        );
    }

    // ── the lean estate: the utility door bound by ROLE (A19 row 17) ──────────

    /// ★ **`utility:<port>` names the box's utility NIC by its role, not by `eth1`** (owner
    /// 2026-10-23, the lean estate): every box boots the same two-NIC spec
    /// (`eth0:dhcp,eth1:dhcp`), and a box laid behind its front has its utility NIC alone, as
    /// `eth0`. The role is the LAST NIC the spec declares that the box has: `eth1` on a box
    /// with both, `eth0` on a box with one. It becomes that interface before anything binds,
    /// and from there it is the interface spelling as ever (no wildcard, no fallback).
    #[test]
    fn the_utility_role_is_the_last_declared_nic_this_box_has() {
        let spec: BindSpec = "utility:50051".parse().expect("the role spelling");
        assert_eq!(
            spec,
            BindSpec::Role {
                role: BindRole::Utility,
                port: 50051
            }
        );
        assert_eq!(spec.to_string(), "utility:50051");
        assert_eq!(spec.port(), 50051);
        assert!(
            !spec.is_loopback_addr(),
            "a role's address is not known yet"
        );
        let declared = ["eth0".to_string(), "eth1".to_string()];
        let eth = |i: &str| BindSpec::Interface {
            interface: i.into(),
            port: 50051,
        };
        assert_eq!(
            spec.clone().for_this_box(&declared, |_| true).unwrap(),
            eth("eth1"),
            "two NICs: the utility one is eth1"
        );
        assert_eq!(
            spec.clone()
                .for_this_box(&declared, |i| i == "eth0")
                .unwrap(),
            eth("eth0"),
            "a lean box: its one NIC is the utility one"
        );
        assert_eq!(
            spec.clone()
                .for_this_box(&["eth0".to_string()], |_| true)
                .unwrap(),
            eth("eth0"),
            "a one-NIC spec"
        );
        let none = spec.clone().for_this_box(&[], |_| true).unwrap_err();
        assert!(
            none.contains("GUNNAR_NET") && none.contains("0.0.0.0"),
            "{none}"
        );
        let absent = spec.clone().for_this_box(&declared, |_| false).unwrap_err();
        assert!(
            absent.contains("eth0") && absent.contains("eth1"),
            "{absent}"
        );
        for kept in ["eth1:50051", "127.0.0.1:50051", "10.6.0.2:50051"] {
            let k: BindSpec = kept.parse().unwrap();
            assert_eq!(
                k.clone().for_this_box(&declared, |_| false).unwrap(),
                k,
                "{kept}: not a role, unchanged"
            );
        }
        let unresolved = spec
            .resolve_with(|_| -> Result<Option<Ipv4Addr>, String> { Ok(Some(Ipv4Addr::LOCALHOST)) })
            .unwrap_err();
        assert!(
            unresolved.contains("utility") && unresolved.contains("0.0.0.0"),
            "a role is never bound as it is: {unresolved}"
        );
    }
}