Skip to main content

agave_xdp/
netlink.rs

1#![allow(clippy::arithmetic_side_effects)]
2
3use {
4    libc::{
5        AF_INET, AF_INET6, AF_NETLINK, IFLA_INFO_DATA, IFLA_INFO_KIND, IFLA_LINKINFO, MSG_DONTWAIT,
6        MSG_TRUNC, NDA_DST, NDA_LLADDR, NETLINK_EXT_ACK, NETLINK_GET_STRICT_CHK, NETLINK_ROUTE,
7        NLA_ALIGNTO, NLA_TYPE_MASK, NLM_F_DUMP, NLM_F_DUMP_INTR, NLM_F_MULTI, NLM_F_REQUEST,
8        NLMSG_DONE, NLMSG_ERROR, RTA_DST, RTA_GATEWAY, RTA_IIF, RTA_OIF, RTA_PREFSRC, RTA_PRIORITY,
9        RTA_TABLE, RTM_GETLINK, RTM_GETNEIGH, RTM_GETROUTE, RTM_NEWLINK, RTM_NEWNEIGH,
10        RTM_NEWROUTE, SO_RCVBUF, SOCK_RAW, SOL_NETLINK, SOL_SOCKET, nlattr, nlmsgerr, nlmsghdr,
11        recv, send, setsockopt, sockaddr_nl, socket,
12    },
13    std::{
14        collections::HashMap,
15        ffi::CStr,
16        io, mem,
17        net::{IpAddr, Ipv4Addr, Ipv6Addr},
18        os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
19        ptr, slice,
20    },
21    thiserror::Error,
22};
23
24const NETLINK_RCVBUF_SIZE: i32 = 1 << 16;
25const NLA_HDR_LEN: usize = align_to(mem::size_of::<nlattr>(), NLA_ALIGNTO as usize);
26
27// MTU of the device (from include/uapi/linux/if_link.h)
28const IFLA_MTU: u16 = 4;
29
30// GRE nested attributes (from include/uapi/linux/if_tunnel.h)
31const IFLA_GRE_LOCAL: u16 = 6;
32const IFLA_GRE_REMOTE: u16 = 7;
33const IFLA_GRE_TTL: u16 = 8;
34const IFLA_GRE_TOS: u16 = 9;
35const IFLA_GRE_PMTUDISC: u16 = 10;
36
37// VLAN nested attributes (from include/uapi/linux/if_link.h)
38const IFLA_VLAN_ID: u16 = 1;
39const IFLA_VLAN_PROTOCOL: u16 = 5;
40
41#[repr(C)]
42#[allow(non_camel_case_types)]
43struct ifinfomsg {
44    ifi_family: u8,
45    __ifi_pad: u8,
46    ifi_type: u16,
47    ifi_index: u32,
48    ifi_flags: u32,
49    ifi_change: u32,
50}
51
52pub struct NetlinkSocket {
53    sock: OwnedFd,
54    _nl_pid: u32,
55}
56
57impl NetlinkSocket {
58    fn open() -> Result<Self, io::Error> {
59        // Safety: libc wrapper
60        let sock = unsafe { socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE) };
61        if sock < 0 {
62            return Err(io::Error::last_os_error());
63        }
64        // SAFETY: `socket` returns a file descriptor.
65        let sock = unsafe { OwnedFd::from_raw_fd(sock) };
66
67        let enable = 1i32;
68        for opt in [NETLINK_EXT_ACK, NETLINK_GET_STRICT_CHK] {
69            // Safety: libc wrapper
70            if unsafe {
71                setsockopt(
72                    sock.as_raw_fd(),
73                    SOL_NETLINK,
74                    opt,
75                    &enable as *const _ as *const _,
76                    mem::size_of::<i32>() as u32,
77                )
78            } < 0
79            {
80                return Err(io::Error::last_os_error());
81            }
82        }
83        Ok(Self { sock, _nl_pid: 0 })
84    }
85
86    fn send(&self, msg: &[u8]) -> Result<(), io::Error> {
87        if unsafe {
88            send(
89                self.sock.as_raw_fd(),
90                msg.as_ptr() as *const _,
91                msg.len(),
92                0,
93            )
94        } < 0
95        {
96            return Err(io::Error::last_os_error());
97        }
98        Ok(())
99    }
100
101    pub(crate) fn recv(&self) -> Result<Vec<NetlinkMessage>, io::Error> {
102        self.recv_with_flags(0)
103    }
104
105    pub(crate) fn recv_nonblocking(&self) -> Result<Option<Vec<NetlinkMessage>>, io::Error> {
106        match self.recv_with_flags(MSG_DONTWAIT) {
107            Ok(messages) => Ok(Some(messages)),
108            Err(e)
109                if e.raw_os_error()
110                    .is_some_and(|errno| errno == libc::EAGAIN || errno == libc::EWOULDBLOCK) =>
111            {
112                Ok(None)
113            }
114            Err(e) => Err(e),
115        }
116    }
117
118    fn recv_with_flags(&self, flags: i32) -> Result<Vec<NetlinkMessage>, io::Error> {
119        // The kernel returns NLMSG_GOODSIZE (8k) as the recommended max allocation for netlink
120        // responses. However that is not a hard cap, and netlink code can in theory return larger
121        // messages. Out of caution we allocate a larger buffer AND use MSG_TRUNC to detect if that
122        // is still not enough.
123        let mut buf = [0u8; 8 * 1024]; // 8 KiB
124        let mut messages = Vec::new();
125        let mut multipart = true;
126        'out: while multipart {
127            multipart = false;
128            // Safety: libc wrapper
129            let len = unsafe {
130                recv(
131                    self.sock.as_raw_fd(),
132                    buf.as_mut_ptr() as *mut _,
133                    buf.len(),
134                    flags | MSG_TRUNC,
135                )
136            };
137            if len < 0 {
138                return Err(io::Error::last_os_error());
139            }
140            if len == 0 {
141                break;
142            }
143
144            let len = len as usize;
145            if len > buf.len() {
146                return Err(io::Error::other("netlink datagram truncated"));
147            }
148            let mut offset = 0;
149            while offset < len {
150                let message = NetlinkMessage::read(&buf[offset..])?;
151                offset += align_to(message.header.nlmsg_len as usize, NLMSG_ALIGNTO as usize);
152                multipart = message.header.nlmsg_flags & NLM_F_MULTI as u16 != 0;
153                if message.header.nlmsg_flags & NLM_F_DUMP_INTR as u16 != 0 {
154                    return Err(io::Error::new(
155                        io::ErrorKind::Interrupted,
156                        "netlink dump interrupted",
157                    ));
158                }
159                match message.header.nlmsg_type as i32 {
160                    NLMSG_ERROR => {
161                        let err = message.error.unwrap();
162                        if err.error == 0 {
163                            // this is an ACK
164                            continue;
165                        }
166                        return Err(io::Error::from_raw_os_error(-err.error));
167                    }
168                    NLMSG_DONE => break 'out,
169                    _ => messages.push(message),
170                }
171            }
172        }
173
174        Ok(messages)
175    }
176
177    /// Opens a listener socket for netlink updates
178    /// NETLINK_ROUTE socket subscribed to `groups` bitmask
179    pub fn bind(groups: u32) -> Result<Self, io::Error> {
180        let sock = Self::open()?;
181
182        // Subscribe to multicast groups
183        let mut addr: sockaddr_nl = unsafe { mem::zeroed() };
184        addr.nl_family = AF_NETLINK as u16;
185        addr.nl_groups = groups;
186        if unsafe {
187            libc::bind(
188                sock.as_raw_fd(),
189                &addr as *const _ as *const _,
190                mem::size_of::<sockaddr_nl>() as u32,
191            )
192        } < 0
193        {
194            return Err(io::Error::last_os_error());
195        }
196
197        unsafe {
198            setsockopt(
199                sock.as_raw_fd(),
200                SOL_SOCKET,
201                SO_RCVBUF,
202                &NETLINK_RCVBUF_SIZE as *const _ as *const _,
203                mem::size_of::<i32>() as u32,
204            );
205        }
206
207        Ok(sock)
208    }
209
210    #[inline]
211    pub fn as_raw_fd(&self) -> RawFd {
212        self.sock.as_raw_fd()
213    }
214}
215
216#[derive(Debug, Clone)]
217pub struct NetlinkMessage {
218    pub(crate) header: nlmsghdr,
219    data: Vec<u8>,
220    error: Option<nlmsgerr>,
221}
222
223impl NetlinkMessage {
224    fn read(buf: &[u8]) -> Result<Self, io::Error> {
225        if mem::size_of::<nlmsghdr>() > buf.len() {
226            return Err(io::Error::other("buffer smaller than nlmsghdr"));
227        }
228
229        // Safety: nlmsghdr is POD so read is safe
230        let header = unsafe { ptr::read_unaligned(buf.as_ptr() as *const nlmsghdr) };
231        let msg_len = header.nlmsg_len as usize;
232        if msg_len < mem::size_of::<nlmsghdr>() || msg_len > buf.len() {
233            return Err(io::Error::other("invalid nlmsg_len"));
234        }
235
236        let data_offset = align_to(mem::size_of::<nlmsghdr>(), NLMSG_ALIGNTO as usize);
237        if data_offset >= buf.len() {
238            return Err(io::Error::other("need more data"));
239        }
240
241        let (data, error) = if header.nlmsg_type == NLMSG_ERROR as u16 {
242            if data_offset + mem::size_of::<nlmsgerr>() > buf.len() {
243                return Err(io::Error::other(
244                    "NLMSG_ERROR but not enough space for nlmsgerr",
245                ));
246            }
247            (
248                Vec::new(),
249                // Safety: nlmsgerr is POD so read is safe
250                Some(unsafe {
251                    ptr::read_unaligned(buf[data_offset..].as_ptr() as *const nlmsgerr)
252                }),
253            )
254        } else {
255            (buf[data_offset..msg_len].to_vec(), None)
256        };
257
258        Ok(Self {
259            header,
260            data,
261            error,
262        })
263    }
264}
265
266const fn align_to(v: usize, align: usize) -> usize {
267    (v + (align - 1)) & !(align - 1)
268}
269
270struct NlAttrsIterator<'a> {
271    attrs: &'a [u8],
272    offset: usize,
273}
274
275impl<'a> NlAttrsIterator<'a> {
276    fn new(attrs: &'a [u8]) -> Self {
277        Self { attrs, offset: 0 }
278    }
279}
280
281impl<'a> Iterator for NlAttrsIterator<'a> {
282    type Item = Result<NlAttr<'a>, NlAttrError>;
283
284    fn next(&mut self) -> Option<Self::Item> {
285        let buf = &self.attrs[self.offset..];
286        if buf.is_empty() {
287            return None;
288        }
289
290        if NLA_HDR_LEN > buf.len() {
291            self.offset = buf.len();
292            return Some(Err(NlAttrError::InvalidBufferLength {
293                size: buf.len(),
294                expected: NLA_HDR_LEN,
295            }));
296        }
297
298        let attr = unsafe { ptr::read_unaligned(buf.as_ptr() as *const nlattr) };
299        let len = attr.nla_len as usize;
300        let align_len = align_to(len, NLA_ALIGNTO as usize);
301        if len < NLA_HDR_LEN {
302            return Some(Err(NlAttrError::InvalidHeaderLength(len)));
303        }
304        if align_len > buf.len() {
305            return Some(Err(NlAttrError::InvalidBufferLength {
306                size: buf.len(),
307                expected: align_len,
308            }));
309        }
310
311        let data = &buf[NLA_HDR_LEN..len];
312
313        self.offset += align_len;
314        Some(Ok(NlAttr { header: attr, data }))
315    }
316}
317
318fn parse_attrs(buf: &[u8]) -> Result<HashMap<u16, NlAttr<'_>>, NlAttrError> {
319    let mut attrs = HashMap::new();
320    for attr in NlAttrsIterator::new(buf) {
321        let attr = attr?;
322        attrs.insert(attr.header.nla_type & NLA_TYPE_MASK as u16, attr);
323    }
324    Ok(attrs)
325}
326
327#[derive(Clone)]
328struct NlAttr<'a> {
329    header: nlattr,
330    data: &'a [u8],
331}
332
333#[derive(Debug, Error, PartialEq, Eq)]
334enum NlAttrError {
335    #[error("invalid buffer size `{size}`, expected `{expected}`")]
336    InvalidBufferLength { size: usize, expected: usize },
337
338    #[error("invalid nlattr header length `{0}`")]
339    InvalidHeaderLength(usize),
340}
341
342impl From<NlAttrError> for io::Error {
343    fn from(e: NlAttrError) -> Self {
344        Self::other(e)
345    }
346}
347
348fn bytes_of<T>(val: &T) -> &[u8] {
349    let size = mem::size_of::<T>();
350    unsafe { slice::from_raw_parts(slice::from_ref(val).as_ptr().cast(), size) }
351}
352
353const NLMSG_ALIGNTO: u32 = 4;
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356pub struct MacAddress(pub [u8; 6]);
357
358impl MacAddress {
359    pub fn new(bytes: [u8; 6]) -> Self {
360        MacAddress(bytes)
361    }
362
363    pub fn as_bytes(&self) -> &[u8; 6] {
364        &self.0
365    }
366}
367
368impl std::fmt::Display for MacAddress {
369    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370        write!(
371            f,
372            "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
373            self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
374        )
375    }
376}
377
378/// GRE tunnel information from netlink
379///
380/// Note: Only supports basic GRE header (no optional fields).
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub struct GreTunnelInfo {
383    /// Source IP address for the GRE tunnel header
384    pub local: IpAddr,
385    /// Destination IP address for the GRE tunnel header
386    pub remote: IpAddr,
387    pub ttl: u8,
388    pub tos: u8,
389    /// PMTU discovery setting (IFLA_GRE_PMTUDISC)
390    pub pmtudisc: u8,
391}
392
393/// 802.1Q VLAN sub-interface information from netlink (IFLA_LINKINFO kind "vlan").
394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
395pub struct VlanLinkInfo {
396    /// VLAN ID (IFLA_VLAN_ID), 1-4094 in practice.
397    pub vid: u16,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq)]
401pub struct InterfaceInfo {
402    pub if_index: u32,
403    pub mtu: u32,
404    pub gre_tunnel: Option<GreTunnelInfo>,
405    pub vlan_link: Option<VlanLinkInfo>,
406}
407
408impl InterfaceInfo {
409    pub fn is_gre(&self) -> bool {
410        self.gre_tunnel.is_some()
411    }
412}
413
414#[repr(C)]
415struct InterfaceRequest {
416    header: nlmsghdr,
417    ifi: ifinfomsg,
418}
419
420pub fn netlink_get_interfaces(family: u8) -> Result<Vec<InterfaceInfo>, io::Error> {
421    let sock = NetlinkSocket::open()?;
422
423    // Safety: ifinfomsg is POD
424    let mut req = unsafe { mem::zeroed::<InterfaceRequest>() };
425
426    let nlmsg_len = mem::size_of::<nlmsghdr>() + mem::size_of::<ifinfomsg>();
427    req.header = nlmsghdr {
428        nlmsg_len: nlmsg_len as u32,
429        nlmsg_flags: (NLM_F_REQUEST | NLM_F_DUMP) as u16,
430        nlmsg_type: RTM_GETLINK,
431        nlmsg_pid: 0,
432        nlmsg_seq: 1,
433    };
434
435    req.ifi.ifi_family = family;
436    sock.send(&bytes_of(&req)[..req.header.nlmsg_len as usize])?;
437
438    let mut interfaces = Vec::new();
439    for msg in sock.recv()? {
440        if msg.header.nlmsg_type != RTM_NEWLINK {
441            continue;
442        }
443
444        if let Some(if_info) = parse_rtm_ifinfomsg(&msg) {
445            interfaces.push(if_info);
446        }
447    }
448
449    Ok(interfaces)
450}
451
452pub(crate) fn parse_rtm_ifinfomsg(msg: &NetlinkMessage) -> Option<InterfaceInfo> {
453    if msg.data.len() < mem::size_of::<ifinfomsg>() {
454        return None;
455    }
456
457    let ifi = unsafe { ptr::read_unaligned(msg.data.as_ptr() as *const ifinfomsg) };
458    let Ok(attrs) = parse_attrs(&msg.data[mem::size_of::<ifinfomsg>()..]) else {
459        return None;
460    };
461
462    let mtu = attrs
463        .get(&IFLA_MTU)
464        .and_then(|a| u32_from_ne_bytes(a.data))?;
465
466    // Parse GRE tunnel information if this is a GRE interface
467    let gre_tunnel = parse_gre_tunnel_info_from_linkinfo(&attrs);
468    // Parse VLAN information if this is an 802.1Q VLAN sub-interface
469    let vlan_link = parse_vlan_link_info_from_linkinfo(&attrs);
470    Some(InterfaceInfo {
471        if_index: ifi.ifi_index,
472        mtu,
473        gre_tunnel,
474        vlan_link,
475    })
476}
477
478// Parse 802.1Q VLAN information from netlink
479fn parse_vlan_link_info_from_linkinfo(attrs: &HashMap<u16, NlAttr>) -> Option<VlanLinkInfo> {
480    let vlan = parse_linkinfo_data_for_kind(attrs, b"vlan")?;
481
482    // Only 802.1Q is supported; skip 802.1ad (QinQ) sub-interfaces. The protocol attribute is a
483    // big-endian u16; kernels predating 802.1ad support omit it, which implies 802.1Q.
484    if let Some(proto) = vlan.get(&IFLA_VLAN_PROTOCOL) {
485        let proto = u16::from_be_bytes(proto.data.get(..2)?.try_into().ok()?);
486        if proto != libc::ETH_P_8021Q as u16 {
487            return None;
488        }
489    }
490
491    let vid = vlan
492        .get(&IFLA_VLAN_ID)
493        .and_then(|a| u16_from_ne_bytes(a.data))?;
494    Some(VlanLinkInfo { vid })
495}
496
497// Parse GRE tunnel information from netlink
498fn parse_gre_tunnel_info_from_linkinfo(attrs: &HashMap<u16, NlAttr>) -> Option<GreTunnelInfo> {
499    let gre = parse_linkinfo_data_for_kind(attrs, b"gre")?;
500
501    let u8_from_bytes = |data: &[u8]| -> Option<u8> { data.first().copied() };
502
503    let local = gre
504        .get(&IFLA_GRE_LOCAL)
505        .and_then(|a| parse_ip_address(a.data, AF_INET as u8))?;
506    let remote = gre
507        .get(&IFLA_GRE_REMOTE)
508        .and_then(|a| parse_ip_address(a.data, AF_INET as u8))?;
509    let ttl = gre.get(&IFLA_GRE_TTL).and_then(|a| u8_from_bytes(a.data))?;
510    let tos = gre.get(&IFLA_GRE_TOS).and_then(|a| u8_from_bytes(a.data))?;
511    let pmtudisc = gre
512        .get(&IFLA_GRE_PMTUDISC)
513        .and_then(|a| u8_from_bytes(a.data))?;
514
515    Some(GreTunnelInfo {
516        local,
517        remote,
518        ttl,
519        tos,
520        pmtudisc,
521    })
522}
523
524fn parse_linkinfo_data_for_kind<'a>(
525    attrs: &HashMap<u16, NlAttr<'a>>,
526    expected_kind: &[u8],
527) -> Option<HashMap<u16, NlAttr<'a>>> {
528    let li = attrs.get(&IFLA_LINKINFO)?;
529    // IFLA_LINKINFO contains nested attributes
530    let info = parse_attrs(li.data).ok()?;
531    let kind_attr = info.get(&IFLA_INFO_KIND)?;
532    if kind_attr.data.is_empty() {
533        return None;
534    }
535    let kind = CStr::from_bytes_until_nul(kind_attr.data).ok()?;
536    if kind.to_bytes() != expected_kind {
537        return None;
538    }
539    // Nested data (GRE attributes) is optional.
540    let data_attr = info.get(&IFLA_INFO_DATA)?;
541    parse_attrs(data_attr.data).ok()
542}
543
544/// Represents an entry in the neighbor table (ARP/NDP cache)
545#[derive(Debug, Clone, Eq, PartialEq)]
546pub struct NeighborEntry {
547    // IPv4 or IPv6 address
548    pub destination: Option<IpAddr>,
549    // MAC address
550    pub lladdr: Option<MacAddress>,
551    // Interface index
552    pub ifindex: i32,
553    // NUD_* state
554    pub state: u16,
555}
556
557impl NeighborEntry {
558    #[inline]
559    pub fn key(&self) -> Option<(i32, Ipv4Addr)> {
560        match self.destination {
561            Some(IpAddr::V4(ip)) => Some((self.ifindex, ip)),
562            _ => None,
563        }
564    }
565}
566
567#[repr(C)]
568#[allow(non_camel_case_types)]
569struct ndmsg {
570    ndm_family: u8,
571    _ndm_pad1: u8,
572    _ndm_pad2: u16,
573    ndm_ifindex: i32,
574    ndm_state: u16,
575    _ndm_flags: u8,
576    _ndm_type: u8,
577}
578
579#[repr(C)]
580struct NeighRequest {
581    header: nlmsghdr,
582    ndm: ndmsg,
583}
584
585/// fetch the kernel's neighbor table (ARP/NDP cache)
586pub fn netlink_get_neighbors(
587    if_index: Option<u32>,
588    family: u8,
589) -> Result<Vec<NeighborEntry>, io::Error> {
590    let sock = NetlinkSocket::open()?;
591
592    // Safety: NeighRequest is POD
593    let mut req = unsafe { mem::zeroed::<NeighRequest>() };
594
595    let nlmsg_len = mem::size_of::<nlmsghdr>() + mem::size_of::<ndmsg>();
596    req.header = nlmsghdr {
597        nlmsg_len: nlmsg_len as u32,
598        nlmsg_flags: (NLM_F_REQUEST | NLM_F_DUMP) as u16,
599        nlmsg_type: RTM_GETNEIGH,
600        nlmsg_pid: 0,
601        nlmsg_seq: 1,
602    };
603
604    req.ndm.ndm_family = family;
605    if let Some(idx) = if_index {
606        req.ndm.ndm_ifindex = idx as i32;
607    }
608
609    sock.send(&bytes_of(&req)[..req.header.nlmsg_len as usize])?;
610
611    let mut neighbors = Vec::new();
612
613    for msg in sock.recv()? {
614        if msg.header.nlmsg_type != RTM_NEWNEIGH {
615            continue;
616        }
617
618        if msg.data.len() < mem::size_of::<ndmsg>() {
619            continue;
620        }
621
622        if let Some(neighbor) = parse_rtm_newneigh(&msg, if_index) {
623            neighbors.push(neighbor);
624        }
625    }
626
627    Ok(neighbors)
628}
629
630pub fn parse_rtm_newneigh(msg: &NetlinkMessage, if_index: Option<u32>) -> Option<NeighborEntry> {
631    if msg.data.len() < mem::size_of::<ndmsg>() {
632        return None;
633    }
634    let nd_msg = unsafe { ptr::read_unaligned(msg.data.as_ptr() as *const ndmsg) };
635    if let Some(idx) = if_index
636        && nd_msg.ndm_ifindex != idx as i32
637    {
638        return None;
639    }
640    let Ok(attrs) = parse_attrs(&msg.data[mem::size_of::<ndmsg>()..]) else {
641        return None;
642    };
643    let mut neighbor = NeighborEntry {
644        destination: None,
645        lladdr: None,
646        ifindex: nd_msg.ndm_ifindex,
647        state: nd_msg.ndm_state,
648    };
649    if let Some(dst_attr) = attrs.get(&NDA_DST) {
650        neighbor.destination = parse_ip_address(dst_attr.data, nd_msg.ndm_family);
651    }
652    if let Some(lladdr_attr) = attrs.get(&NDA_LLADDR)
653        && lladdr_attr.data.len() >= 6
654    {
655        let mut mac = [0u8; 6];
656        mac.copy_from_slice(&lladdr_attr.data[0..6]);
657        neighbor.lladdr = Some(MacAddress(mac));
658    }
659    Some(neighbor)
660}
661
662#[derive(Clone)]
663pub struct RouteEntry {
664    pub destination: Option<IpAddr>,
665    pub gateway: Option<IpAddr>,
666    pub pref_src: Option<IpAddr>,
667    pub out_if_index: Option<i32>,
668    pub in_if_index: Option<i32>,
669    pub priority: Option<u32>,
670    pub table: Option<u32>,
671    pub protocol: u8,
672    pub scope: u8,
673    pub type_: u8,
674    pub family: u8,
675    pub dst_len: u8,
676    pub flags: u32,
677}
678
679#[repr(C)]
680#[allow(non_camel_case_types)]
681struct rtmsg {
682    rtm_family: u8,
683    rtm_dst_len: u8,
684    rtm_src_len: u8,
685    rtm_tos: u8,
686    rtm_table: u8,
687    rtm_protocol: u8,
688    rtm_scope: u8,
689    rtm_type: u8,
690    rtm_flags: u32,
691}
692
693#[repr(C)]
694struct RouteRequest {
695    header: nlmsghdr,
696    rtm: rtmsg,
697}
698
699fn parse_ip_address(data: &[u8], family: u8) -> Option<IpAddr> {
700    match family as i32 {
701        AF_INET if data.len() == 4 => Some(IpAddr::V4(Ipv4Addr::new(
702            data[0], data[1], data[2], data[3],
703        ))),
704        AF_INET6 if data.len() == 16 => {
705            let mut segments = [0u16; 8];
706            for i in 0..8 {
707                segments[i] = ((data[i * 2] as u16) << 8) | (data[i * 2 + 1] as u16);
708            }
709            Some(IpAddr::V6(Ipv6Addr::from(segments)))
710        }
711        _ => None,
712    }
713}
714
715pub fn netlink_get_routes(family: u8, table: u32) -> Result<Vec<RouteEntry>, io::Error> {
716    let sock = NetlinkSocket::open()?;
717
718    // Safety: RouteRequest is POD
719    let mut req = unsafe { mem::zeroed::<RouteRequest>() };
720
721    let nlmsg_len = mem::size_of::<nlmsghdr>() + mem::size_of::<rtmsg>();
722    let table_attr_len = align_to(NLA_HDR_LEN + mem::size_of::<u32>(), NLA_ALIGNTO as usize);
723    req.header = nlmsghdr {
724        nlmsg_len: (nlmsg_len + table_attr_len) as u32,
725        nlmsg_flags: (NLM_F_REQUEST | NLM_F_DUMP) as u16,
726        nlmsg_type: RTM_GETROUTE,
727        nlmsg_pid: 0,
728        nlmsg_seq: 1,
729    };
730
731    req.rtm.rtm_family = family;
732
733    let mut req_buf = bytes_of(&req)[..nlmsg_len].to_vec();
734    push_nlattr(&mut req_buf, RTA_TABLE, &table);
735    sock.send(&req_buf)?;
736
737    let mut routes = Vec::new();
738
739    for msg in sock.recv()? {
740        if msg.header.nlmsg_type != RTM_NEWROUTE {
741            continue;
742        }
743        if let Some(route) = parse_rtm_newroute(&msg) {
744            // with strict checking, the kernel should return routes for the requested table only
745            debug_assert!(route.table == Some(table));
746            if route.table == Some(table) {
747                routes.push(route);
748            }
749        }
750    }
751
752    Ok(routes)
753}
754
755pub fn parse_rtm_newroute(msg: &NetlinkMessage) -> Option<RouteEntry> {
756    if msg.data.len() < mem::size_of::<rtmsg>() {
757        return None;
758    }
759
760    let rt_msg = unsafe { ptr::read_unaligned(msg.data.as_ptr() as *const rtmsg) };
761    let Ok(attrs) = parse_attrs(&msg.data[mem::size_of::<rtmsg>()..]) else {
762        return None;
763    };
764    let mut route = RouteEntry {
765        destination: None,
766        gateway: None,
767        pref_src: None,
768        out_if_index: None,
769        in_if_index: None,
770        priority: None,
771        table: Some(u32::from(rt_msg.rtm_table)),
772        protocol: rt_msg.rtm_protocol,
773        scope: rt_msg.rtm_scope,
774        type_: rt_msg.rtm_type,
775        family: rt_msg.rtm_family,
776        dst_len: rt_msg.rtm_dst_len,
777        flags: rt_msg.rtm_flags,
778    };
779    if let Some(dst_attr) = attrs.get(&RTA_DST) {
780        route.destination = parse_ip_address(dst_attr.data, rt_msg.rtm_family);
781    }
782    if let Some(gateway_attr) = attrs.get(&RTA_GATEWAY) {
783        route.gateway = parse_ip_address(gateway_attr.data, rt_msg.rtm_family);
784    }
785
786    if let Some(oif_attr) = attrs.get(&RTA_OIF) {
787        route.out_if_index = u32_from_ne_bytes(oif_attr.data).map(|i| i as i32);
788    }
789    if let Some(iif_attr) = attrs.get(&RTA_IIF) {
790        route.in_if_index = u32_from_ne_bytes(iif_attr.data).map(|i| i as i32);
791    }
792    if let Some(priority_attr) = attrs.get(&RTA_PRIORITY) {
793        route.priority = u32_from_ne_bytes(priority_attr.data);
794    }
795    if let Some(table_attr) = attrs.get(&RTA_TABLE) {
796        route.table = u32_from_ne_bytes(table_attr.data);
797    }
798    if let Some(prefsrc_attr) = attrs.get(&RTA_PREFSRC) {
799        route.pref_src = parse_ip_address(prefsrc_attr.data, rt_msg.rtm_family);
800    }
801    Some(route)
802}
803
804fn push_nlattr<T>(buf: &mut Vec<u8>, attr_type: u16, value: &T) {
805    let attr_len = NLA_HDR_LEN + mem::size_of::<T>();
806    let aligned_len = align_to(attr_len, NLA_ALIGNTO as usize);
807    let attr = nlattr {
808        nla_len: attr_len as u16,
809        nla_type: attr_type,
810    };
811    buf.extend_from_slice(bytes_of(&attr));
812    buf.extend_from_slice(bytes_of(value));
813    buf.resize(buf.len() + (aligned_len - attr_len), 0);
814}
815
816fn u32_from_ne_bytes(data: &[u8]) -> Option<u32> {
817    let bytes: [u8; 4] = data.get(..4)?.try_into().ok()?;
818    Some(u32::from_ne_bytes(bytes))
819}
820
821fn u16_from_ne_bytes(data: &[u8]) -> Option<u16> {
822    let bytes: [u8; 2] = data.get(..2)?.try_into().ok()?;
823    Some(u16::from_ne_bytes(bytes))
824}