Skip to main content

netcore/
link.rs

1//! Layer 1 — thin typed wrappers over what the kernel exposes via netlink/sysfs.
2//!
3//! These are the primitives returned by [`InventoryRaw`](crate::InventoryRaw).
4//! Users only see them via `jip raw *`.
5
6use std::fmt;
7use std::net::{IpAddr, SocketAddr};
8use std::time::Duration;
9
10use serde::{Deserialize, Serialize};
11
12use crate::process::ProcessInfo;
13
14/// A kernel-visible network interface (link).
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Link {
17    pub name: String,
18    pub index: u32,
19    pub kind: LinkKind,
20    pub mac: Option<MacAddr>,
21    pub mtu: u32,
22    pub state: OperState,
23    /// `DORMANT` distinguishes wifi-radio-on-but-not-associated from cable-unplugged.
24    pub linkmode: LinkMode,
25    pub flags: LinkFlags,
26}
27
28/// The type of network link, as inferred from the kernel's layer-2 type and
29/// interface name.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum LinkKind {
33    Ethernet,
34    Wifi,
35    Loopback,
36    Bridge,
37    Veth,
38    Tun,
39    Tap,
40    Wireguard,
41    Vlan,
42    Bond,
43    /// An unrecognised link-layer type; the raw name is preserved.
44    Other(String),
45}
46
47/// RFC 2863 operational state as reported by the kernel via `IF_OPER_*`.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "lowercase")]
50pub enum OperState {
51    /// Link is up and ready to pass traffic.
52    Up,
53    /// Link is administratively or physically down.
54    Down,
55    /// Radio is on but not associated (wifi) or cable connected but no carrier.
56    Dormant,
57    /// State not reported or not applicable (e.g. loopback).
58    Unknown,
59}
60
61/// Kernel link mode (`IFLA_LINKMODE`). `Dormant` means the link waits for an
62/// upper layer (e.g. 802.1X authentication) before coming up.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "lowercase")]
65pub enum LinkMode {
66    /// Normal operation.
67    Default,
68    /// Waiting for upper-layer confirmation before the link is usable.
69    Dormant,
70}
71
72/// IFF_* style flags as reported by the kernel. Stored as strings to avoid a
73/// brittle enum; query via helper methods.
74#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
75pub struct LinkFlags(pub Vec<String>);
76
77impl LinkFlags {
78    /// Return `true` if the given flag name is present (case-insensitive).
79    pub fn has(&self, flag: &str) -> bool {
80        self.0.iter().any(|f| f.eq_ignore_ascii_case(flag))
81    }
82    /// Return `true` when `IFF_LOOPBACK` is set.
83    pub fn is_loopback(&self) -> bool {
84        self.has("LOOPBACK")
85    }
86    /// Return `true` when `IFF_LOWER_UP` is set (physical layer is up).
87    pub fn lower_up(&self) -> bool {
88        self.has("LOWER_UP")
89    }
90    /// Return `true` when the link is administratively up but has no carrier.
91    pub fn no_carrier(&self) -> bool {
92        self.has("NO-CARRIER")
93    }
94}
95
96/// Six-byte link-layer address. Displayed as `aa:bb:cc:dd:ee:ff`.
97#[derive(Clone, Copy, PartialEq, Eq, Hash)]
98pub struct MacAddr(pub [u8; 6]);
99
100impl fmt::Debug for MacAddr {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        fmt::Display::fmt(self, f)
103    }
104}
105
106impl fmt::Display for MacAddr {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        let b = self.0;
109        write!(
110            f,
111            "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
112            b[0], b[1], b[2], b[3], b[4], b[5]
113        )
114    }
115}
116
117impl std::str::FromStr for MacAddr {
118    type Err = MacAddrParseError;
119    fn from_str(s: &str) -> Result<Self, Self::Err> {
120        let mut out = [0u8; 6];
121        let parts: Vec<&str> = s.split([':', '-']).collect();
122        if parts.len() != 6 {
123            return Err(MacAddrParseError);
124        }
125        for (i, p) in parts.iter().enumerate() {
126            out[i] = u8::from_str_radix(p, 16).map_err(|_| MacAddrParseError)?;
127        }
128        Ok(MacAddr(out))
129    }
130}
131
132impl Serialize for MacAddr {
133    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
134        s.collect_str(self)
135    }
136}
137
138impl<'de> Deserialize<'de> for MacAddr {
139    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
140        let s = String::deserialize(d)?;
141        s.parse().map_err(serde::de::Error::custom)
142    }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
146#[error("invalid MAC address")]
147pub struct MacAddrParseError;
148
149/// A single address bound to a link.
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151pub struct Addr {
152    /// The IP address.
153    pub ip: IpAddr,
154    /// Prefix length (CIDR).
155    pub prefix: u8,
156    pub scope: AddrScope,
157    /// Address was assigned dynamically (DHCP or SLAAC).
158    pub dynamic: bool,
159    /// RFC 4941 privacy extension address (IPv6).
160    pub temporary: bool,
161    /// `preferred_lft == 0`. New outgoing connections avoid these.
162    pub deprecated: bool,
163    /// SLAAC "manage temporary addresses" — the stable base from which
164    /// privacy addresses are derived.
165    pub mngtmpaddr: bool,
166    /// `IFA_F_NOPREFIXROUTE`: a connected prefix route was not installed.
167    pub noprefixroute: bool,
168    /// How long the address is valid. `Forever` for static addresses.
169    pub valid_lft: Lifetime,
170    /// How long the address is preferred for new outgoing connections.
171    pub preferred_lft: Lifetime,
172    /// Optional label string (from `ip addr`).
173    pub label: Option<String>,
174}
175
176/// Scope of an IP address, as reported by the kernel (`RT_SCOPE_*`).
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "lowercase")]
179pub enum AddrScope {
180    /// Globally routable address.
181    Global,
182    /// Link-local address (fe80::/10 or 169.254.0.0/16).
183    Link,
184    /// Loopback address.
185    Host,
186    /// Site-local (deprecated, RFC 3879).
187    Site,
188    /// No scope / unreachable.
189    Nowhere,
190}
191
192/// `valid_lft` / `preferred_lft` as reported by the kernel. `forever` is
193/// `u32::MAX` (4294967295) in `ip -j`; backends lift that to [`Lifetime::Forever`].
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
195#[serde(untagged)]
196pub enum Lifetime {
197    Forever,
198    Seconds(u32),
199}
200
201impl Lifetime {
202    /// Return `true` when the lifetime has reached zero (address expired).
203    pub fn is_expired(self) -> bool {
204        matches!(self, Lifetime::Seconds(0))
205    }
206    /// Convert to a [`Duration`], returning `None` for [`Lifetime::Forever`].
207    pub fn as_duration(self) -> Option<Duration> {
208        match self {
209            Lifetime::Forever => None,
210            Lifetime::Seconds(s) => Some(Duration::from_secs(u64::from(s))),
211        }
212    }
213}
214
215/// A kernel routing table entry.
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217pub struct Route {
218    /// Destination prefix or `Default` for the default route.
219    pub dst: RouteDst,
220    /// Next-hop gateway IP, when present.
221    pub gateway: Option<IpAddr>,
222    /// Output interface name, when known.
223    pub oif: Option<String>,
224    /// Route metric (lower = preferred among equal destinations).
225    pub metric: Option<u32>,
226    /// Routing table number (254 = main).
227    pub table: u32,
228    /// Protocol that installed this route (e.g. `"dhcp"`, `"kernel"`).
229    pub protocol: String,
230    pub scope: RouteScope,
231    /// Preferred source address for packets using this route.
232    pub prefsrc: Option<IpAddr>,
233    /// Route flags as strings (e.g. `"onlink"`).
234    pub flags: Vec<String>,
235}
236
237/// Routing table destination: either the default route or a specific prefix.
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239pub enum RouteDst {
240    /// The default route (`0.0.0.0/0` or `::/0`).
241    Default,
242    /// A specific destination prefix.
243    Prefix { ip: IpAddr, prefix: u8 },
244}
245
246/// Scope of a routing table entry (`RT_SCOPE_*`).
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
248#[serde(rename_all = "lowercase")]
249pub enum RouteScope {
250    /// Globally routable.
251    Global,
252    /// Equivalent to `Global`; used by the kernel for unicast routes.
253    Universe,
254    /// Site-scoped (deprecated).
255    Site,
256    /// Directly reachable on the link — no gateway needed.
257    Link,
258    /// Loopback route.
259    Host,
260    /// Unreachable destination.
261    Nowhere,
262}
263
264/// A neighbor table entry (ARP/ND).
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct Neighbor {
267    pub ip: IpAddr,
268    pub lladdr: Option<MacAddr>,
269    pub oif: String,
270    pub state: NeighState,
271    /// `ip -j neigh` encodes router-ness as `"router": null` (the key being
272    /// present at all, with a null value). Backends must normalise that quirk.
273    pub is_router: bool,
274}
275
276/// ARP/ND neighbor cache state (`NUD_*` flags from the kernel).
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
278#[serde(rename_all = "UPPERCASE")]
279pub enum NeighState {
280    /// ARP/ND request sent; reply not yet received.
281    Incomplete,
282    /// L2 address confirmed reachable within the reachable time.
283    Reachable,
284    /// Entry valid but not recently confirmed.
285    Stale,
286    /// Waiting before sending a reachability probe.
287    Delay,
288    /// Actively probing for reachability.
289    Probe,
290    /// ARP/ND failed; address is unreachable at L2.
291    Failed,
292    /// Statically configured entry; never expires.
293    Permanent,
294    /// No ARP needed for this address (e.g. point-to-point links).
295    Noarp,
296    /// No state information available.
297    None,
298}
299
300/// A listening or established socket.
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
302pub struct Socket {
303    pub proto: L4Proto,
304    pub local: SocketAddr,
305    pub remote: Option<SocketAddr>,
306    pub state: TcpState,
307    pub process: ProcessInfo,
308    /// Set when the socket is bound with `SO_BINDTODEVICE` or a `%iface` suffix.
309    pub bound_iface: Option<String>,
310}
311
312/// Layer 4 transport protocol.
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
314#[serde(rename_all = "lowercase")]
315pub enum L4Proto {
316    Tcp,
317    Udp,
318}
319
320/// Kernel TCP state names (also used for UDP: `Unconn` is the UDP "listen").
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(rename_all = "UPPERCASE")]
323pub enum TcpState {
324    Listen,
325    Established,
326    SynSent,
327    SynRecv,
328    FinWait1,
329    FinWait2,
330    TimeWait,
331    Close,
332    CloseWait,
333    LastAck,
334    Closing,
335    Unconn,
336    Unknown,
337}