netget-tool 0.3.4

Network protocol stubbing toolkit: ~280 protocol servers and clients whose responses come from an LLM, a script, a static rule or a person.
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
569
570
571
572
573
574
575
576
577
578
use std::net::{TcpListener, UdpSocket};
use tracing::{debug, warn};

/// A class of local hardware device a protocol needs to open.
///
/// Distinct from every network privilege: opening a Bluetooth adapter or a USB
/// device is neither a port bind nor a socket, and on a correctly configured
/// desktop it needs no elevation at all — a user in the right group, or one who has
/// granted the app permission, simply has it. Declaring `Root` for these would be
/// false *and* would refuse users who do have access.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DeviceClass {
    /// A Bluetooth adapter (BlueZ over D-Bus on Linux, CoreBluetooth on macOS)
    Bluetooth,
    /// A USB device (libusb / IOKit)
    Usb,
    /// An NFC reader (PC/SC — pcscd on Linux, the PCSC framework on macOS)
    Nfc,
}

impl DeviceClass {
    /// Short lowercase name used in messages
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Bluetooth => "Bluetooth adapter",
            Self::Usb => "USB device",
            Self::Nfc => "NFC reader",
        }
    }

    /// Platform-specific hint for a user who has been refused this device class
    pub fn access_hint(&self) -> &'static str {
        match self {
            Self::Bluetooth => {
                "Linux: install/start BlueZ and ensure a powered adapter exists \
                 (`bluetoothctl list`). macOS: grant Bluetooth permission in \
                 System Settings > Privacy & Security > Bluetooth."
            }
            Self::Usb => {
                "Linux: add a udev rule granting your user access to the device, or \
                 join the group owning /dev/bus/usb. macOS: no elevation is normally \
                 needed."
            }
            Self::Nfc => {
                "Linux: install and start pcscd (`systemctl start pcscd`). macOS: the \
                 PCSC framework is built in."
            }
        }
    }
}

/// System capabilities detected at startup
#[derive(Debug, Clone)]
pub struct SystemCapabilities {
    /// Whether we can bind to privileged ports (< 1024)
    pub can_bind_privileged_ports: bool,
    /// Whether we can open **raw IP sockets** (`SOCK_RAW`) — what ICMP, IGMP and
    /// OSPF need. On Linux this is `CAP_NET_RAW`; elsewhere it is root.
    ///
    /// Deliberately *not* the same thing as [`Self::has_packet_capture_access`]:
    /// this flag used to cover both, and a single flag cannot refuse an ICMP server
    /// for a user who only has capture rights.
    pub has_raw_socket_access: bool,
    /// Whether we can capture/inject at layer 2 via BPF or `AF_PACKET` — what ARP,
    /// DataLink and IS-IS need.
    ///
    /// Genuinely separable from raw IP sockets: a macOS user in the ChmodBPF group
    /// owns `/dev/bpf*` without being root, and so has capture but not raw sockets.
    pub has_packet_capture_access: bool,
    /// Whether a Bluetooth adapter appears to be reachable
    pub has_bluetooth_access: bool,
    /// Whether USB devices appear to be reachable
    pub has_usb_access: bool,
    /// Whether an NFC/PC-SC reader stack appears to be reachable
    pub has_nfc_access: bool,
    /// Whether running as root/administrator
    pub is_root: bool,
}

impl SystemCapabilities {
    /// Detect system capabilities at startup
    pub fn detect() -> Self {
        // A browser page has no root, no raw sockets and no devices; and on the virtual
        // network of the wasm build any port number binds, so the privileged-port gate in
        // `server_startup` must not refuse 80 or 23.
        #[cfg(target_arch = "wasm32")]
        {
            return Self {
                can_bind_privileged_ports: true,
                has_raw_socket_access: false,
                has_packet_capture_access: false,
                has_bluetooth_access: false,
                has_usb_access: false,
                has_nfc_access: false,
                is_root: false,
            };
        }
        #[cfg(not(target_arch = "wasm32"))]
        Self::probe()
    }

