Skip to main content

rlldpctl/
model.rs

1//! The ergonomic, safe-Rust view of what lldpd knows: interfaces and the
2//! neighbors discovered on them.
3
4use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
5use std::sync::Arc;
6
7/// One local interface, as returned by [`crate::Client::interfaces`].
8///
9/// This is the cheap, no-neighbor-query view (`GET_INTERFACES`); pass its
10/// [`name`](Interface::name) to [`crate::Client::interface`] to get full
11/// details including discovered neighbors.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Interface {
14    pub name: String,
15    pub alias: Option<String>,
16}
17
18/// Full state of one local interface (`GET_INTERFACE`): its own identity plus
19/// every neighbor lldpd has heard from on it.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct InterfaceDetails {
22    pub name: String,
23    pub alias: Option<String>,
24    pub mac_address: [u8; 6],
25    /// The chassis lldpd advertises as *us* on this interface (i.e. what a
26    /// neighbor would see if it looked back at this machine).
27    pub local_chassis: Option<Arc<Chassis>>,
28    pub neighbors: Vec<Neighbor>,
29}
30
31/// One neighbor device seen on an interface.
32///
33/// `chassis` is reference-counted because lldpd deduplicates chassis
34/// internally: two neighbor entries reached via different protocols (say,
35/// LLDP and CDP) but originating from the same physical device share one
36/// `Chassis`. It's an `Arc` rather than a plain `Rc` so that values
37/// containing a `Neighbor` (e.g. `Vec<InterfaceDetails>`) stay `Send` -
38/// needed to `.await` calls that return them from inside a `Send`-bound
39/// future (an `#[async_trait]` method, a `tokio::spawn`ed task, ...), which
40/// is the whole point of this crate's `tokio`-feature async API.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct Neighbor {
43    pub chassis: Arc<Chassis>,
44    pub port_id_subtype: PortIdSubtype,
45    /// Raw port identifier bytes; interpretation depends on `port_id_subtype`
46    /// (e.g. 6 raw bytes for `MacAddress`, a printable string for `IfName`).
47    pub port_id: Vec<u8>,
48    pub port_description: Option<String>,
49    /// Time-to-live announced for this neighbor entry, in seconds.
50    pub ttl: u16,
51}
52
53impl Neighbor {
54    /// Convenience accessor for the common case where `port_id` is a
55    /// printable string (true for `IfAlias`, `IfName`, `Local`, and often in
56    /// practice for others too). Returns `None` on invalid UTF-8.
57    pub fn port_id_str(&self) -> Option<&str> {
58        std::str::from_utf8(&self.port_id).ok()
59    }
60}
61
62/// One neighbor-table change, as pushed by [`crate::Subscription`]
63/// (`NOTIFICATION`, after a `SUBSCRIBE`).
64///
65/// lldpd sends one of these per changed neighbor on *any* interface - the
66/// protocol has no per-interface filtering, so check `interface` yourself if
67/// you only care about some of them.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct NeighborChange {
70    pub interface: String,
71    pub interface_alias: Option<String>,
72    pub kind: NeighborChangeKind,
73    /// The neighbor this change is about. Upstream always populates this in
74    /// practice (see `src/daemon/lldpd.c`'s three call sites of
75    /// `levent_ctl_notify`), but nothing in the protocol *requires* it, so
76    /// this stays an `Option` rather than asserting it's always present.
77    pub neighbor: Option<Neighbor>,
78}
79
80/// What happened to a neighbor entry (`NEIGHBOR_CHANGE_*` in
81/// `src/lldpd-structs.h`).
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum NeighborChangeKind {
84    /// A new neighbor was discovered.
85    Added,
86    /// An existing neighbor's advertised information changed.
87    Updated,
88    /// A neighbor aged out or its port went down.
89    Deleted,
90    /// A value this crate doesn't have a name for.
91    Other(i32),
92}
93
94impl From<i32> for NeighborChangeKind {
95    fn from(v: i32) -> Self {
96        match v {
97            1 => Self::Added,
98            0 => Self::Updated,
99            -1 => Self::Deleted,
100            other => Self::Other(other),
101        }
102    }
103}
104
105/// A neighbor (or local) chassis: the "this whole box" identity, as opposed
106/// to one of its ports.
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct Chassis {
109    pub id_subtype: ChassisIdSubtype,
110    /// Raw chassis identifier bytes; interpretation depends on `id_subtype`.
111    pub id: Vec<u8>,
112    pub name: Option<String>,
113    pub description: Option<String>,
114    pub capabilities_available: Capabilities,
115    pub capabilities_enabled: Capabilities,
116    pub management_addresses: Vec<ManagementAddress>,
117    /// LLDP-MED inventory TLVs, if the neighbor sent any (all fields are
118    /// `None` when it didn't, rather than this being wrapped in an `Option`).
119    pub med: MedInventory,
120}
121
122impl Chassis {
123    /// Convenience accessor for the common case where `id` is a printable
124    /// string (true for `ChassisComponent`, `IfAlias`, `IfName`, `Local`).
125    pub fn id_str(&self) -> Option<&str> {
126        std::str::from_utf8(&self.id).ok()
127    }
128}
129
130/// LLDP-MED "Inventory" TLVs (IEC/TR 61936, TIA-1057 ยง12.3). Populated only if
131/// the neighbor implements LLDP-MED and advertises them.
132#[derive(Debug, Clone, Default, PartialEq, Eq)]
133pub struct MedInventory {
134    pub hardware_revision: Option<String>,
135    pub firmware_revision: Option<String>,
136    pub software_revision: Option<String>,
137    pub serial_number: Option<String>,
138    pub manufacturer: Option<String>,
139    pub model: Option<String>,
140    pub asset_id: Option<String>,
141}
142
143/// A management address advertised by a chassis (IEEE 802.1AB Management
144/// Address TLV).
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct ManagementAddress {
147    family: u32,
148    octets: Vec<u8>,
149    pub interface_index: u32,
150}
151
152impl ManagementAddress {
153    pub(crate) fn new(family: u32, octets: Vec<u8>, interface_index: u32) -> Self {
154        Self {
155            family,
156            octets,
157            interface_index,
158        }
159    }
160
161    /// Decodes the address as an [`IpAddr`], if it is one lldpd recognizes as
162    /// IPv4 or IPv6 (lldpd can in principle carry other address families, in
163    /// which case this returns `None` - use [`Self::raw_octets`] instead).
164    pub fn ip(&self) -> Option<IpAddr> {
165        match self.family {
166            1 if self.octets.len() >= 4 => Some(IpAddr::V4(Ipv4Addr::new(
167                self.octets[0],
168                self.octets[1],
169                self.octets[2],
170                self.octets[3],
171            ))),
172            2 if self.octets.len() >= 16 => {
173                let mut b = [0u8; 16];
174                b.copy_from_slice(&self.octets[..16]);
175                Some(IpAddr::V6(Ipv6Addr::from(b)))
176            }
177            _ => None,
178        }
179    }
180
181    /// The raw address bytes as lldpd stored them (network byte order),
182    /// regardless of address family.
183    pub fn raw_octets(&self) -> &[u8] {
184        &self.octets
185    }
186}
187
188/// `IEEE 802.1AB` chassis ID subtype (`LLDP_CHASSISID_SUBTYPE_*` in
189/// `lldp-const.h`).
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum ChassisIdSubtype {
192    ChassisComponent,
193    IfAlias,
194    PortComponent,
195    MacAddress,
196    NetworkAddress,
197    IfName,
198    Local,
199    /// A value this crate doesn't have a name for (future TLV extension).
200    Other(u8),
201}
202
203impl From<u8> for ChassisIdSubtype {
204    fn from(v: u8) -> Self {
205        match v {
206            1 => Self::ChassisComponent,
207            2 => Self::IfAlias,
208            3 => Self::PortComponent,
209            4 => Self::MacAddress,
210            5 => Self::NetworkAddress,
211            6 => Self::IfName,
212            7 => Self::Local,
213            other => Self::Other(other),
214        }
215    }
216}
217
218/// `IEEE 802.1AB` port ID subtype (`LLDP_PORTID_SUBTYPE_*` in `lldp-const.h`).
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum PortIdSubtype {
221    Unknown,
222    IfAlias,
223    PortComponent,
224    MacAddress,
225    NetworkAddress,
226    IfName,
227    AgentCircuitId,
228    Local,
229    /// A value this crate doesn't have a name for (future TLV extension).
230    Other(u8),
231}
232
233impl From<u8> for PortIdSubtype {
234    fn from(v: u8) -> Self {
235        match v {
236            0 => Self::Unknown,
237            1 => Self::IfAlias,
238            2 => Self::PortComponent,
239            3 => Self::MacAddress,
240            4 => Self::NetworkAddress,
241            5 => Self::IfName,
242            6 => Self::AgentCircuitId,
243            7 => Self::Local,
244            other => Self::Other(other),
245        }
246    }
247}
248
249/// System capabilities bitmap (IEEE 802.1AB Table 8-4). Wraps the raw `u16`
250/// lldpd sends; use [`Capabilities::contains`] to test individual bits.
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
252pub struct Capabilities(pub u16);
253
254impl Capabilities {
255    pub const OTHER: u16 = 0x0001;
256    pub const REPEATER: u16 = 0x0002;
257    pub const BRIDGE: u16 = 0x0004;
258    pub const WLAN_ACCESS_POINT: u16 = 0x0008;
259    pub const ROUTER: u16 = 0x0010;
260    pub const TELEPHONE: u16 = 0x0020;
261    pub const DOCSIS_CABLE_DEVICE: u16 = 0x0040;
262    pub const STATION_ONLY: u16 = 0x0080;
263    pub const CVLAN_COMPONENT: u16 = 0x0100;
264    pub const SVLAN_COMPONENT: u16 = 0x0200;
265    pub const TWO_PORT_MAC_RELAY: u16 = 0x0400;
266
267    pub fn contains(self, bit: u16) -> bool {
268        self.0 & bit == bit
269    }
270}