Skip to main content

nexcore_network/
interface.rs

1// Copyright (c) 2026 Matthew Campion, PharmD; NexVigilant
2// All Rights Reserved. See LICENSE file for details.
3
4//! Network interface abstraction.
5//!
6//! Tier: T2-C (Σ + μ + ∃ — sum of interface types mapped to hardware existence)
7//!
8//! Every physical or virtual network adapter is an `Interface`. The OS manages
9//! multiple interfaces simultaneously — WiFi + cellular on a phone, Ethernet +
10//! WiFi on a desktop, Bluetooth on a watch.
11
12use serde::{Deserialize, Serialize};
13
14/// Unique identifier for a network interface.
15///
16/// Tier: T2-P (∃ Existence — identity)
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct InterfaceId(String);
19
20impl InterfaceId {
21    /// Create a new interface ID.
22    pub fn new(id: impl Into<String>) -> Self {
23        Self(id.into())
24    }
25
26    /// Get the ID string.
27    pub fn as_str(&self) -> &str {
28        &self.0
29    }
30}
31
32/// Network interface type.
33///
34/// Tier: T2-P (Σ Sum — enumeration of transport types)
35#[non_exhaustive]
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub enum InterfaceType {
38    /// WiFi (802.11).
39    WiFi,
40    /// Cellular (LTE/5G).
41    Cellular,
42    /// Wired Ethernet.
43    Ethernet,
44    /// Bluetooth (PAN/tethering).
45    Bluetooth,
46    /// Loopback (localhost).
47    Loopback,
48    /// VPN tunnel.
49    Vpn,
50    /// Unknown type.
51    Unknown,
52}
53
54impl InterfaceType {
55    /// Human-readable label.
56    pub const fn label(&self) -> &'static str {
57        match self {
58            Self::WiFi => "WiFi",
59            Self::Cellular => "Cellular",
60            Self::Ethernet => "Ethernet",
61            Self::Bluetooth => "Bluetooth",
62            Self::Loopback => "Loopback",
63            Self::Vpn => "VPN",
64            Self::Unknown => "Unknown",
65        }
66    }
67
68    /// Default priority (lower = preferred). Used for route selection.
69    pub const fn default_priority(&self) -> u8 {
70        match self {
71            Self::Ethernet => 10,
72            Self::WiFi => 20,
73            Self::Vpn => 30,
74            Self::Cellular => 40,
75            Self::Bluetooth => 50,
76            Self::Loopback => 100,
77            Self::Unknown => 200,
78        }
79    }
80
81    /// Whether this interface type is metered (costs money per byte).
82    pub const fn is_metered(&self) -> bool {
83        matches!(self, Self::Cellular)
84    }
85}
86
87/// IP address representation (supports IPv4 and IPv6).
88///
89/// Tier: T2-P (λ Location — network address)
90#[non_exhaustive]
91#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
92pub enum IpAddr {
93    /// IPv4 address (4 bytes).
94    V4([u8; 4]),
95    /// IPv6 address (16 bytes).
96    V6([u8; 16]),
97}
98
99impl IpAddr {
100    /// Create an IPv4 address.
101    pub const fn v4(a: u8, b: u8, c: u8, d: u8) -> Self {
102        Self::V4([a, b, c, d])
103    }
104
105    /// Create the loopback address (127.0.0.1).
106    pub const fn loopback_v4() -> Self {
107        Self::V4([127, 0, 0, 1])
108    }
109
110    /// Create the unspecified address (0.0.0.0).
111    pub const fn unspecified_v4() -> Self {
112        Self::V4([0, 0, 0, 0])
113    }
114
115    /// Whether this is a loopback address.
116    pub fn is_loopback(&self) -> bool {
117        match self {
118            Self::V4(b) => b[0] == 127,
119            Self::V6(b) => b[..15].iter().all(|&x| x == 0) && b[15] == 1,
120        }
121    }
122
123    /// Whether this is a private/local address.
124    pub fn is_private(&self) -> bool {
125        match self {
126            Self::V4(b) => {
127                b[0] == 10
128                    || (b[0] == 172 && (16..=31).contains(&b[1]))
129                    || (b[0] == 192 && b[1] == 168)
130            }
131            Self::V6(b) => b[0] == 0xfe && (b[1] & 0xc0) == 0x80,
132        }
133    }
134
135    /// Format as string.
136    pub fn to_string_repr(&self) -> String {
137        match self {
138            Self::V4(b) => format!("{}.{}.{}.{}", b[0], b[1], b[2], b[3]),
139            Self::V6(b) => {
140                // b is exactly [u8; 16]; indices i*2 and i*2+1 for i in 0..8 are always valid
141                let parts: Vec<String> = (0_u8..8_u8)
142                    .map(|i| {
143                        let lo = usize::from(i.saturating_mul(2));
144                        let hi = usize::from(i.saturating_mul(2).saturating_add(1));
145                        let pair = [
146                            b.get(lo).copied().unwrap_or(0),
147                            b.get(hi).copied().unwrap_or(0),
148                        ];
149                        format!("{:x}", u16::from_be_bytes(pair))
150                    })
151                    .collect();
152                parts.join(":")
153            }
154        }
155    }
156}
157
158/// Network interface descriptor.
159///
160/// Tier: T2-C (Σ + μ + ∃ + ς — typed, mapped, existent, stateful)
161#[non_exhaustive]
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct Interface {
164    /// Unique interface ID.
165    pub id: InterfaceId,
166    /// Human-readable name (e.g., "wlan0", "eth0").
167    pub name: String,
168    /// Interface type.
169    pub interface_type: InterfaceType,
170    /// Whether the interface is currently up.
171    pub is_up: bool,
172    /// Assigned IP addresses.
173    pub addresses: Vec<IpAddr>,
174    /// Hardware (MAC) address, if available.
175    pub mac: Option<[u8; 6]>,
176    /// Signal strength (0-100), if applicable.
177    pub signal_strength: Option<u8>,
178    /// Link speed in Mbps, if known.
179    pub link_speed_mbps: Option<u32>,
180    /// Whether this interface is metered.
181    pub is_metered: bool,
182}
183
184impl Interface {
185    /// Create a new interface.
186    pub fn new(
187        id: impl Into<String>,
188        name: impl Into<String>,
189        interface_type: InterfaceType,
190    ) -> Self {
191        Self {
192            id: InterfaceId::new(id),
193            name: name.into(),
194            interface_type,
195            is_up: false,
196            addresses: Vec::new(),
197            mac: None,
198            signal_strength: None,
199            link_speed_mbps: None,
200            is_metered: interface_type.is_metered(),
201        }
202    }
203
204    /// Builder: set the interface as up.
205    pub fn up(mut self) -> Self {
206        self.is_up = true;
207        self
208    }
209
210    /// Builder: add an IP address.
211    pub fn with_address(mut self, addr: IpAddr) -> Self {
212        self.addresses.push(addr);
213        self
214    }
215
216    /// Builder: set MAC address.
217    pub fn with_mac(mut self, mac: [u8; 6]) -> Self {
218        self.mac = Some(mac);
219        self
220    }
221
222    /// Builder: set signal strength.
223    pub fn with_signal(mut self, strength: u8) -> Self {
224        self.signal_strength = Some(strength.min(100));
225        self
226    }
227
228    /// Builder: set link speed.
229    pub fn with_speed(mut self, mbps: u32) -> Self {
230        self.link_speed_mbps = Some(mbps);
231        self
232    }
233
234    /// Whether this interface has any usable address.
235    pub fn has_address(&self) -> bool {
236        !self.addresses.is_empty()
237    }
238
239    /// Whether this interface is ready for traffic.
240    pub fn is_ready(&self) -> bool {
241        self.is_up && self.has_address()
242    }
243
244    /// Get the primary (first) address.
245    pub fn primary_address(&self) -> Option<&IpAddr> {
246        self.addresses.first()
247    }
248
249    /// Effective routing priority (lower = preferred).
250    pub fn effective_priority(&self) -> u16 {
251        let base = u16::from(self.interface_type.default_priority());
252        let signal_penalty: u16 = match self.signal_strength {
253            Some(s) if s < 30 => 50, // Weak signal
254            Some(s) if s < 60 => 20, // Moderate signal
255            _ => 0,
256        };
257        base.saturating_add(signal_penalty)
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn interface_id() {
267        let id = InterfaceId::new("wlan0");
268        assert_eq!(id.as_str(), "wlan0");
269    }
270
271    #[test]
272    fn interface_type_labels() {
273        assert_eq!(InterfaceType::WiFi.label(), "WiFi");
274        assert_eq!(InterfaceType::Cellular.label(), "Cellular");
275        assert_eq!(InterfaceType::Ethernet.label(), "Ethernet");
276    }
277
278    #[test]
279    fn interface_type_priority() {
280        assert!(
281            InterfaceType::Ethernet.default_priority() < InterfaceType::WiFi.default_priority()
282        );
283        assert!(
284            InterfaceType::WiFi.default_priority() < InterfaceType::Cellular.default_priority()
285        );
286    }
287
288    #[test]
289    fn interface_type_metered() {
290        assert!(InterfaceType::Cellular.is_metered());
291        assert!(!InterfaceType::WiFi.is_metered());
292        assert!(!InterfaceType::Ethernet.is_metered());
293    }
294
295    #[test]
296    fn ip_addr_v4() {
297        let addr = IpAddr::v4(192, 168, 1, 100);
298        assert!(addr.is_private());
299        assert!(!addr.is_loopback());
300        assert_eq!(addr.to_string_repr(), "192.168.1.100");
301    }
302
303    #[test]
304    fn ip_addr_loopback() {
305        let addr = IpAddr::loopback_v4();
306        assert!(addr.is_loopback());
307        assert!(!addr.is_private());
308    }
309
310    #[test]
311    fn ip_addr_private_ranges() {
312        assert!(IpAddr::v4(10, 0, 0, 1).is_private());
313        assert!(IpAddr::v4(172, 16, 0, 1).is_private());
314        assert!(IpAddr::v4(192, 168, 0, 1).is_private());
315        assert!(!IpAddr::v4(8, 8, 8, 8).is_private());
316    }
317
318    #[test]
319    fn interface_builder() {
320        let iface = Interface::new("wlan0", "WiFi Adapter", InterfaceType::WiFi)
321            .up()
322            .with_address(IpAddr::v4(192, 168, 1, 100))
323            .with_signal(75)
324            .with_speed(300)
325            .with_mac([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
326
327        assert!(iface.is_up);
328        assert!(iface.has_address());
329        assert!(iface.is_ready());
330        assert_eq!(iface.signal_strength, Some(75));
331        assert_eq!(iface.link_speed_mbps, Some(300));
332        assert!(iface.mac.is_some());
333    }
334
335    #[test]
336    fn interface_not_ready_when_down() {
337        let iface = Interface::new("eth0", "Ethernet", InterfaceType::Ethernet)
338            .with_address(IpAddr::v4(10, 0, 0, 5));
339        assert!(!iface.is_ready()); // has address but is down
340    }
341
342    #[test]
343    fn interface_not_ready_without_address() {
344        let iface = Interface::new("wlan0", "WiFi", InterfaceType::WiFi).up();
345        assert!(!iface.is_ready()); // is up but no address
346    }
347
348    #[test]
349    fn effective_priority_weak_signal() {
350        let strong = Interface::new("w0", "WiFi", InterfaceType::WiFi).with_signal(80);
351        let weak = Interface::new("w1", "WiFi", InterfaceType::WiFi).with_signal(20);
352        assert!(strong.effective_priority() < weak.effective_priority());
353    }
354
355    #[test]
356    fn interface_metered_from_type() {
357        let cell = Interface::new("rmnet0", "Cellular", InterfaceType::Cellular);
358        assert!(cell.is_metered);
359        let wifi = Interface::new("wlan0", "WiFi", InterfaceType::WiFi);
360        assert!(!wifi.is_metered);
361    }
362
363    #[test]
364    fn signal_strength_clamped() {
365        let iface = Interface::new("w0", "WiFi", InterfaceType::WiFi).with_signal(255); // over 100
366        assert_eq!(iface.signal_strength, Some(100));
367    }
368}