    #[cfg(not(target_arch = "wasm32"))]
    fn probe() -> Self {
        let is_root = is_running_as_root();
        let can_bind_privileged_ports = can_bind_privileged_port();
        let has_raw_socket_access = has_raw_ip_socket_capability();
        let has_packet_capture_access = has_packet_capture_capability();
        let has_bluetooth_access = has_bluetooth_capability();
        let has_usb_access = has_usb_capability();
        let has_nfc_access = has_nfc_capability();

        debug!(
            "Detected capabilities: root={}, privileged_ports={}, raw_ip_sockets={}, \
             packet_capture={}, bluetooth={}, usb={}, nfc={}",
            is_root,
            can_bind_privileged_ports,
            has_raw_socket_access,
            has_packet_capture_access,
            has_bluetooth_access,
            has_usb_access,
            has_nfc_access
        );

        Self {
            can_bind_privileged_ports,
            has_raw_socket_access,
            has_packet_capture_access,
            has_bluetooth_access,
            has_usb_access,
            has_nfc_access,
            is_root,
        }
    }

    /// Whether the given device class appears to be reachable
    pub fn has_device_access(&self, class: DeviceClass) -> bool {
        match class {
            DeviceClass::Bluetooth => self.has_bluetooth_access,
            DeviceClass::Usb => self.has_usb_access,
            DeviceClass::Nfc => self.has_nfc_access,
        }
    }

    /// Get a human-readable description of capabilities
    pub fn description(&self) -> String {
        let mut parts = Vec::new();

        if self.is_root {
            parts.push("running as root/admin");
        }

        parts.push(if self.can_bind_privileged_ports {
            "privileged ports available"
        } else {
            "privileged ports unavailable"
        });

        parts.push(if self.has_raw_socket_access {
            "raw IP sockets available"
        } else {
            "raw IP sockets unavailable"
        });

        parts.push(if self.has_packet_capture_access {
            "packet capture available"
        } else {
            "packet capture unavailable"
        });

        // Device access is only worth mentioning when something is missing: on a
        // normal desktop all three are present and the line is noise.
        if !self.has_bluetooth_access {
            parts.push("no Bluetooth adapter");
        }
        if !self.has_usb_access {
            parts.push("no USB access");
        }
        if !self.has_nfc_access {
            parts.push("no NFC reader stack");
        }

        parts.join(", ")
    }
}

/// Check if running as root/administrator
fn is_running_as_root() -> bool {
    #[cfg(unix)]
    {
        // geteuid() is the actual answer. The previous implementation stat'd /root
        // and compared $USER/$LOGNAME, which is wrong in both directions: it reported
        // root under `sudo -E` (which preserves $USER) or in any container with a
        // world-readable /root, and reported non-root for a root account whose home
        // is not /root.
        //
        // SAFETY: geteuid() takes no arguments, cannot fail, and touches no memory.
        unsafe { libc::geteuid() == 0 }
    }

    #[cfg(windows)]
    {
        // On Windows, check if running as Administrator
        // For now, return false - can be enhanced later with Windows-specific APIs
        false
    }

    #[cfg(not(any(unix, windows)))]
    {
        false
    }
}

/// What a single privileged-port bind attempt actually proved.
///
/// The distinction exists because a failed bind is not one fact but three, and the
/// original probe collapsed them: it tried 80, 67, 123 and 53, and returned `false` if
/// none bound. A developer machine with anything already listening on those ports —
/// a local web server on 80, `dnsmasq` on 53, an NTP daemon on 123 — was therefore
/// reported as unable to bind privileged ports, and `server_startup` refused to start
/// servers the machine could in fact run. "Someone else holds this port" says nothing
/// whatsoever about this process's privilege.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PortProbe {
    /// The bind succeeded: proof the process *can* bind privileged ports.
    Bound,
    /// `EACCES`/`EPERM`: proof it *cannot*.
    Denied,
    /// `EADDRINUSE`/`EADDRNOTAVAIL`, or any other error: the port was unavailable for
    /// reasons unrelated to privilege, so this attempt proved nothing either way.
    Inconclusive,
}

