ndn_faces/
iface_watcher.rs

1//! Dynamic network interface add/remove watcher.
2//!
3//! When `watch_interfaces` is enabled in `[face_system]`, the router spawns a
4//! background task that subscribes to OS network interface events and notifies
5//! the face system when interfaces appear or disappear.
6//!
7//! ## Platform support
8//!
9//! - **Linux**: `RTMGRP_LINK` netlink socket (RTM_NEWLINK / RTM_DELLINK).
10//! - **macOS**: not yet supported — logs a warning and the task exits.
11//! - **Windows**: not yet supported — logs a warning and the task exits.
12
13/// An interface lifecycle event delivered by the watcher task.
14#[derive(Debug, Clone)]
15pub enum InterfaceEvent {
16    /// A new interface has appeared (or come up).
17    Added(String),
18    /// An interface has been removed (or gone down permanently).
19    Removed(String),
20}
21
22/// Spawn an async task that watches for interface add/remove events.
23///
24/// Events are sent on `tx`.  The task exits when the receiver is dropped or
25/// `cancel` is triggered.
26///
27/// On unsupported platforms the task logs a warning and returns immediately.
28pub async fn watch_interfaces(
29    tx: tokio::sync::mpsc::Sender<InterfaceEvent>,
30    cancel: tokio_util::sync::CancellationToken,
31) {
32    #[cfg(target_os = "linux")]
33    {
34        watch_interfaces_linux(tx, cancel).await;
35    }
36    #[cfg(not(target_os = "linux"))]
37    {
38        let _ = (tx, cancel);
39        tracing::warn!(
40            "`watch_interfaces` is only supported on Linux; \
41             interface hotplug disabled on this platform"
42        );
43    }
44}
45
46// ── Linux implementation ──────────────────────────────────────────────────────
47
48#[cfg(target_os = "linux")]
49async fn watch_interfaces_linux(
50    tx: tokio::sync::mpsc::Sender<InterfaceEvent>,
51    cancel: tokio_util::sync::CancellationToken,
52) {
53    use std::os::unix::io::OwnedFd;
54    use tokio::io::unix::AsyncFd;
55
56    // RTM_NEWLINK = 16 (RTM_DELLINK = 17 is handled by the else branch below)
57    const RTM_NEWLINK: u16 = 16;
58
59    // Open NETLINK_ROUTE socket subscribed to RTMGRP_LINK.
60    let fd: i32 = unsafe {
61        libc::socket(
62            libc::AF_NETLINK,
63            libc::SOCK_RAW | libc::SOCK_CLOEXEC | libc::SOCK_NONBLOCK,
64            libc::NETLINK_ROUTE,
65        )
66    };
67    if fd < 0 {
68        tracing::warn!(
69            error = %std::io::Error::last_os_error(),
70            "failed to open netlink socket for interface watching"
71        );
72        return;
73    }
74
75    // Bind to RTMGRP_LINK multicast group.
76    // SAFETY: sockaddr_nl is a plain C struct; zero-initialising is correct
77    // (nl_pid=0 means kernel assigns, nl_pad=0 is reserved).
78    let mut addr: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
79    addr.nl_family = libc::AF_NETLINK as u16;
80    // RTMGRP_LINK = 1 is a stable Linux kernel ABI constant.
81    // We use a literal to avoid libc type variance (c_uint vs c_int across versions).
82    addr.nl_groups = 1;
83    let rc = unsafe {
84        libc::bind(
85            fd,
86            &addr as *const libc::sockaddr_nl as *const libc::sockaddr,
87            std::mem::size_of::<libc::sockaddr_nl>() as u32,
88        )
89    };
90    if rc != 0 {
91        tracing::warn!(
92            error = %std::io::Error::last_os_error(),
93            "failed to bind netlink socket — interface hotplug disabled"
94        );
95        unsafe {
96            libc::close(fd);
97        }
98        return;
99    }
100
101    // Wrap in OwnedFd so it closes on drop.
102    let owned: OwnedFd = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(fd) };
103    let async_fd = match AsyncFd::new(owned) {
104        Ok(f) => f,
105        Err(e) => {
106            tracing::warn!(error=%e, "failed to register netlink fd with tokio");
107            return;
108        }
109    };
110
111    tracing::info!("interface watcher active (netlink RTMGRP_LINK)");
112
113    let mut buf = vec![0u8; 8192];
114
115    loop {
116        tokio::select! {
117            _ = cancel.cancelled() => break,
118            result = async_fd.readable() => {
119                let mut guard = match result {
120                    Ok(g) => g,
121                    Err(e) => {
122                        tracing::warn!(error=%e, "netlink read error");
123                        break;
124                    }
125                };
126                let n = unsafe {
127                    libc::recv(
128                        async_fd.as_raw_fd(),
129                        buf.as_mut_ptr() as *mut libc::c_void,
130                        buf.len(),
131                        0,
132                    )
133                };
134                guard.clear_ready();
135                if n <= 0 {
136                    continue;
137                }
138                // Parse netlink messages from the buffer.
139                let msgs = parse_rtm_link_messages(&buf[..n as usize]);
140                for (msg_type, iface_name) in msgs {
141                    let event = if msg_type == RTM_NEWLINK {
142                        InterfaceEvent::Added(iface_name.clone())
143                    } else {
144                        InterfaceEvent::Removed(iface_name.clone())
145                    };
146                    tracing::debug!(
147                        iface = %iface_name,
148                        event = if msg_type == RTM_NEWLINK { "added" } else { "removed" },
149                        "interface event"
150                    );
151                    if tx.send(event).await.is_err() {
152                        return; // receiver dropped
153                    }
154                }
155            }
156        }
157    }
158}
159
160#[cfg(target_os = "linux")]
161use std::os::unix::io::AsRawFd;
162
163/// Parse RTM_NEWLINK / RTM_DELLINK messages from a raw netlink buffer.
164///
165/// Returns `(msg_type, interface_name)` pairs for all IFLA_IFNAME attributes found.
166#[cfg(target_os = "linux")]
167fn parse_rtm_link_messages(buf: &[u8]) -> Vec<(u16, String)> {
168    // Netlink header is 16 bytes, ifinfomsg is 16 bytes.
169    // Attribute (rtattr) header is 4 bytes.
170    const NLMSG_HDR: usize = 16;
171    const IFINFO_HDR: usize = 16;
172    const RTA_HDR: usize = 4;
173    const IFLA_IFNAME: u16 = 3;
174    const RTM_NEWLINK: u16 = 16;
175    const RTM_DELLINK: u16 = 17;
176
177    let mut results = Vec::new();
178    let mut offset = 0usize;
179
180    while offset + NLMSG_HDR <= buf.len() {
181        // Parse nlmsghdr.
182        let nlmsg_len = u32::from_ne_bytes(buf[offset..offset + 4].try_into().unwrap()) as usize;
183        let nlmsg_type = u16::from_ne_bytes(buf[offset + 4..offset + 6].try_into().unwrap());
184
185        if nlmsg_len < NLMSG_HDR || offset + nlmsg_len > buf.len() {
186            break;
187        }
188
189        if nlmsg_type == RTM_NEWLINK || nlmsg_type == RTM_DELLINK {
190            // Skip nlmsghdr (16) + ifinfomsg (16) to reach attributes.
191            let attr_start = offset + NLMSG_HDR + IFINFO_HDR;
192            let attr_end = offset + nlmsg_len;
193            let mut attr_off = attr_start;
194
195            while attr_off + RTA_HDR <= attr_end {
196                let rta_len =
197                    u16::from_ne_bytes(buf[attr_off..attr_off + 2].try_into().unwrap()) as usize;
198                let rta_type =
199                    u16::from_ne_bytes(buf[attr_off + 2..attr_off + 4].try_into().unwrap());
200                if rta_len < RTA_HDR || attr_off + rta_len > attr_end {
201                    break;
202                }
203                if rta_type == IFLA_IFNAME {
204                    let data = &buf[attr_off + RTA_HDR..attr_off + rta_len];
205                    // IFLA_IFNAME is a NUL-terminated string.
206                    let name = data.split(|&b| b == 0).next().unwrap_or(data);
207                    if let Ok(s) = std::str::from_utf8(name) {
208                        results.push((nlmsg_type, s.to_owned()));
209                    }
210                }
211                // rtattr lengths are aligned to 4 bytes.
212                let aligned = (rta_len + 3) & !3;
213                attr_off += aligned;
214            }
215        }
216
217        // Advance to the next message (NLMSG_ALIGN to 4 bytes).
218        let aligned = (nlmsg_len + 3) & !3;
219        offset += aligned;
220    }
221
222    results
223}