Skip to main content

agave_xdp/
route.rs

1use {
2    crate::{
3        lpm::Ipv4Lpm,
4        netlink::{
5            GreTunnelInfo, InterfaceInfo, MacAddress, NeighborEntry, RouteEntry,
6            netlink_get_interfaces, netlink_get_neighbors, netlink_get_routes,
7        },
8    },
9    libc::{AF_INET, RT_TABLE_DEFAULT, RT_TABLE_LOCAL, RT_TABLE_MAIN},
10    log::warn,
11    std::{
12        cmp::Ordering,
13        fmt, io,
14        net::{IpAddr, Ipv4Addr},
15    },
16    thiserror::Error,
17};
18
19#[derive(Debug, Error)]
20pub enum RouteError {
21    #[error("no route found to destination {0}")]
22    NoRouteFound(IpAddr),
23
24    #[error("missing output interface in route")]
25    MissingOutputInterface,
26
27    #[error("could not resolve MAC address")]
28    MacResolutionError,
29
30    #[error("unknown interface index {0}")]
31    UnknownInterfaceIndex(u32),
32}
33
34#[derive(Debug, Clone)]
35pub struct GreRouteInfo {
36    pub if_index: u32,
37    pub mtu: u32,
38    pub underlay_mtu: u32,
39    pub tunnel_info: GreTunnelInfo,
40    pub mac_addr: MacAddress,
41}
42
43/// VLAN encapsulation directive carried on a resolved NextHop.
44///
45/// When present, the tx loop prepends an 802.1Q tag to the Ethernet header before the inner IP
46/// header. The MAC and IP fields on the NextHop are still the real packet destinations: VLAN
47/// encapsulation changes the L2 framing only. XDP cannot transmit on a VLAN sub-interface
48/// directly (it is bound to the physical parent), so the tag has to be written in userspace.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct VlanRouteInfo {
51    /// if_index of the VLAN sub-interface the route resolved to.
52    pub if_index: u32,
53    pub vid: u16,
54    /// Priority Code Point. Always 0 for now, matching the kernel's default egress QoS mapping;
55    /// a configurable mapping can be added later if needed.
56    pub pcp: u8,
57}
58
59#[derive(Debug, Clone)]
60pub struct NextHop {
61    pub mac_addr: Option<MacAddress>,
62    pub ip_addr: IpAddr,
63    pub if_index: u32,
64    pub mtu: u32,
65    pub preferred_src_ip: Option<Ipv4Addr>,
66    pub gre: Option<GreRouteInfo>,
67    pub vlan: Option<VlanRouteInfo>,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum RouteTable {
72    Default,
73    Local,
74    Main,
75    Other(u32),
76}
77
78impl std::fmt::Display for RouteTable {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            Self::Default => f.write_str("default"),
82            Self::Local => f.write_str("local"),
83            Self::Main => f.write_str("main"),
84            Self::Other(table) => write!(f, "{table}"),
85        }
86    }
87}
88
89impl From<RouteTable> for u32 {
90    fn from(table: RouteTable) -> Self {
91        match table {
92            RouteTable::Default => u32::from(RT_TABLE_DEFAULT),
93            RouteTable::Local => u32::from(RT_TABLE_LOCAL),
94            RouteTable::Main => u32::from(RT_TABLE_MAIN),
95            RouteTable::Other(table) => table,
96        }
97    }
98}
99
100impl From<u32> for RouteTable {
101    fn from(table: u32) -> Self {
102        match table {
103            table if table == u32::from(RT_TABLE_DEFAULT) => Self::Default,
104            table if table == u32::from(RT_TABLE_LOCAL) => Self::Local,
105            table if table == u32::from(RT_TABLE_MAIN) => Self::Main,
106            table => Self::Other(table),
107        }
108    }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct Route<T> {
113    pub destination: Option<T>,
114    pub gateway: Option<T>,
115    pub preferred_src: Option<T>,
116    pub out_if_index: Option<u32>,
117    pub priority: Option<u32>,
118    pub type_: u8,
119    pub dst_len: u8,
120}
121
122impl<T: PartialEq> Route<T> {
123    fn same_key(&self, other: &Self) -> bool {
124        self.destination == other.destination
125            && self.dst_len == other.dst_len
126            && self.priority == other.priority
127            && self.type_ == other.type_
128    }
129}
130
131impl TryFrom<RouteEntry> for Route<Ipv4Addr> {
132    type Error = ();
133
134    fn try_from(entry: RouteEntry) -> Result<Self, Self::Error> {
135        if entry.family != AF_INET as u8 {
136            return Err(());
137        }
138
139        let destination = match entry.destination {
140            Some(IpAddr::V4(addr)) => Some(addr),
141            Some(IpAddr::V6(_)) => return Err(()),
142            None => None,
143        };
144        let gateway = match entry.gateway {
145            Some(IpAddr::V4(addr)) => Some(addr),
146            Some(IpAddr::V6(_)) => return Err(()),
147            None => None,
148        };
149        let preferred_src = match entry.pref_src {
150            Some(IpAddr::V4(addr)) => Some(addr),
151            Some(IpAddr::V6(_)) => return Err(()),
152            None => None,
153        };
154        let out_if_index = entry
155            .out_if_index
156            .map(u32::try_from)
157            .transpose()
158            .map_err(|_| ())?;
159
160        Ok(Self {
161            destination,
162            gateway,
163            preferred_src,
164            out_if_index,
165            priority: entry.priority,
166            type_: entry.type_,
167            dst_len: entry.dst_len.min(32),
168        })
169    }
170}
171
172#[derive(Clone, Debug)]
173pub struct Interfaces {
174    interfaces: Vec<InterfaceInfo>,
175}
176
177impl Interfaces {
178    pub fn new(interfaces: Vec<InterfaceInfo>) -> Self {
179        Self { interfaces }
180    }
181
182    pub fn from_netlink() -> Result<Self, io::Error> {
183        Ok(Self::new(netlink_get_interfaces(AF_INET as u8)?))
184    }
185
186    pub fn iter(&self) -> impl ExactSizeIterator<Item = &InterfaceInfo> {
187        self.interfaces.iter()
188    }
189
190    fn upsert(&mut self, new_interface: InterfaceInfo) -> bool {
191        if let Some(existing) = self
192            .interfaces
193            .iter_mut()
194            .find(|old| old.if_index == new_interface.if_index)
195        {
196            if existing != &new_interface {
197                *existing = new_interface;
198                return true;
199            }
200            return false;
201        }
202        self.interfaces.push(new_interface);
203        true
204    }
205
206    fn remove(&mut self, if_index: u32) -> bool {
207        if let Some(i) = self
208            .interfaces
209            .iter()
210            .position(|old| old.if_index == if_index)
211        {
212            self.interfaces.swap_remove(i);
213            return true;
214        }
215        false
216    }
217}
218
219#[derive(Clone)]
220pub struct Neighbors {
221    neighbors: Vec<NeighborEntry>,
222}
223
224impl Neighbors {
225    pub fn new(neighbors: Vec<NeighborEntry>) -> Self {
226        Self { neighbors }
227    }
228
229    pub fn from_netlink() -> Result<Self, io::Error> {
230        Ok(Self::new(netlink_get_neighbors(None, AF_INET as u8)?))
231    }
232
233    pub fn iter(&self) -> impl ExactSizeIterator<Item = &NeighborEntry> {
234        self.neighbors.iter()
235    }
236
237    fn lookup(&self, ip: IpAddr, if_index: u32) -> Option<&MacAddress> {
238        self.neighbors
239            .iter()
240            .find(|n| n.ifindex == if_index as i32 && n.destination == Some(ip))
241            .and_then(|n| n.lladdr.as_ref())
242    }
243
244    fn upsert(&mut self, new_neighbor: NeighborEntry) -> bool {
245        let Some((ifidx, ip)) = new_neighbor.key() else {
246            return false;
247        };
248
249        if let Some(i) = self
250            .neighbors
251            .iter()
252            .position(|old| old.ifindex == ifidx && old.destination == Some(IpAddr::V4(ip)))
253        {
254            if self.neighbors[i] != new_neighbor {
255                self.neighbors[i] = new_neighbor;
256                return true;
257            }
258            false
259        } else {
260            self.neighbors.push(new_neighbor);
261            true
262        }
263    }
264
265    fn remove(&mut self, ip: Ipv4Addr, if_index: u32) -> bool {
266        if let Some(i) = self.neighbors.iter().position(|old| {
267            old.ifindex == if_index as i32 && old.destination == Some(IpAddr::V4(ip))
268        }) {
269            self.neighbors.swap_remove(i);
270            return true;
271        }
272        false
273    }
274}
275
276#[derive(Clone)]
277pub struct Routes {
278    pub(crate) table: RouteTable,
279    routes: Vec<Route<Ipv4Addr>>,
280}
281
282impl Routes {
283    pub fn new(table: RouteTable, mut routes: Vec<Route<Ipv4Addr>>) -> Self {
284        routes.sort_by(Self::compare_routes);
285        Self { table, routes }
286    }
287
288    pub fn from_netlink(table: RouteTable) -> Result<Self, io::Error> {
289        let routes = netlink_get_routes(AF_INET as u8, u32::from(table))?
290            .into_iter()
291            .filter_map(|entry| Route::try_from(entry).ok())
292            .collect();
293        Ok(Self::new(table, routes))
294    }
295
296    fn compare_routes(left: &Route<Ipv4Addr>, right: &Route<Ipv4Addr>) -> Ordering {
297        right
298            .dst_len
299            .cmp(&left.dst_len)
300            .then_with(|| left.priority.unwrap_or(0).cmp(&right.priority.unwrap_or(0)))
301    }
302
303    pub fn iter(&self) -> impl ExactSizeIterator<Item = &Route<Ipv4Addr>> {
304        self.routes.iter()
305    }
306
307    fn get(&self, route_idx: usize) -> Option<&Route<Ipv4Addr>> {
308        self.routes.get(route_idx)
309    }
310
311    fn as_slice(&self) -> &[Route<Ipv4Addr>] {
312        &self.routes
313    }
314
315    fn upsert(&mut self, new_route: RouteEntry) -> bool {
316        if !new_route
317            .table
318            .is_some_and(|table| self.table == RouteTable::from(table))
319        {
320            return false;
321        }
322
323        let Ok(new_route) = Route::try_from(new_route) else {
324            return false;
325        };
326
327        if let Some(existing) = self.routes.iter_mut().find(|old| old.same_key(&new_route)) {
328            if existing != &new_route {
329                *existing = new_route;
330                return true;
331            }
332            false
333        } else {
334            let route = new_route;
335            let insert_at = self.routes.partition_point(|existing| {
336                matches!(
337                    Self::compare_routes(existing, &route),
338                    Ordering::Less | Ordering::Equal
339                )
340            });
341            self.routes.insert(insert_at, route);
342            true
343        }
344    }
345
346    fn remove(&mut self, new_route: RouteEntry) -> bool {
347        if !new_route
348            .table
349            .is_some_and(|table| self.table == RouteTable::from(table))
350        {
351            return false;
352        }
353
354        let Ok(new_route) = Route::try_from(new_route) else {
355            return false;
356        };
357
358        if let Some(i) = self.routes.iter().position(|old| old.same_key(&new_route)) {
359            self.routes.remove(i);
360            true
361        } else {
362            false
363        }
364    }
365}
366
367#[derive(Clone)]
368pub struct RoutingTables {
369    pub(crate) routes: Routes,
370    neighbors: Neighbors,
371    interfaces: Interfaces,
372}
373
374impl RoutingTables {
375    pub fn new(routes: Routes, neighbors: Neighbors, interfaces: Interfaces) -> Self {
376        Self {
377            routes,
378            neighbors,
379            interfaces,
380        }
381    }
382
383    pub fn from_netlink(table: RouteTable) -> Result<Self, io::Error> {
384        Ok(Self::new(
385            Routes::from_netlink(table)?,
386            Neighbors::from_netlink()?,
387            Interfaces::from_netlink()?,
388        ))
389    }
390
391    pub fn upsert_route(&mut self, new_route: RouteEntry) -> bool {
392        self.routes.upsert(new_route)
393    }
394
395    pub fn remove_route(&mut self, new_route: RouteEntry) -> bool {
396        self.routes.remove(new_route)
397    }
398
399    pub fn upsert_neighbor(&mut self, new_neighbor: NeighborEntry) -> bool {
400        self.neighbors.upsert(new_neighbor)
401    }
402
403    pub fn remove_neighbor(&mut self, ip: Ipv4Addr, if_index: u32) -> bool {
404        self.neighbors.remove(ip, if_index)
405    }
406
407    pub fn upsert_interface(&mut self, new_interface: InterfaceInfo) -> bool {
408        self.interfaces.upsert(new_interface)
409    }
410
411    pub fn remove_interface(&mut self, if_index: u32) -> bool {
412        self.interfaces.remove(if_index)
413    }
414}
415
416#[derive(Clone)]
417pub struct Router {
418    neighbors: Neighbors,
419    routes: Routes,
420    ipv4_lpm: Ipv4Lpm,
421    interfaces: Interfaces,
422    // cache for the default route next hop so we can avoid arp table lookups on the common case
423    cached_default_route: Option<NextHop>,
424    // cache for gre route info keyed by interface index. This stays tiny in practice.
425    cached_gre_info: Vec<GreRouteInfo>,
426    // cache for vlan encap info keyed by interface index. One entry per VLAN sub-interface.
427    cached_vlan_info: Vec<VlanRouteInfo>,
428}
429
430struct RoutingTableDisplay<'a>(&'a [Route<Ipv4Addr>]);
431
432impl fmt::Display for RoutingTableDisplay<'_> {
433    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434        if self.0.is_empty() {
435            return f.write_str("<empty>");
436        }
437
438        for (i, route) in self.0.iter().enumerate() {
439            if i > 0 {
440                f.write_str("\n")?;
441            }
442            format_route(f, route)?;
443        }
444
445        Ok(())
446    }
447}
448
449impl Router {
450    pub fn new() -> Result<Self, io::Error> {
451        Self::from_tables(RoutingTables::from_netlink(RouteTable::Main)?)
452    }
453
454    pub fn from_tables(tables: RoutingTables) -> Result<Self, io::Error> {
455        let ipv4_lpm = Ipv4Lpm::build(tables.routes.as_slice());
456        let mut router = Self {
457            neighbors: tables.neighbors,
458            routes: tables.routes,
459            ipv4_lpm,
460            interfaces: tables.interfaces,
461            cached_default_route: None,
462            cached_gre_info: Vec::new(),
463            cached_vlan_info: Vec::new(),
464        };
465
466        let mut has_gre_interface = false;
467        for interface in router.interfaces.iter() {
468            if interface.gre_tunnel.is_some() {
469                has_gre_interface = true;
470                if let Some(gre) = router.interface_gre_route_info(interface) {
471                    router.cached_gre_info.push(gre);
472                }
473            }
474            if let Some(vlan) = interface.vlan_link {
475                router.cached_vlan_info.push(VlanRouteInfo {
476                    if_index: interface.if_index,
477                    vid: vlan.vid,
478                    // Kernel default egress QoS mapping; a configurable mapping can come later.
479                    pcp: 0,
480                });
481            }
482        }
483        if router.cached_gre_info.is_empty() && has_gre_interface {
484            warn!("GRE cache: GRE interface(s) present but none with valid remote resolved");
485        }
486
487        router.cached_default_route = match router.default_route() {
488            Ok(hop) => Some(hop),
489            Err(RouteError::NoRouteFound(_)) => None,
490            Err(e) => return Err(io::Error::other(e)),
491        };
492
493        Ok(router)
494    }
495
496    pub fn routing_table(&self) -> impl fmt::Display + '_ {
497        RoutingTableDisplay(self.routes.as_slice())
498    }
499
500    fn cached_gre_route_info(&self, if_index: u32) -> Option<&GreRouteInfo> {
501        self.cached_gre_info
502            .iter()
503            .find(|gre| gre.if_index == if_index)
504    }
505
506    fn gre_route_info(&self, if_index: u32) -> Option<GreRouteInfo> {
507        if let Some(gre) = self.cached_gre_route_info(if_index) {
508            return Some(gre.clone());
509        }
510
511        let interface = self
512            .interfaces
513            .iter()
514            .find(|interface| interface.if_index == if_index)?;
515        self.interface_gre_route_info(interface)
516    }
517
518    fn cached_vlan_route_info(&self, if_index: u32) -> Option<VlanRouteInfo> {
519        self.cached_vlan_info
520            .iter()
521            .find(|vlan| vlan.if_index == if_index)
522            .copied()
523    }
524
525    fn resolve_next_hop(
526        &self,
527        route_ip: Ipv4Addr,
528        route: &Route<Ipv4Addr>,
529    ) -> Result<NextHop, RouteError> {
530        let if_index = route
531            .out_if_index
532            .ok_or(RouteError::MissingOutputInterface)?;
533        let next_hop_v4 = route.gateway.unwrap_or(route_ip);
534        let next_hop_ip = IpAddr::V4(next_hop_v4);
535        let preferred_src_ip = route.preferred_src;
536
537        if let Some(default_route) = &self.cached_default_route
538            && default_route.ip_addr == next_hop_ip
539            && default_route.if_index == if_index
540        {
541            return Ok(NextHop {
542                ip_addr: next_hop_ip,
543                if_index,
544                mtu: default_route.mtu,
545                mac_addr: default_route.mac_addr,
546                preferred_src_ip,
547                gre: default_route.gre.clone(),
548                vlan: default_route.vlan,
549            });
550        }
551
552        if let Some(gre) = self.gre_route_info(if_index) {
553            return Ok(NextHop {
554                if_index,
555                mtu: gre.underlay_mtu,
556                ip_addr: next_hop_ip,
557                mac_addr: Some(gre.mac_addr),
558                preferred_src_ip,
559                gre: Some(gre),
560                vlan: None,
561            });
562        }
563
564        let mtu = self
565            .interfaces
566            .iter()
567            .find(|interface| interface.if_index == if_index)
568            .map(|interface| interface.mtu)
569            .ok_or(RouteError::UnknownInterfaceIndex(if_index))?;
570        let vlan = self.cached_vlan_route_info(if_index);
571
572        // IPv4 multicast destinations map deterministically to a multicast MAC (RFC 1112 §6.4)
573        // and never participate in ARP, so there is no neighbor entry to look up. Otherwise
574        // consult the neighbor cache for the unicast MAC.
575        let mac_addr = if next_hop_v4.is_multicast() {
576            Some(ipv4_multicast_mac(next_hop_v4))
577        } else {
578            self.neighbors.lookup(next_hop_ip, if_index).cloned()
579        };
580        Ok(NextHop {
581            ip_addr: next_hop_ip,
582            mac_addr,
583            if_index,
584            mtu,
585            preferred_src_ip,
586            gre: None,
587            vlan,
588        })
589    }
590
591    fn default_route(&self) -> Result<NextHop, RouteError> {
592        let default_route = self
593            .ipv4_lpm
594            .default_route()
595            .and_then(|route_idx| self.routes.get(route_idx as usize))
596            .ok_or(RouteError::NoRouteFound(IpAddr::V4(Ipv4Addr::UNSPECIFIED)))?;
597        self.resolve_next_hop(Ipv4Addr::UNSPECIFIED, default_route)
598    }
599
600    pub fn default(&self) -> Result<NextHop, RouteError> {
601        if let Some(default_route) = &self.cached_default_route {
602            Ok(default_route.clone())
603        } else {
604            self.default_route()
605        }
606    }
607
608    fn lookup_route_v4(&self, dest_ip: Ipv4Addr) -> Option<&Route<Ipv4Addr>> {
609        self.ipv4_lpm
610            .lookup(dest_ip)
611            .and_then(|route_idx| self.routes.get(route_idx as usize))
612    }
613
614    pub fn route_v4(&self, dest_ip: Ipv4Addr) -> Result<NextHop, RouteError> {
615        let route = self
616            .lookup_route_v4(dest_ip)
617            .ok_or(RouteError::NoRouteFound(dest_ip.into()))?;
618        self.resolve_next_hop(dest_ip, route)
619    }
620
621    fn interface_gre_route_info(&self, interface: &InterfaceInfo) -> Option<GreRouteInfo> {
622        let tunnel_info = interface.gre_tunnel.as_ref()?;
623        let remote = match tunnel_info.remote {
624            IpAddr::V4(remote) => remote,
625            IpAddr::V6(_) => return None,
626        };
627        let local = match tunnel_info.local {
628            IpAddr::V4(local) => local,
629            IpAddr::V6(_) => return None,
630        };
631        // Skip unconfigured tunnels (remote/local 0.0.0.0)
632        if remote == Ipv4Addr::UNSPECIFIED || local == Ipv4Addr::UNSPECIFIED {
633            return None;
634        }
635
636        let underlay_route = self.lookup_route_v4(remote)?;
637        let underlay_if_index = underlay_route.out_if_index?;
638        let underlay_interface = self
639            .interfaces
640            .iter()
641            .find(|candidate| candidate.if_index == underlay_if_index)?;
642        if underlay_interface.is_gre() {
643            warn!(
644                "GRE interface {} has remote {} that routes via another GRE interface {}. \
645                 gre-over-gre is not supported.",
646                interface.if_index, tunnel_info.remote, underlay_interface.if_index
647            );
648            return None;
649        }
650        let underlay_next_hop_ip = IpAddr::V4(underlay_route.gateway.unwrap_or(remote));
651        let mac_addr = self
652            .neighbors
653            .lookup(underlay_next_hop_ip, underlay_if_index)
654            .copied()?;
655
656        Some(GreRouteInfo {
657            if_index: interface.if_index,
658            mtu: interface.mtu,
659            underlay_mtu: underlay_interface.mtu,
660            tunnel_info: tunnel_info.clone(),
661            mac_addr,
662        })
663    }
664}
665
666/// Map an IPv4 multicast address to its Ethernet destination MAC per RFC 1112 §6.4:
667/// the low 23 bits of the address are placed into the low 23 bits of `01:00:5e:00:00:00`.
668#[inline]
669fn ipv4_multicast_mac(ip: Ipv4Addr) -> MacAddress {
670    let [_, b1, b2, b3] = ip.octets();
671    MacAddress([0x01, 0x00, 0x5e, b1 & 0x7f, b2, b3])
672}
673
674fn format_route(f: &mut fmt::Formatter<'_>, route: &Route<Ipv4Addr>) -> fmt::Result {
675    match route.destination {
676        Some(destination) if route.dst_len == 32 => write!(f, "{destination}")?,
677        Some(destination) => write!(f, "{destination}/{}", route.dst_len)?,
678        None => f.write_str("default")?,
679    }
680
681    if let Some(gateway) = route.gateway {
682        write!(f, " via {gateway}")?;
683    }
684    if let Some(if_index) = route.out_if_index {
685        write!(f, " dev if{if_index}")?;
686    }
687    if let Some(preferred_src) = route.preferred_src {
688        write!(f, " src {preferred_src}")?;
689    }
690    if let Some(priority) = route.priority {
691        write!(f, " metric {priority}")?;
692    }
693
694    Ok(())
695}
696
697#[cfg(test)]
698mod tests {
699    use {
700        super::*,
701        crate::netlink::{MacAddress, NeighborEntry, RouteEntry, VlanLinkInfo},
702        libc::{AF_INET, NUD_REACHABLE},
703        std::net::{IpAddr, Ipv4Addr},
704    };
705
706    const DEFAULT_MTU_FOR_TESTS: u32 = 1500;
707
708    fn test_route(destination: Option<Ipv4Addr>, out_if_index: u32) -> Route<Ipv4Addr> {
709        Route {
710            destination,
711            gateway: None,
712            preferred_src: None,
713            out_if_index: Some(out_if_index),
714            priority: None,
715            type_: 0,
716            dst_len: 32,
717        }
718    }
719
720    fn test_route_entry(
721        destination: Option<Ipv4Addr>,
722        gateway: Option<Ipv4Addr>,
723        out_if_index: u32,
724        dst_len: u8,
725        table: u32,
726    ) -> RouteEntry {
727        test_route_entry_with_priority(destination, gateway, out_if_index, dst_len, table, None)
728    }
729
730    fn test_route_entry_with_priority(
731        destination: Option<Ipv4Addr>,
732        gateway: Option<Ipv4Addr>,
733        out_if_index: u32,
734        dst_len: u8,
735        table: u32,
736        priority: Option<u32>,
737    ) -> RouteEntry {
738        RouteEntry {
739            destination: destination.map(IpAddr::V4),
740            gateway: gateway.map(IpAddr::V4),
741            pref_src: None,
742            out_if_index: Some(out_if_index as i32),
743            in_if_index: None,
744            priority,
745            table: Some(table),
746            protocol: 0,
747            scope: 0,
748            type_: 0,
749            family: AF_INET as u8,
750            dst_len,
751            flags: 0,
752        }
753    }
754
755    fn router_from_tables(
756        neighbors: Vec<NeighborEntry>,
757        routes: Vec<Route<Ipv4Addr>>,
758        interfaces: Vec<InterfaceInfo>,
759    ) -> Router {
760        let tables = RoutingTables::new(
761            Routes::new(RouteTable::Main, routes),
762            Neighbors::new(neighbors),
763            Interfaces::new(interfaces),
764        );
765        Router::from_tables(tables).unwrap()
766    }
767
768    fn empty_tables() -> RoutingTables {
769        RoutingTables::new(
770            Routes::new(RouteTable::Main, Vec::new()),
771            Neighbors::new(Vec::new()),
772            Interfaces::new(Vec::new()),
773        )
774    }
775
776    fn dual_default_tables(primary_gateway: Ipv4Addr, backup_gateway: Ipv4Addr) -> RoutingTables {
777        let mut tables = empty_tables();
778        assert!(tables.upsert_interface(InterfaceInfo {
779            if_index: 1,
780            mtu: DEFAULT_MTU_FOR_TESTS,
781            gre_tunnel: None,
782            vlan_link: None,
783        }));
784        assert!(tables.upsert_interface(InterfaceInfo {
785            if_index: 2,
786            mtu: DEFAULT_MTU_FOR_TESTS,
787            gre_tunnel: None,
788            vlan_link: None,
789        }));
790        assert!(tables.upsert_neighbor(NeighborEntry {
791            destination: Some(IpAddr::V4(primary_gateway)),
792            lladdr: Some(MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01])),
793            ifindex: 1,
794            state: NUD_REACHABLE,
795        }));
796        assert!(tables.upsert_neighbor(NeighborEntry {
797            destination: Some(IpAddr::V4(backup_gateway)),
798            lladdr: Some(MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x02])),
799            ifindex: 2,
800            state: NUD_REACHABLE,
801        }));
802        assert!(tables.upsert_route(test_route_entry_with_priority(
803            None,
804            Some(backup_gateway),
805            2,
806            0,
807            u32::from(RouteTable::Main),
808            Some(200),
809        )));
810        assert!(tables.upsert_route(test_route_entry_with_priority(
811            None,
812            Some(primary_gateway),
813            1,
814            0,
815            u32::from(RouteTable::Main),
816            Some(100),
817        )));
818        tables
819    }
820
821    #[test]
822    fn test_router() {
823        let mut tables = empty_tables();
824        let before_routes_len = tables.routes.iter().len();
825
826        assert!(tables.upsert_interface(InterfaceInfo {
827            if_index: 1,
828            mtu: DEFAULT_MTU_FOR_TESTS,
829            gre_tunnel: None,
830            vlan_link: None,
831        }));
832
833        let gateway = Ipv4Addr::new(10, 255, 255, 1);
834        let neighbor = NeighborEntry {
835            destination: Some(IpAddr::V4(gateway)),
836            lladdr: Some(MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01])),
837            ifindex: 1,
838            state: NUD_REACHABLE,
839        };
840        assert!(tables.upsert_neighbor(neighbor));
841
842        // Create a unique, private IPv4 /32 route to avoid collisions
843        let test_dst = Ipv4Addr::new(10, 255, 255, 123);
844        let route = RouteEntry {
845            destination: Some(IpAddr::V4(test_dst)),
846            gateway: Some(IpAddr::V4(gateway)),
847            pref_src: None,
848            out_if_index: Some(1),
849            in_if_index: None,
850            priority: None,
851            table: Some(u32::from(RouteTable::Main)),
852            protocol: 0,
853            scope: 0,
854            type_: 0,
855            family: AF_INET as u8,
856            dst_len: 32,
857            flags: 0,
858        };
859
860        // Upsert new route and check that it was inserted and routes are dirty
861        assert!(tables.upsert_route(route.clone()));
862        assert!(tables.routes.iter().any(|r| {
863            r.destination == Some(test_dst)
864                && r.gateway == Some(gateway)
865                && r.out_if_index == Some(1)
866        }));
867        assert!(tables.routes.iter().len() >= before_routes_len);
868
869        let router = Router::from_tables(tables.clone()).unwrap();
870        let next_hop = router.route_v4(test_dst).unwrap();
871        assert_eq!(next_hop.if_index, 1);
872        assert_eq!(next_hop.ip_addr, IpAddr::V4(gateway));
873
874        // Delete using same key should remove the route
875        assert!(tables.remove_route(route.clone()));
876        assert!(
877            tables
878                .routes
879                .iter()
880                .all(|r| r.destination != Some(test_dst))
881        );
882        assert_eq!(tables.routes.iter().len(), before_routes_len);
883    }
884
885    #[test]
886    fn test_routing_table_display() {
887        let router = router_from_tables(
888            vec![],
889            vec![
890                Route {
891                    destination: Some(Ipv4Addr::new(10, 0, 0, 0)),
892                    gateway: Some(Ipv4Addr::new(192, 168, 1, 1)),
893                    preferred_src: Some(Ipv4Addr::new(192, 168, 1, 10)),
894                    out_if_index: Some(2),
895                    priority: Some(100),
896                    type_: 0,
897                    dst_len: 24,
898                },
899                Route {
900                    destination: None,
901                    gateway: Some(Ipv4Addr::new(192, 168, 1, 254)),
902                    preferred_src: None,
903                    out_if_index: Some(3),
904                    priority: None,
905                    type_: 0,
906                    dst_len: 0,
907                },
908            ],
909            vec![
910                InterfaceInfo {
911                    if_index: 2,
912                    mtu: DEFAULT_MTU_FOR_TESTS,
913                    gre_tunnel: None,
914                    vlan_link: None,
915                },
916                InterfaceInfo {
917                    if_index: 3,
918                    mtu: DEFAULT_MTU_FOR_TESTS,
919                    gre_tunnel: None,
920                    vlan_link: None,
921                },
922            ],
923        );
924
925        assert_eq!(
926            router.routing_table().to_string(),
927            "10.0.0.0/24 via 192.168.1.1 dev if2 src 192.168.1.10 metric 100\ndefault via \
928             192.168.1.254 dev if3"
929        );
930    }
931
932    #[test]
933    fn test_neighbors_table() {
934        let mut tables = empty_tables();
935        let before_neigh_len = tables.neighbors.iter().len();
936
937        // Create a unique, private neighbor entry on a dummy ifindex
938        let neigh_ip = Ipv4Addr::new(10, 255, 255, 77);
939        let entry = NeighborEntry {
940            destination: Some(IpAddr::V4(neigh_ip)),
941            lladdr: Some(MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01])),
942            ifindex: 1,
943            state: NUD_REACHABLE,
944        };
945
946        // Upsert new neighbor and check that it was inserted and neighbors are dirty
947        assert!(tables.upsert_neighbor(entry.clone()));
948        assert!(tables.neighbors.iter().any(|n| n == &entry));
949        assert!(tables.neighbors.iter().len() >= before_neigh_len);
950
951        // Delete neighbor and check that it was deleted
952        assert!(tables.remove_neighbor(neigh_ip, 1));
953        assert!(tables.neighbors.iter().all(|n| n != &entry));
954        assert_eq!(tables.neighbors.iter().len(), before_neigh_len);
955    }
956
957    #[test]
958    fn test_interface_table() {
959        let mut tables = empty_tables();
960        let before_interface_len = tables.interfaces.iter().len();
961
962        // Create a unique, private interface with a dummy ifindex
963        let test_if_index = 99999;
964        let interface = InterfaceInfo {
965            if_index: test_if_index,
966            mtu: DEFAULT_MTU_FOR_TESTS,
967            gre_tunnel: None,
968            vlan_link: None,
969        };
970
971        // Upsert new interface and check that it was inserted
972        assert!(tables.upsert_interface(interface.clone()));
973        assert!(tables.interfaces.iter().any(|i| i == &interface));
974        assert!(tables.interfaces.iter().len() >= before_interface_len);
975
976        // Upsert same interface with no changes should return false
977        assert!(!tables.upsert_interface(interface.clone()));
978
979        // Upsert with changes should return true
980        let mut modified_interface = interface.clone();
981        modified_interface.gre_tunnel = Some(GreTunnelInfo {
982            local: IpAddr::V4(Ipv4Addr::new(10, 255, 255, 2)),
983            remote: IpAddr::V4(Ipv4Addr::new(10, 255, 255, 1)),
984            ttl: 0,
985            tos: 0,
986            pmtudisc: 0,
987        });
988        assert!(tables.upsert_interface(modified_interface.clone()));
989        assert!(tables.interfaces.iter().any(|i| i == &modified_interface));
990        assert!(tables.interfaces.iter().all(|i| i != &interface));
991
992        // Delete interface and check that it was deleted
993        assert!(tables.remove_interface(test_if_index));
994        assert!(
995            tables
996                .interfaces
997                .iter()
998                .all(|i| i.if_index != test_if_index)
999        );
1000        assert_eq!(tables.interfaces.iter().len(), before_interface_len);
1001    }
1002
1003    #[test]
1004    fn test_routes_ignore_other_table_updates() {
1005        let test_dst = Ipv4Addr::new(10, 0, 0, 1);
1006        let main_gateway = Ipv4Addr::new(10, 0, 0, 2);
1007        let other_gateway = Ipv4Addr::new(10, 0, 0, 3);
1008        let main_table = u32::from(RouteTable::Main);
1009
1010        let mut tables = empty_tables();
1011        assert!(tables.upsert_interface(InterfaceInfo {
1012            if_index: 1,
1013            mtu: DEFAULT_MTU_FOR_TESTS,
1014            gre_tunnel: None,
1015            vlan_link: None,
1016        }));
1017        assert!(tables.upsert_neighbor(NeighborEntry {
1018            destination: Some(IpAddr::V4(main_gateway)),
1019            lladdr: Some(MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01])),
1020            ifindex: 1,
1021            state: NUD_REACHABLE,
1022        }));
1023        assert!(tables.upsert_route(test_route_entry(
1024            Some(test_dst),
1025            Some(main_gateway),
1026            1,
1027            32,
1028            main_table,
1029        )));
1030
1031        assert!(!tables.upsert_route(test_route_entry(
1032            Some(test_dst),
1033            Some(other_gateway),
1034            2,
1035            32,
1036            100,
1037        )));
1038
1039        let router = Router::from_tables(tables).unwrap();
1040        let next_hop = router.route_v4(test_dst).unwrap();
1041        assert_eq!(next_hop.if_index, 1);
1042        assert_eq!(next_hop.ip_addr, IpAddr::V4(main_gateway));
1043    }
1044
1045    #[test]
1046    fn test_routes_ignore_other_table_deletes() {
1047        let test_dst = Ipv4Addr::new(10, 0, 0, 1);
1048        let main_gateway = Ipv4Addr::new(10, 0, 0, 2);
1049        let main_table = u32::from(RouteTable::Main);
1050
1051        let mut tables = empty_tables();
1052        assert!(tables.upsert_interface(InterfaceInfo {
1053            if_index: 1,
1054            mtu: DEFAULT_MTU_FOR_TESTS,
1055            gre_tunnel: None,
1056            vlan_link: None,
1057        }));
1058        assert!(tables.upsert_neighbor(NeighborEntry {
1059            destination: Some(IpAddr::V4(main_gateway)),
1060            lladdr: Some(MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01])),
1061            ifindex: 1,
1062            state: NUD_REACHABLE,
1063        }));
1064        assert!(tables.upsert_route(test_route_entry(
1065            Some(test_dst),
1066            Some(main_gateway),
1067            1,
1068            32,
1069            main_table,
1070        )));
1071
1072        assert!(!tables.remove_route(test_route_entry(
1073            Some(test_dst),
1074            Some(Ipv4Addr::new(10, 0, 0, 3)),
1075            2,
1076            32,
1077            100,
1078        )));
1079
1080        let router = Router::from_tables(tables).unwrap();
1081        let next_hop = router.route_v4(test_dst).unwrap();
1082        assert_eq!(next_hop.if_index, 1);
1083        assert_eq!(next_hop.ip_addr, IpAddr::V4(main_gateway));
1084    }
1085
1086    #[test]
1087    fn test_multiple_gre_interfaces() {
1088        let remote1 = Ipv4Addr::new(10, 0, 0, 1);
1089        let remote2 = Ipv4Addr::new(10, 0, 0, 2);
1090        let gre_dest1 = Ipv4Addr::new(192, 168, 0, 1);
1091        let gre_dest2 = Ipv4Addr::new(192, 168, 0, 2);
1092        let if_index_underlay = 1;
1093        let if_index_gre1 = 100;
1094        let if_index_gre2 = 200;
1095        let mac1 = MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01]);
1096        let mac2 = MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x02]);
1097
1098        let neighbors = vec![
1099            NeighborEntry {
1100                destination: Some(IpAddr::V4(remote1)),
1101                lladdr: Some(mac1),
1102                ifindex: if_index_underlay,
1103                state: NUD_REACHABLE,
1104            },
1105            NeighborEntry {
1106                destination: Some(IpAddr::V4(remote2)),
1107                lladdr: Some(mac2),
1108                ifindex: if_index_underlay,
1109                state: NUD_REACHABLE,
1110            },
1111        ];
1112
1113        let routes = vec![
1114            test_route(Some(remote1), if_index_underlay as u32),
1115            test_route(Some(remote2), if_index_underlay as u32),
1116            test_route(Some(gre_dest1), if_index_gre1 as u32),
1117            test_route(Some(gre_dest2), if_index_gre2 as u32),
1118        ];
1119
1120        let interfaces = vec![
1121            InterfaceInfo {
1122                if_index: if_index_underlay as u32,
1123                mtu: DEFAULT_MTU_FOR_TESTS,
1124                gre_tunnel: None,
1125                vlan_link: None,
1126            },
1127            InterfaceInfo {
1128                if_index: if_index_gre1 as u32,
1129                mtu: DEFAULT_MTU_FOR_TESTS - 100,
1130                gre_tunnel: Some(GreTunnelInfo {
1131                    local: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 3)),
1132                    remote: IpAddr::V4(remote1),
1133                    ttl: 0,
1134                    tos: 0,
1135                    pmtudisc: 0,
1136                }),
1137                vlan_link: None,
1138            },
1139            InterfaceInfo {
1140                if_index: if_index_gre2 as u32,
1141                mtu: DEFAULT_MTU_FOR_TESTS + 100,
1142                gre_tunnel: Some(GreTunnelInfo {
1143                    local: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 4)),
1144                    remote: IpAddr::V4(remote2),
1145                    ttl: 0,
1146                    tos: 0,
1147                    pmtudisc: 0,
1148                }),
1149                vlan_link: None,
1150            },
1151        ];
1152
1153        let router = router_from_tables(neighbors, routes, interfaces);
1154        let hop1 = router.route_v4(gre_dest1).unwrap();
1155        assert_eq!(hop1.if_index, if_index_gre1 as u32);
1156        assert_eq!(hop1.mtu, DEFAULT_MTU_FOR_TESTS);
1157        assert_eq!(hop1.mac_addr, Some(mac1));
1158
1159        let hop1_gre = hop1.gre.as_ref().unwrap();
1160        assert_eq!(hop1_gre.if_index, if_index_gre1 as u32);
1161        assert_eq!(hop1_gre.mtu, DEFAULT_MTU_FOR_TESTS - 100);
1162        assert_eq!(hop1_gre.underlay_mtu, DEFAULT_MTU_FOR_TESTS);
1163        assert_eq!(hop1.mtu, hop1_gre.underlay_mtu);
1164
1165        let hop2 = router.route_v4(gre_dest2).unwrap();
1166        assert_eq!(hop2.if_index, if_index_gre2 as u32);
1167        assert_eq!(hop2.mtu, DEFAULT_MTU_FOR_TESTS);
1168        assert_eq!(hop2.mac_addr, Some(mac2));
1169
1170        let hop2_gre = hop2.gre.as_ref().unwrap();
1171        assert_eq!(hop2_gre.if_index, if_index_gre2 as u32);
1172        assert_eq!(hop2_gre.mtu, DEFAULT_MTU_FOR_TESTS + 100);
1173        assert_eq!(hop2_gre.underlay_mtu, DEFAULT_MTU_FOR_TESTS);
1174        assert_eq!(hop2.mtu, hop2_gre.underlay_mtu);
1175    }
1176
1177    #[test]
1178    fn test_default_route_via_gre() {
1179        let remote = Ipv4Addr::new(10, 0, 0, 1);
1180        let dest = Ipv4Addr::new(203, 0, 113, 9);
1181        let if_index_underlay = 1;
1182        let if_index_gre = 100;
1183        let mac = MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01]);
1184
1185        let neighbors = vec![NeighborEntry {
1186            destination: Some(IpAddr::V4(remote)),
1187            lladdr: Some(mac),
1188            ifindex: if_index_underlay,
1189            state: NUD_REACHABLE,
1190        }];
1191
1192        let routes = vec![
1193            test_route(Some(remote), if_index_underlay as u32),
1194            Route {
1195                destination: None,
1196                gateway: None,
1197                preferred_src: None,
1198                out_if_index: Some(if_index_gre as u32),
1199                priority: None,
1200                type_: 0,
1201                dst_len: 0,
1202            },
1203        ];
1204
1205        let interfaces = vec![
1206            InterfaceInfo {
1207                if_index: if_index_underlay as u32,
1208                mtu: DEFAULT_MTU_FOR_TESTS,
1209                gre_tunnel: None,
1210                vlan_link: None,
1211            },
1212            InterfaceInfo {
1213                if_index: if_index_gre as u32,
1214                mtu: DEFAULT_MTU_FOR_TESTS - 100,
1215                gre_tunnel: Some(GreTunnelInfo {
1216                    local: IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1)),
1217                    remote: IpAddr::V4(remote),
1218                    ttl: 0,
1219                    tos: 0,
1220                    pmtudisc: 0,
1221                }),
1222                vlan_link: None,
1223            },
1224        ];
1225
1226        let router = router_from_tables(neighbors, routes, interfaces);
1227
1228        let hop_default = router.default().unwrap();
1229        assert_eq!(hop_default.if_index, if_index_gre as u32);
1230        assert_eq!(hop_default.mtu, DEFAULT_MTU_FOR_TESTS);
1231        assert_eq!(hop_default.mac_addr, Some(mac));
1232
1233        let hop_default_gre = hop_default.gre.as_ref().unwrap();
1234        assert_eq!(hop_default_gre.if_index, if_index_gre as u32);
1235        assert_eq!(hop_default_gre.mtu, DEFAULT_MTU_FOR_TESTS - 100);
1236        assert_eq!(hop_default_gre.underlay_mtu, DEFAULT_MTU_FOR_TESTS);
1237        assert_eq!(hop_default.mtu, hop_default_gre.underlay_mtu);
1238
1239        let hop_route = router.route_v4(dest).unwrap();
1240        assert_eq!(hop_route.if_index, if_index_gre as u32);
1241        assert_eq!(hop_route.mtu, DEFAULT_MTU_FOR_TESTS);
1242        assert_eq!(hop_route.mac_addr, Some(mac));
1243
1244        let hop_route_gre = hop_route.gre.as_ref().unwrap();
1245        assert_eq!(hop_route_gre.if_index, if_index_gre as u32);
1246        assert_eq!(hop_route_gre.mtu, DEFAULT_MTU_FOR_TESTS - 100);
1247        assert_eq!(hop_route_gre.underlay_mtu, DEFAULT_MTU_FOR_TESTS);
1248        assert_eq!(hop_route.mtu, hop_route_gre.underlay_mtu);
1249    }
1250
1251    #[test]
1252    fn test_missing_output_interface_blocks_fallback_route() {
1253        let blocked_dst = Ipv4Addr::new(10, 0, 0, 2);
1254        let default_gateway = Ipv4Addr::new(10, 0, 0, 1);
1255
1256        let mut tables = empty_tables();
1257        assert!(tables.upsert_interface(InterfaceInfo {
1258            if_index: 1,
1259            mtu: DEFAULT_MTU_FOR_TESTS,
1260            gre_tunnel: None,
1261            vlan_link: None,
1262        }));
1263        assert!(tables.upsert_neighbor(NeighborEntry {
1264            destination: Some(IpAddr::V4(default_gateway)),
1265            lladdr: Some(MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01])),
1266            ifindex: 1,
1267            state: NUD_REACHABLE,
1268        }));
1269
1270        assert!(tables.upsert_route(test_route_entry(
1271            None,
1272            Some(default_gateway),
1273            1,
1274            0,
1275            u32::from(RouteTable::Main),
1276        )));
1277
1278        assert!(tables.upsert_route(RouteEntry {
1279            destination: Some(IpAddr::V4(blocked_dst)),
1280            gateway: None,
1281            pref_src: None,
1282            out_if_index: None, // missing output interface should block this route
1283            in_if_index: None,
1284            priority: None,
1285            table: Some(u32::from(RouteTable::Main)),
1286            protocol: 0,
1287            scope: 0,
1288            type_: 0,
1289            family: AF_INET as u8,
1290            dst_len: 32,
1291            flags: 0,
1292        }));
1293
1294        let router = Router::from_tables(tables).unwrap();
1295        assert!(matches!(
1296            router.route_v4(blocked_dst),
1297            Err(RouteError::MissingOutputInterface)
1298        ));
1299    }
1300
1301    #[test]
1302    fn test_same_prefix_diff_priority() {
1303        let primary = Ipv4Addr::new(10, 0, 0, 1);
1304        let backup = Ipv4Addr::new(10, 0, 0, 2);
1305        let tables = dual_default_tables(primary, backup);
1306
1307        let router = Router::from_tables(tables.clone()).unwrap();
1308        let next_hop = router.default().unwrap();
1309        assert_eq!(next_hop.if_index, 1);
1310        assert_eq!(next_hop.ip_addr, IpAddr::V4(primary));
1311
1312        {
1313            let mut tables = tables.clone();
1314            assert!(tables.remove_route(test_route_entry_with_priority(
1315                None,
1316                Some(backup),
1317                2,
1318                0,
1319                u32::from(RouteTable::Main),
1320                Some(200),
1321            )));
1322
1323            let router = Router::from_tables(tables).unwrap();
1324            let next_hop = router.default().unwrap();
1325            assert_eq!(next_hop.if_index, 1);
1326            assert_eq!(next_hop.ip_addr, IpAddr::V4(primary));
1327        }
1328
1329        let mut tables = tables;
1330        assert!(tables.remove_route(test_route_entry_with_priority(
1331            None,
1332            Some(primary),
1333            1,
1334            0,
1335            u32::from(RouteTable::Main),
1336            Some(100),
1337        )));
1338
1339        let router = Router::from_tables(tables).unwrap();
1340        let next_hop = router.default().unwrap();
1341        assert_eq!(next_hop.if_index, 2);
1342        assert_eq!(next_hop.ip_addr, IpAddr::V4(backup));
1343    }
1344
1345    #[test]
1346    fn test_vlan_route_resolves_with_vlan_info() {
1347        // A route through a configured VLAN sub-interface attaches VlanRouteInfo to the NextHop;
1348        // the MAC still comes from the neighbor cache and no GRE encap is set.
1349        let peer = Ipv4Addr::new(10, 228, 0, 99);
1350        let vlan_if = 100u32;
1351        let vlan_src = Ipv4Addr::new(10, 228, 0, 5);
1352        let peer_mac = MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x09]);
1353
1354        let neighbors = vec![NeighborEntry {
1355            destination: Some(IpAddr::V4(peer)),
1356            lladdr: Some(peer_mac),
1357            ifindex: vlan_if as i32,
1358            state: NUD_REACHABLE,
1359        }];
1360        let interfaces = vec![InterfaceInfo {
1361            if_index: vlan_if,
1362            mtu: DEFAULT_MTU_FOR_TESTS,
1363            gre_tunnel: None,
1364            vlan_link: Some(VlanLinkInfo { vid: 900 }),
1365        }];
1366        let routes = vec![Route {
1367            destination: Some(Ipv4Addr::new(10, 228, 0, 0)),
1368            gateway: None,
1369            preferred_src: Some(vlan_src),
1370            out_if_index: Some(vlan_if),
1371            priority: None,
1372            type_: 0,
1373            dst_len: 22,
1374        }];
1375
1376        let router = router_from_tables(neighbors, routes, interfaces);
1377        let next_hop = router.route_v4(peer).unwrap();
1378        assert_eq!(next_hop.if_index, vlan_if);
1379        assert_eq!(next_hop.preferred_src_ip, Some(vlan_src));
1380        assert_eq!(next_hop.mac_addr, Some(peer_mac));
1381        assert!(next_hop.gre.is_none());
1382        let vlan = next_hop.vlan.expect("vlan info must be populated");
1383        assert_eq!(vlan.if_index, vlan_if);
1384        assert_eq!(vlan.vid, 900);
1385        assert_eq!(vlan.pcp, 0);
1386    }
1387
1388    #[test]
1389    fn test_multiple_vlan_interfaces_resolve_their_own_vid() {
1390        // With more than one VLAN sub-interface, each route is tagged with the vid of the
1391        // interface it egresses through.
1392        let peer_a = Ipv4Addr::new(10, 1, 0, 9);
1393        let peer_b = Ipv4Addr::new(10, 2, 0, 9);
1394        let vlan_if_a = 100u32;
1395        let vlan_if_b = 101u32;
1396        let mac_a = MacAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x0a]);
1397        let mac_b = MacAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x0b]);
1398
1399        let neighbors = vec![
1400            NeighborEntry {
1401                destination: Some(IpAddr::V4(peer_a)),
1402                lladdr: Some(mac_a),
1403                ifindex: vlan_if_a as i32,
1404                state: NUD_REACHABLE,
1405            },
1406            NeighborEntry {
1407                destination: Some(IpAddr::V4(peer_b)),
1408                lladdr: Some(mac_b),
1409                ifindex: vlan_if_b as i32,
1410                state: NUD_REACHABLE,
1411            },
1412        ];
1413        let interfaces = vec![
1414            InterfaceInfo {
1415                if_index: vlan_if_a,
1416                mtu: DEFAULT_MTU_FOR_TESTS,
1417                gre_tunnel: None,
1418                vlan_link: Some(VlanLinkInfo { vid: 900 }),
1419            },
1420            InterfaceInfo {
1421                if_index: vlan_if_b,
1422                mtu: DEFAULT_MTU_FOR_TESTS,
1423                gre_tunnel: None,
1424                vlan_link: Some(VlanLinkInfo { vid: 901 }),
1425            },
1426        ];
1427        let routes = vec![
1428            test_route(Some(peer_a), vlan_if_a),
1429            test_route(Some(peer_b), vlan_if_b),
1430        ];
1431
1432        let router = router_from_tables(neighbors, routes, interfaces);
1433        let hop_a = router.route_v4(peer_a).unwrap();
1434        assert_eq!(hop_a.vlan.map(|v| v.vid), Some(900));
1435        assert_eq!(hop_a.mac_addr, Some(mac_a));
1436        let hop_b = router.route_v4(peer_b).unwrap();
1437        assert_eq!(hop_b.vlan.map(|v| v.vid), Some(901));
1438        assert_eq!(hop_b.mac_addr, Some(mac_b));
1439    }
1440
1441    #[test]
1442    fn test_gre_wins_when_both_gre_and_vlan_configured() {
1443        // Defensive: if a configured VLAN if_index also happens to be a GRE interface, the resolve
1444        // path picks GRE first (single-encap-per-interface contract).
1445        let remote = Ipv4Addr::new(10, 0, 0, 1);
1446        let gre_dest = Ipv4Addr::new(192, 168, 0, 1);
1447        let if_index_underlay = 1u32;
1448        let if_index_iface = 100u32;
1449        let underlay_mac = MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01]);
1450
1451        let neighbors = vec![NeighborEntry {
1452            destination: Some(IpAddr::V4(remote)),
1453            lladdr: Some(underlay_mac),
1454            ifindex: if_index_underlay as i32,
1455            state: NUD_REACHABLE,
1456        }];
1457        let routes = vec![
1458            test_route(Some(remote), if_index_underlay),
1459            test_route(Some(gre_dest), if_index_iface),
1460        ];
1461        let interfaces = vec![
1462            InterfaceInfo {
1463                if_index: if_index_underlay,
1464                mtu: DEFAULT_MTU_FOR_TESTS,
1465                gre_tunnel: None,
1466                vlan_link: None,
1467            },
1468            InterfaceInfo {
1469                if_index: if_index_iface,
1470                mtu: DEFAULT_MTU_FOR_TESTS,
1471                gre_tunnel: Some(GreTunnelInfo {
1472                    local: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 3)),
1473                    remote: IpAddr::V4(remote),
1474                    ttl: 0,
1475                    tos: 0,
1476                    pmtudisc: 0,
1477                }),
1478                vlan_link: Some(VlanLinkInfo { vid: 5 }),
1479            },
1480        ];
1481        let router = router_from_tables(neighbors, routes, interfaces);
1482        let next_hop = router.route_v4(gre_dest).unwrap();
1483        assert!(next_hop.gre.is_some());
1484        assert!(next_hop.vlan.is_none());
1485    }
1486
1487    #[test]
1488    fn test_ipv4_multicast_mac_mapping() {
1489        // RFC 1112: 01:00:5e + (octet1 & 0x7f) + octet2 + octet3
1490        assert_eq!(
1491            ipv4_multicast_mac(Ipv4Addr::new(224, 0, 0, 1)),
1492            MacAddress([0x01, 0x00, 0x5e, 0x00, 0x00, 0x01]),
1493        );
1494        assert_eq!(
1495            ipv4_multicast_mac(Ipv4Addr::new(239, 0, 0, 3)),
1496            MacAddress([0x01, 0x00, 0x5e, 0x00, 0x00, 0x03]),
1497        );
1498        // High bit of the second octet must be cleared (only the low 23 bits map).
1499        assert_eq!(
1500            ipv4_multicast_mac(Ipv4Addr::new(239, 128, 0, 1)),
1501            MacAddress([0x01, 0x00, 0x5e, 0x00, 0x00, 0x01]),
1502        );
1503        assert_eq!(
1504            ipv4_multicast_mac(Ipv4Addr::new(239, 255, 255, 250)),
1505            MacAddress([0x01, 0x00, 0x5e, 0x7f, 0xff, 0xfa]),
1506        );
1507    }
1508
1509    #[test]
1510    fn test_multicast_route_resolves_with_computed_mac_without_arp() {
1511        // Route 239.0.0.0/8 dev iface (no gateway, no ARP entry): resolution must yield the
1512        // RFC-1112 multicast MAC instead of falling through to a missing neighbor.
1513        let multicast_dst = Ipv4Addr::new(239, 0, 0, 3);
1514        let if_index = 100u32;
1515        let routes = vec![Route {
1516            destination: Some(Ipv4Addr::new(239, 0, 0, 0)),
1517            gateway: None,
1518            preferred_src: None,
1519            out_if_index: Some(if_index),
1520            priority: None,
1521            type_: 0,
1522            dst_len: 8,
1523        }];
1524        let interfaces = vec![InterfaceInfo {
1525            if_index,
1526            mtu: DEFAULT_MTU_FOR_TESTS,
1527            gre_tunnel: None,
1528            vlan_link: None,
1529        }];
1530        let router = router_from_tables(vec![], routes, interfaces);
1531        let next_hop = router.route_v4(multicast_dst).unwrap();
1532        assert_eq!(
1533            next_hop.mac_addr,
1534            Some(MacAddress([0x01, 0x00, 0x5e, 0x00, 0x00, 0x03]))
1535        );
1536        assert_eq!(next_hop.if_index, if_index);
1537    }
1538
1539    #[test]
1540    fn test_unicast_without_neighbor_returns_no_mac() {
1541        // Regression guard for the unicast branch: with no neighbor entry the MAC stays None so
1542        // the tx loop drops the packet rather than sending it to a bogus address.
1543        let dst = Ipv4Addr::new(10, 1, 2, 3);
1544        let if_index = 5u32;
1545        let routes = vec![test_route(Some(dst), if_index)];
1546        let interfaces = vec![InterfaceInfo {
1547            if_index,
1548            mtu: DEFAULT_MTU_FOR_TESTS,
1549            gre_tunnel: None,
1550            vlan_link: None,
1551        }];
1552        let router = router_from_tables(vec![], routes, interfaces);
1553        let next_hop = router.route_v4(dst).unwrap();
1554        assert!(next_hop.mac_addr.is_none());
1555    }
1556}