/// Classify a failed `bind(2)` by what it says about privilege.
pub fn classify_bind_error(err: &std::io::Error) -> PortProbe {
    match err.kind() {
        std::io::ErrorKind::PermissionDenied => PortProbe::Denied,
        // AddrInUse: another process holds it. AddrNotAvailable: the address is not
        // ours to bind. Neither is a statement about privilege.
        std::io::ErrorKind::AddrInUse | std::io::ErrorKind::AddrNotAvailable => {
            PortProbe::Inconclusive
        }
        _ => PortProbe::Inconclusive,
    }
}

/// Decide the capability from a set of probe outcomes.
///
/// - Any `Bound` ⇒ capable. One success is proof.
/// - Otherwise any `Denied` ⇒ not capable. The kernel said so explicitly.
/// - Otherwise (every probe inconclusive, e.g. every test port already in use) the
///   honest answer is **unknown**, and we report `true` — capable.
///
/// That last choice is deliberate. This flag *hard-gates* startup in
/// `server_startup.rs`, so mapping "unknown" to `false` turns "we could not tell" into
/// a refusal, which is the failure mode the capability probes were rewritten to
/// eliminate. Reporting `true` on unknown costs nothing worse than the real `bind`
/// failing a moment later with its own `EACCES`, which is a clear, specific, actionable
/// error — strictly better than refusing a machine that would have worked. Note this is
/// a *capability* probe, not an authorization decision: the permissive default here
/// grants no access that the kernel would not independently grant.
pub fn privileged_port_capability(probes: &[PortProbe]) -> bool {
    if probes.contains(&PortProbe::Bound) {
        return true;
    }
    if probes.contains(&PortProbe::Denied) {
        return false;
    }
    true
}

/// Check if we can bind to privileged ports (< 1024)
///
/// Probes several well-known privileged ports and classifies each failure; see
/// [`PortProbe`] and [`privileged_port_capability`] for why the failures must be
/// told apart.
fn can_bind_privileged_port() -> bool {
    // If we're root, we definitely can
    if is_running_as_root() {
        return true;
    }

    // Try to bind to common privileged ports
    // Use a quick test bind that doesn't actually listen
    let test_ports = [80, 67, 123, 53];
    let mut probes = Vec::with_capacity(test_ports.len() * 2);

    for port in test_ports {
        let addr = format!("127.0.0.1:{}", port);

        // Try TCP first
        match TcpListener::bind(&addr) {
            Ok(listener) => {
                drop(listener);
                debug!("Successfully test-bound to privileged TCP port {}", port);
                return true;
            }
            Err(e) => {
                let outcome = classify_bind_error(&e);
                debug!(
                    "TCP bind to privileged port {} failed: {} ({:?})",
                    port, e, outcome
                );
                probes.push(outcome);
            }
        }

        // Try UDP
        match UdpSocket::bind(&addr) {
            Ok(socket) => {
                drop(socket);
                debug!("Successfully test-bound to privileged UDP port {}", port);
                return true;
            }
            Err(e) => {
                let outcome = classify_bind_error(&e);
                debug!(
                    "UDP bind to privileged port {} failed: {} ({:?})",
                    port, e, outcome
                );
                probes.push(outcome);
            }
        }
    }

    // If all test binds failed, check if it's because of privileges or port in use
    // Try binding to a high port to make sure networking works
    if TcpListener::bind("127.0.0.1:0").is_err() {
        warn!("Cannot bind to any ports - networking may be broken");
    }

    let capable = privileged_port_capability(&probes);
    if capable {
        warn!(
            "Every privileged test port ({:?}) was unavailable for reasons other than \
             permission - assuming privileged ports are bindable. A server on a port \
             below 1024 will report the real error if this assumption is wrong.",
            test_ports
        );
    } else {
        debug!("Cannot bind to privileged ports - requires elevated privileges");
    }
    capable
}

