libnet/
lib.rs

1// Copyright 2024 Oxide Computer Company
2
3#![allow(clippy::uninlined_format_args)]
4
5use colored::*;
6use num_enum::TryFromPrimitiveError;
7use oxnet::IpNet;
8use std::collections::BTreeMap;
9use std::fmt::{Display, Formatter};
10use std::net::IpAddr;
11use std::str::FromStr;
12use thiserror::Error;
13use tracing::debug;
14
15/// Structures and functions for interacting with IP network configuration and
16/// state.
17pub mod ip;
18
19/// Structures and functions for interacting with link-layer network
20/// configuration and state.
21pub mod link;
22
23/// Structures and functions for interacting with routing configuration and
24/// state.
25pub mod route;
26
27/// System-level machinery
28pub mod sys;
29
30/// Functions for managing RFC2367 keys
31pub mod pf_key;
32
33mod ioctl;
34mod kstat;
35mod ndpd;
36mod nvlist;
37
38/// Error variants returned by netadm_sys.
39#[derive(thiserror::Error, Debug)]
40pub enum Error {
41    #[error("not implemented")]
42    NotImplemented,
43    #[error("kstat: {0}")]
44    Kstat(String),
45    #[error("ioctl: {0}")]
46    Ioctl(String),
47    #[error("io err: {0}")]
48    Io(#[from] std::io::Error),
49    #[error("encoding error: {0}")]
50    Encoding(#[from] std::str::Utf8Error),
51    #[error("array conversion error: {0}")]
52    Conversion(#[from] std::array::TryFromSliceError),
53    #[error("dlmgmtd: {0}")]
54    Dlmgmtd(String),
55    #[error("ipmgmtd: {0}")]
56    Ipmgmtd(String),
57    #[error("bad argument: {0}")]
58    BadArgument(String),
59    #[error("not found: {0}")]
60    NotFound(String),
61    #[error("already exists: {0}")]
62    AlreadyExists(String),
63    #[error("nvpair: {0}")]
64    NvPair(String),
65    #[error("route error: {0}")]
66    Route(#[from] route::Error),
67    #[error("ndp error: {0}")]
68    Ndp(String),
69    #[error("unrecognized neighbor state: {0}")]
70    NeighborState(#[from] TryFromPrimitiveError<ioctl::NeighborState>),
71}
72
73// Datalink management --------------------------------------------------------
74
75/// Link flags specifiy if a link is active, persistent, or both.
76#[derive(Copy, Clone, Debug, PartialEq, Eq)]
77#[repr(u32)]
78pub enum LinkFlags {
79    Active = 0x1,
80    Persistent = 0x2,
81    ActivePersistent = 0x3,
82}
83
84impl Display for LinkFlags {
85    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
86        match self {
87            LinkFlags::Active => write!(f, "{}", "active".bright_green()),
88            LinkFlags::Persistent => {
89                write!(f, "{}", "persistent".bright_blue())
90            }
91            LinkFlags::ActivePersistent => write!(
92                f,
93                "{} {}",
94                "active".bright_green(),
95                "persistent".bright_blue(),
96            ),
97        }
98    }
99}
100
101/// Link class specifies the type of datalink.
102#[derive(Debug, PartialEq, Eq, Clone)]
103#[repr(C)]
104pub enum LinkClass {
105    Phys = 0x01,
106    Vlan = 0x02,
107    Aggr = 0x04,
108    Vnic = 0x08,
109    Etherstub = 0x10,
110    Simnet = 0x20,
111    Bridge = 0x40,
112    IPtun = 0x80,
113    Part = 0x100,
114    Overlay = 0x200,
115    Misc = 0x400,
116    Tfport = 0x800,
117    All = 0xfff,
118}
119
120impl Display for LinkClass {
121    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
122        match self {
123            LinkClass::Phys => write!(f, "physical"),
124            LinkClass::Vlan => write!(f, "vlan"),
125            LinkClass::Aggr => write!(f, "aggr"),
126            LinkClass::Vnic => write!(f, "vnic"),
127            LinkClass::Etherstub => write!(f, "etherstub"),
128            LinkClass::Simnet => write!(f, "simnet"),
129            LinkClass::Bridge => write!(f, "bridge"),
130            LinkClass::IPtun => write!(f, "iptun"),
131            LinkClass::Part => write!(f, "part"),
132            LinkClass::Misc => write!(f, "misc"),
133            LinkClass::Overlay => write!(f, "overlay"),
134            LinkClass::Tfport => write!(f, "tfport"),
135            LinkClass::All => write!(f, "all"),
136        }
137    }
138}
139
140/// Link state indicates the carrier status of the link.
141#[derive(Debug, PartialEq, Eq, Clone)]
142pub enum LinkState {
143    Unknown,
144    Down,
145    Up,
146}
147
148impl Display for LinkState {
149    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
150        match self {
151            LinkState::Up => {
152                write!(f, "{}", "up".bright_green())
153            }
154            LinkState::Down => {
155                write!(f, "{}", "down".bright_red())
156            }
157            LinkState::Unknown => {
158                write!(f, "{}", "unknown".bright_red())
159            }
160        }
161    }
162}
163
164/// Information about a datalink.
165#[derive(Debug, PartialEq, Eq, Clone)]
166pub struct LinkInfo {
167    pub id: u32,
168    pub name: String,
169    pub flags: LinkFlags,
170    pub class: LinkClass,
171    pub state: LinkState,
172    pub mac: [u8; 6],
173    pub mtu: Option<u32>,
174    pub over: u32,
175}
176
177impl LinkInfo {
178    /// Get a [`LinkHandle`] for the link this object refers to.
179    pub fn handle(&self) -> LinkHandle {
180        LinkHandle::Id(self.id)
181    }
182    /// Get an updated [`LinkInfo`] instance.
183    pub fn update(&mut self) -> Result<(), Error> {
184        *self = get_link(&self.handle())?;
185        Ok(())
186    }
187}
188
189/// A link handle can be either a string or a numeric id.
190#[derive(Debug, Clone)]
191pub enum LinkHandle {
192    Id(u32),
193    Name(String),
194}
195
196impl LinkHandle {
197    pub fn id(&self) -> Result<u32, Error> {
198        Ok(match self {
199            LinkHandle::Id(id) => *id,
200            LinkHandle::Name(name) => linkname_to_id(name)?,
201        })
202    }
203}
204
205impl FromStr for LinkHandle {
206    type Err = Error;
207
208    fn from_str(s: &str) -> Result<Self, Self::Err> {
209        Ok(match s.parse::<u32>() {
210            Ok(id) => Self::Id(id),
211            Err(_) => Self::Name(s.into()),
212        })
213    }
214}
215
216/// Get a vector of all Layer-2 links present on the system.
217pub fn get_links() -> Result<Vec<LinkInfo>, Error> {
218    crate::link::get_links()
219}
220
221/// Get a datalink with the given `id`.
222pub fn get_link(handle: &LinkHandle) -> Result<LinkInfo, Error> {
223    crate::link::get_link(handle.id()?)
224}
225
226/// Given a datalink name, return it's numeric id.
227pub fn linkname_to_id(name: &str) -> Result<u32, Error> {
228    crate::link::linkname_to_id(name)
229}
230
231/// Create a simnet link.
232///
233/// Simnet links are used in pairs. When a pair of simnet links is created,
234/// whaterver ingreses into one flows to the other.
235pub fn create_simnet_link(
236    name: &str,
237    flags: LinkFlags,
238) -> Result<LinkInfo, Error> {
239    debug!("creating simnet link {}", name);
240    crate::link::create_simnet_link(name, flags)
241}
242
243/// Create a tfport link.
244///
245/// Each tfport link is layered on top of a mac device and is associated with
246/// a 16-bit port number.  When the tfport driver receives a packet with a
247/// "sidecar" header attached, it uses the port in that header to forward the
248/// packet to the link associated with that port.  Packets that are transmitted
249/// through a tfport will have a sidecar header prepended by the tfport driver
250/// before forwarding them to the underlying mac device.
251pub fn create_tfport_link(
252    name: &str,
253    over: &str,
254    port: u16,
255    mac: Option<String>,
256    flags: LinkFlags,
257) -> Result<LinkInfo, Error> {
258    debug!("creating tfport link {}", name);
259    crate::link::create_tfport_link(name, over, port, mac, flags)
260}
261
262/// Information about a single tfport link
263pub struct TfportInfo {
264    pub name: String,
265    pub pkt_src: String,
266    pub port: u16,
267    pub mac: String,
268}
269
270/// Given the LinkHandle for a tfport, return the details of the link.
271pub fn get_tfport_info(link: &LinkHandle) -> Result<TfportInfo, Error> {
272    let link = get_link(link)?;
273    if link.class != LinkClass::Tfport {
274        return Err(Error::BadArgument(format!(
275            "{} is not a tfport",
276            link.name
277        )));
278    }
279
280    let info = crate::ioctl::get_tfport_info(link.id).map_err(|_| {
281        Error::Ioctl(format!("failed to get link details for {}", link.name))
282    })?;
283
284    let pkt_src = match crate::link::get_link(info.pktsrc_id) {
285        Ok(l) => l.name,
286        Err(_) => "unknown".to_string(),
287    };
288
289    let mac = {
290        let m = &info.mac_addr;
291        format!(
292            "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
293            m[0], m[1], m[2], m[3], m[4], m[5]
294        )
295    };
296
297    Ok(TfportInfo {
298        name: link.name,
299        pkt_src,
300        port: info.port,
301        mac,
302    })
303}
304
305/// Create a virtual NIC link.
306///
307/// Virtual NICs are devices that are attached to another device. Packets that
308/// ingress the attached device also ingress the VNIC and vice versa.
309pub fn create_vnic_link(
310    name: &str,
311    link: &LinkHandle,
312    mac: Option<Vec<u8>>,
313    flags: LinkFlags,
314) -> Result<LinkInfo, Error> {
315    crate::link::create_vnic_link(name, link.id()?, mac, flags)
316}
317
318/// Delete a data link identified by `handle`.
319pub fn delete_link(handle: &LinkHandle, flags: LinkFlags) -> Result<(), Error> {
320    let id = match handle.id() {
321        Err(Error::NotFound(_)) => return Ok(()),
322        Err(e) => return Err(e),
323        Ok(id) => id,
324    };
325
326    crate::link::delete_link(id, flags)
327}
328
329/// Connect two simnet peers.
330///
331/// This means packets that ingress `a` will egress `a` to `b` and vice versa.
332pub fn connect_simnet_peers(
333    a: &LinkHandle,
334    b: &LinkHandle,
335) -> Result<(), Error> {
336    let a_id = a.id()?;
337    let b_id = b.id()?;
338
339    crate::ioctl::connect_simnet_peers(a_id, b_id)
340    /* only for persistent links
341    crate::link::connect_simnet_peers(a_id, b_id)?;
342    crate::link::connect_simnet_peers(b_id, a_id)
343    */
344}
345
346// IP address management ------------------------------------------------------
347
348/// The state of an IP address in the kernel.
349#[derive(Debug, PartialEq, Eq, Clone)]
350#[repr(i32)]
351pub enum IpState {
352    Disabled = 0,
353    Duplicate = 1,
354    Down = 2,
355    Tentative = 3,
356    OK = 4,
357    Inaccessible = 5,
358}
359
360/// Information in the kernel about an IP address.
361#[derive(Debug, PartialEq, Eq, Clone)]
362pub struct IpInfo {
363    pub ifname: String,
364    pub index: i32,
365    pub addr: IpAddr,
366    pub mask: u32,
367    pub family: u16,
368    pub state: IpState,
369}
370
371impl IpInfo {
372    /// Get the address object associated with this IP address.
373    ///
374    /// The return value is a tuple of the form (name, kind). Name is an illumos
375    /// address object name of the form `link-name`/`address-name` and kind is
376    /// the kind of address such as static, dhcp, etc.
377    pub fn obj(&self) -> Result<(String, String), Error> {
378        match crate::ip::ifname_to_addrobj(&self.ifname, self.family) {
379            Ok((name, kind)) => Ok((name, kind)),
380            Err(e) => Err(Error::Ipmgmtd(e)),
381        }
382    }
383}
384
385/// Get a list of all IP addresses on the system.
386///
387/// The return value is a map whose keys are data link names, and values are the
388/// addresses associated with those links.
389pub fn get_ipaddrs() -> Result<BTreeMap<String, Vec<IpInfo>>, Error> {
390    crate::ioctl::get_ipaddrs()
391    // TODO incorporate more persistent address information from here
392    //let paddrs = crate::ip::get_persistent_ipinfo()
393    //  .map_err(|e| anyhow!("{}", e))?;
394}
395
396/// Get information about a specific IP interface
397pub use crate::ioctl::get_ipaddr_info;
398
399/// Get neighbor information for a particular IP address.
400pub use crate::ioctl::get_neighbor;
401
402/// Create an IP address and give it the provided address object name.
403///
404/// Standard convention is to use a name of the form
405/// `<datalink-name>/<interface-name>`.
406pub fn create_ipaddr(name: impl AsRef<str>, addr: IpNet) -> Result<(), Error> {
407    crate::ioctl::create_ipaddr(name, addr)
408}
409
410/// Enable generation of an IPv6 link-local address for an interface
411pub fn enable_v6_link_local(
412    ifname: impl AsRef<str>,
413    addrname: impl AsRef<str>,
414) -> Result<(), Error> {
415    crate::ioctl::enable_v6_link_local(ifname.as_ref(), addrname.as_ref())
416}
417
418/// Delete an IP address with the given address object name.
419pub fn delete_ipaddr(name: impl AsRef<str>) -> Result<(), Error> {
420    crate::ioctl::delete_ipaddr(name)
421}
422
423/// Check if an IP address with the given address object name exists.
424pub fn ipaddr_exists(name: impl AsRef<str>) -> Result<bool, Error> {
425    crate::ioctl::ipaddr_exists(name)
426}
427
428// Route management -----------------------------------------------------------
429
430/// Get all routes present on the system.
431pub fn get_routes() -> Result<Vec<crate::route::Route>, Error> {
432    Ok(crate::route::get_routes()?)
433}
434
435/// Get route information for the provided `destination`
436pub use crate::route::add_route;
437
438/// Add a route to `destination` via `gateway`.
439pub use crate::route::get_route;
440
441/// Ensure a route to `destination` via `gateway` is present.
442///
443/// Same as `add_route` except no error is returned if the route already exists
444/// on the system.
445pub use crate::route::ensure_route_present;
446
447/// Delete a route to `destination` via `gateway`.
448pub use crate::route::delete_route;
449
450/// A wrapper for LinkInfo that deletes the associated link when dropped. Mostly
451/// for testing purposes< carefully.
452pub struct DropLink {
453    pub info: LinkInfo,
454}
455impl DropLink {
456    pub fn handle(&self) -> LinkHandle {
457        self.info.handle()
458    }
459    pub fn update(&mut self) -> Result<(), Error> {
460        self.info.update()
461    }
462}
463impl Drop for DropLink {
464    fn drop(&mut self) {
465        if let Err(e) = delete_link(&self.info.handle(), self.info.flags) {
466            println!("deleting {} failed: {}", self.info.name, e);
467        }
468    }
469}
470impl From<LinkInfo> for DropLink {
471    fn from(info: LinkInfo) -> Self {
472        Self { info }
473    }
474}
475
476/// A wrapper for IpInfo that deletes the associated address when dropped. Mostly
477/// for testing purposes< carefully.
478pub struct DropIp {
479    pub info: IpInfo,
480}
481impl Drop for DropIp {
482    fn drop(&mut self) {
483        let name = match self.info.obj() {
484            Ok((name, _)) => name,
485            Err(e) => {
486                println!("delete {:?}: obj() failed: {}", self.info, e);
487                return;
488            }
489        };
490        if let Err(e) = delete_ipaddr(name) {
491            println!("delete {:?} failed: {}", self.info, e);
492        }
493    }
494}
495impl From<IpInfo> for DropIp {
496    fn from(info: IpInfo) -> Self {
497        Self { info }
498    }
499}
500
501/// A wrapper for a link name that deletes the associated IPv6 link localaddress
502/// when dropped. Mostly for testing purposes< carefully.
503pub struct DropLinkLocal {
504    pub name: String,
505}
506impl Drop for DropLinkLocal {
507    fn drop(&mut self) {
508        if let Err(e) = delete_ipaddr(&self.name) {
509            println!("delete link-local {:?} failed: {}", self.name, e);
510        }
511    }
512}