/// Check whether this process can open a **raw IP socket** (`SOCK_RAW`).
///
/// This is the capability ICMP, IGMP and OSPF need, and it must be *probed*, not
/// inferred. The original implementation called `pcap::Device::list()`, which is a
/// thin wrapper over `getifaddrs(3)` and succeeds for any unprivileged user — so it
/// reported `true` unconditionally, which in turn made the pre-flight in
/// `server_startup` never fire. Every raw-socket protocol then failed later with an
/// opaque `EPERM` from its own socket call instead of a clear refusal at startup.
///
/// Layer-2 capture is a *separate* capability; see
/// [`has_packet_capture_capability`]. Merging the two — as a single flag once did —
/// makes it impossible to refuse an ICMP server for a capture-only user.
fn has_raw_ip_socket_capability() -> bool {
    #[cfg(unix)]
    {
        if is_running_as_root() {
            return true;
        }

        // A raw IP socket is the definitive test: it succeeds exactly with
        // CAP_NET_RAW on Linux, and only as root on macOS/BSD. Note SOCK_RAW
        // specifically — Linux lets unprivileged users open SOCK_DGRAM ICMP sockets
        // via ping_group_range, which is not the capability these protocols need.
        //
        // SAFETY: socket(2) with constant arguments; the fd is closed immediately
        // on success and nothing else touches it.
        let raw_fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_RAW, libc::IPPROTO_ICMP) };
        if raw_fd >= 0 {
            unsafe { libc::close(raw_fd) };
            debug!("Opened a raw ICMP socket - raw IP socket access available");
            return true;
        }

        debug!(
            "Raw ICMP socket failed ({}) and not root - raw-socket protocols \
             (ICMP, IGMP, OSPF) will be refused at startup.",
            std::io::Error::last_os_error()
        );
        false
    }

    #[cfg(windows)]
    {
        is_running_as_root()
    }

    #[cfg(not(any(unix, windows)))]
    {
        false
    }
}

/// Check whether this process can capture or inject frames at layer 2.
///
/// This is what ARP, DataLink and IS-IS need, and it is genuinely distinct from a
/// raw IP socket: a macOS user in the ChmodBPF group owns `/dev/bpf*` and can
/// capture without being root, while still being unable to open `SOCK_RAW`.
fn has_packet_capture_capability() -> bool {
    #[cfg(unix)]
    {
        if is_running_as_root() {
            return true;
        }

        // Linux: AF_PACKET is the capture path, gated on CAP_NET_RAW.
        //
        // SAFETY: socket(2) with constant arguments; the fd is closed immediately
        // on success and nothing else touches it.
        #[cfg(target_os = "linux")]
        {
            let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, 0) };
            if fd >= 0 {
                unsafe { libc::close(fd) };
                debug!("Opened an AF_PACKET socket - L2 capture available");
                return true;
            }
        }

        // macOS/BSD: capture goes through a /dev/bpf* clone device, whose
        // permissions can be relaxed independently of root (ChmodBPF).
        //
        // **A BPF device is exclusive: one open handle each.** So "could not open
        // /dev/bpf0" says nothing about permission when some other capture already
        // holds it — libpcap itself answers this by scanning upward until it finds a
        // free one, and this probe has to do the same or it will disagree with the
        // library it is gating.
        //
        // Scanning only 0..4 made the answer depend on load. Running the suite at
        // --test-threads=100, or alongside another netget, occupies the first few
        // devices and the probe then reported "no capture access" on a host that
        // plainly had it — `is_met_by()` refuses ARP, DataLink and IS-IS at startup on
        // that answer, so those servers became un-startable whenever the machine was
        // busy. It showed up as `isis_spawn_outcome_matches_capture_privilege` failing
        // with "spawn returned Ok without capture privilege": the capture opened fine,
        // and only the probe disagreed.
        //
        // `EBUSY` is therefore *positive* evidence — the device exists and we were
        // allowed to try, it is merely in use. Only `EACCES`/`EPERM` on every device we
        // can see is a real denial.
        #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
        {
            let mut denied = false;
            for n in 0..256 {
                match std::fs::File::open(format!("/dev/bpf{}", n)) {
                    Ok(_) => {
                        debug!("Opened /dev/bpf{} - L2 capture available without root", n);
                        return true;
                    }
                    Err(e) => match e.raw_os_error() {
                        // In use by someone else: we were permitted to try, which is
                        // what the caller is actually asking about.
                        Some(libc::EBUSY) => {
                            debug!("/dev/bpf{} is busy - capture access is present", n);
                            return true;
                        }
                        Some(libc::EACCES) | Some(libc::EPERM) => {
                            denied = true;
                            continue;
                        }
                        // Past the last device the system created.
                        Some(libc::ENOENT) | Some(libc::ENXIO) => break,
                        _ => continue,
                    },
                }
            }
            if denied {
                debug!("every /dev/bpf* refused permission - no L2 capture access");
            }
        }

        debug!(
            "No capture handle available ({}) and not root - capture protocols \
             (ARP, DataLink, IS-IS) will be refused at startup.",
            std::io::Error::last_os_error()
        );
        false
    }

    #[cfg(windows)]
    {
        // Npcap/WinPcap driver access requires Administrator.
        is_running_as_root()
    }

    #[cfg(not(any(unix, windows)))]
    {
        false
    }
}

/// Whether any of the given paths exists (used by the Linux device probes).
#[cfg(target_os = "linux")]
fn any_path_exists(paths: &[&str]) -> bool {
    paths.iter().any(|p| std::path::Path::new(p).exists())
}

/// Probe for a usable Bluetooth adapter.
///
/// Device probes are deliberately **permissive**: they return `false` only on
/// positive evidence that the stack is absent, never on "could not tell". Refusing
/// a start is a hard failure, and the alternative — the protocol failing later with
/// its own diagnostic — is strictly better than refusing a user who does have
/// access.
fn has_bluetooth_capability() -> bool {
    #[cfg(target_os = "linux")]
    {
        // BlueZ is reached over the D-Bus system bus, and an adapter shows up under
        // /sys/class/bluetooth. Both are needed; neither requires root.
        let bus = any_path_exists(&[
            "/run/dbus/system_bus_socket",
            "/var/run/dbus/system_bus_socket",
        ]);
        let adapter = std::fs::read_dir("/sys/class/bluetooth")
            .map(|mut d| d.next().is_some())
            .unwrap_or(false);
        if !bus {
            debug!("No D-Bus system bus socket - BlueZ is unreachable");
        } else if !adapter {
            debug!("No adapter under /sys/class/bluetooth");
        }
        bus && adapter
    }

    // macOS gates CoreBluetooth through TCC, which cannot be probed without
    // triggering the permission prompt, so assume available and let the protocol
    // report the real error. Same for any platform we have no probe for.
    #[cfg(not(target_os = "linux"))]
    {
        true
    }
}

/// Probe for usable USB device access (see [`has_bluetooth_capability`] on why
/// these probes are permissive).
fn has_usb_capability() -> bool {
    #[cfg(target_os = "linux")]
    {
        // libusb enumerates through /dev/bus/usb; per-device write access is a udev
        // matter we cannot check generically, so existence is the whole test.
        let present = std::path::Path::new("/dev/bus/usb").is_dir();
        if !present {
            debug!("/dev/bus/usb is absent - usbfs is not mounted");
        }
        present
    }

    // macOS reaches USB through IOKit, which needs no elevation for the device
    // classes NetGet uses.
    #[cfg(not(target_os = "linux"))]
    {
        true
    }
}

/// Probe for a usable NFC/PC-SC reader stack (see [`has_bluetooth_capability`] on
/// why these probes are permissive).
fn has_nfc_capability() -> bool {
    #[cfg(target_os = "linux")]
    {
        // The pcsc crate talks to pcscd over its socket; without the daemon running
        // there is nothing to talk to.
        let present = any_path_exists(&["/run/pcscd/pcscd.comm", "/var/run/pcscd/pcscd.comm"]);
        if !present {
            debug!("No pcscd socket - the PC/SC daemon is not running");
        }
        present
    }

    // macOS ships the PCSC framework; other platforms are not probed.
    #[cfg(not(target_os = "linux"))]
    {
        true
    }
}