Skip to main content

nlink/netlink/
fdb.rs

1//! Bridge Forwarding Database (FDB) management.
2//!
3//! This module provides typed builders for managing bridge FDB entries,
4//! which are used for MAC address learning and forwarding in Linux bridges.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use nlink::netlink::{Connection, Route};
10//! use nlink::netlink::fdb::FdbEntryBuilder;
11//!
12//! let conn = Connection::<Route>::new()?;
13//!
14//! // List FDB entries for a bridge
15//! let entries = conn.get_fdb("br0").await?;
16//! for entry in &entries {
17//!     println!("{} vlan={:?}", entry.mac_str(), entry.vlan);
18//! }
19//!
20//! // Add a static FDB entry
21//! let mac = FdbEntryBuilder::parse_mac("aa:bb:cc:dd:ee:ff")?;
22//! conn.add_fdb(
23//!     FdbEntryBuilder::new(mac)
24//!         .dev("veth0")
25//!         .master("br0")
26//!         .permanent()
27//! ).await?;
28//!
29//! // Add VXLAN FDB entry (remote VTEP)
30//! use std::net::Ipv4Addr;
31//! conn.add_fdb(
32//!     FdbEntryBuilder::new([0x00; 6])  // all-zeros for BUM traffic
33//!         .dev("vxlan0")
34//!         .dst(Ipv4Addr::new(192, 168, 1, 100).into())
35//! ).await?;
36//!
37//! // Delete an entry
38//! conn.del_fdb("veth0", mac, None).await?;
39//! ```
40
41use std::net::IpAddr;
42
43use super::builder::MessageBuilder;
44use super::connection::Connection;
45use super::error::{Error, Result};
46use super::interface_ref::InterfaceRef;
47use super::message::{NLM_F_ACK, NLM_F_DUMP, NLM_F_REQUEST, NlMsgType};
48use super::messages::NeighborMessage;
49use super::protocol::Route;
50use super::types::neigh::{NdMsg, NdaAttr, NeighborState};
51
52/// NLM_F_CREATE flag
53const NLM_F_CREATE: u16 = 0x400;
54/// NLM_F_EXCL flag - fail if entry exists
55const NLM_F_EXCL: u16 = 0x200;
56/// NLM_F_REPLACE flag - replace existing entry
57const NLM_F_REPLACE: u16 = 0x100;
58
59/// AF_BRIDGE constant
60const AF_BRIDGE: u8 = 7;
61
62/// Neighbor flags for FDB entries.
63mod ntf {
64    /// Entry for the interface itself
65    pub const SELF: u8 = 0x02;
66    /// Entry for the master bridge
67    pub const MASTER: u8 = 0x04;
68    /// Externally learned entry
69    pub const EXT_LEARNED: u8 = 0x10;
70}
71
72/// Neighbor states
73mod nud {
74    /// Permanent (static) entry
75    pub const PERMANENT: u16 = 0x80;
76    /// Reachable (dynamic) entry
77    pub const REACHABLE: u16 = 0x02;
78}
79
80/// FDB entry information.
81///
82/// Represents a bridge forwarding database entry, containing MAC address
83/// to port mappings, optional VLAN information, and VXLAN remote endpoint
84/// data.
85#[derive(Debug, Clone)]
86pub struct FdbEntry {
87    /// Interface index (bridge port)
88    pub ifindex: u32,
89    /// MAC address (6 bytes)
90    pub mac: [u8; 6],
91    /// VLAN ID (if VLAN filtering is enabled)
92    pub vlan: Option<u16>,
93    /// Destination IP (for VXLAN remote VTEP)
94    pub dst: Option<IpAddr>,
95    /// VNI (for VXLAN)
96    pub vni: Option<u32>,
97    /// Entry state (permanent, reachable, etc.)
98    pub state: NeighborState,
99    /// Entry flags (NTF_SELF, NTF_MASTER, etc.)
100    pub flags: u8,
101    /// Master device index (bridge interface)
102    pub master: Option<u32>,
103}
104
105impl FdbEntry {
106    /// Create from a NeighborMessage.
107    ///
108    /// Returns `None` if the message doesn't have a valid MAC address.
109    pub fn from_neighbor(msg: &NeighborMessage) -> Option<Self> {
110        let lladdr = msg.lladdr()?;
111        if lladdr.len() != 6 {
112            return None;
113        }
114
115        let mut mac = [0u8; 6];
116        mac.copy_from_slice(lladdr);
117
118        Some(Self {
119            ifindex: msg.ifindex(),
120            mac,
121            vlan: msg.vlan(),
122            dst: msg.destination().cloned(),
123            vni: msg.vni(),
124            state: msg.state(),
125            flags: msg.flags(),
126            master: msg.master(),
127        })
128    }
129
130    /// Check if this is a permanent (static) entry.
131    pub fn is_permanent(&self) -> bool {
132        self.state == NeighborState::Permanent
133    }
134
135    /// Check if this is a dynamic (learned) entry.
136    pub fn is_dynamic(&self) -> bool {
137        !self.is_permanent()
138    }
139
140    /// Check if entry is for the interface itself (NTF_SELF).
141    pub fn is_self(&self) -> bool {
142        self.flags & ntf::SELF != 0
143    }
144
145    /// Check if entry is for the master bridge (NTF_MASTER).
146    pub fn is_master(&self) -> bool {
147        self.flags & ntf::MASTER != 0
148    }
149
150    /// Check if entry was externally learned (NTF_EXT_LEARNED).
151    pub fn is_extern_learn(&self) -> bool {
152        self.flags & ntf::EXT_LEARNED != 0
153    }
154
155    /// Format MAC address as a colon-separated hex string.
156    pub fn mac_str(&self) -> String {
157        format!(
158            "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
159            self.mac[0], self.mac[1], self.mac[2], self.mac[3], self.mac[4], self.mac[5]
160        )
161    }
162}
163
164/// Builder for FDB entries.
165///
166/// # Example
167///
168/// ```ignore
169/// use nlink::netlink::fdb::FdbEntryBuilder;
170/// use std::net::Ipv4Addr;
171///
172/// // Static entry on a bridge port
173/// let entry = FdbEntryBuilder::new([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff])
174///     .dev("veth0")
175///     .master("br0")
176///     .vlan(100)
177///     .permanent();
178///
179/// // VXLAN remote VTEP entry
180/// let vxlan_entry = FdbEntryBuilder::new([0x00; 6])
181///     .dev("vxlan0")
182///     .dst(Ipv4Addr::new(192, 168, 1, 100).into());
183/// ```
184#[derive(Debug, Clone, Default)]
185#[must_use = "builders do nothing unless used"]
186pub struct FdbEntryBuilder {
187    mac: [u8; 6],
188    dev: Option<InterfaceRef>,
189    vlan: Option<u16>,
190    dst: Option<IpAddr>,
191    vni: Option<u32>,
192    master: Option<InterfaceRef>,
193    permanent: bool,
194    self_flag: bool,
195}
196
197impl FdbEntryBuilder {
198    /// Create a new FDB entry builder with the given MAC address.
199    ///
200    /// By default, the entry is marked as permanent (static).
201    pub fn new(mac: [u8; 6]) -> Self {
202        Self {
203            mac,
204            permanent: true,
205            ..Default::default()
206        }
207    }
208
209    /// Parse a MAC address from a colon-separated hex string.
210    ///
211    /// # Example
212    ///
213    /// ```ignore
214    /// let mac = FdbEntryBuilder::parse_mac("aa:bb:cc:dd:ee:ff")?;
215    /// ```
216    pub fn parse_mac(mac_str: &str) -> Result<[u8; 6]> {
217        crate::util::addr::parse_mac(mac_str)
218            .map_err(|e| Error::InvalidMessage(format!("invalid MAC: {}", e)))
219    }
220
221    /// Set the device name (bridge port interface).
222    pub fn dev(mut self, dev: impl Into<String>) -> Self {
223        self.dev = Some(InterfaceRef::Name(dev.into()));
224        self
225    }
226
227    /// Set the interface index directly (namespace-safe).
228    ///
229    /// Use this instead of `dev()` when operating in a network namespace
230    /// to avoid reading `/sys/class/net/` from the wrong namespace.
231    pub fn ifindex(mut self, ifindex: u32) -> Self {
232        self.dev = Some(InterfaceRef::Index(ifindex));
233        self
234    }
235
236    /// Get the device reference.
237    pub fn device_ref(&self) -> Option<&InterfaceRef> {
238        self.dev.as_ref()
239    }
240
241    /// Set the VLAN ID.
242    ///
243    /// Only relevant for bridges with VLAN filtering enabled.
244    pub fn vlan(mut self, vlan: u16) -> Self {
245        self.vlan = Some(vlan);
246        self
247    }
248
249    /// Set the destination IP address (for VXLAN FDB entries).
250    ///
251    /// This specifies the remote VTEP IP address for VXLAN tunneling.
252    pub fn dst(mut self, dst: IpAddr) -> Self {
253        self.dst = Some(dst);
254        self
255    }
256
257    /// Set the VNI (for VXLAN FDB entries).
258    pub fn vni(mut self, vni: u32) -> Self {
259        self.vni = Some(vni);
260        self
261    }
262
263    /// Set the master bridge device by name.
264    pub fn master(mut self, master: impl Into<String>) -> Self {
265        self.master = Some(InterfaceRef::Name(master.into()));
266        self
267    }
268
269    /// Set the master bridge device by interface index (namespace-safe).
270    pub fn master_ifindex(mut self, ifindex: u32) -> Self {
271        self.master = Some(InterfaceRef::Index(ifindex));
272        self
273    }
274
275    /// Get the master device reference.
276    pub fn master_ref(&self) -> Option<&InterfaceRef> {
277        self.master.as_ref()
278    }
279
280    /// Mark entry as permanent (static). This is the default.
281    pub fn permanent(mut self) -> Self {
282        self.permanent = true;
283        self
284    }
285
286    /// Mark entry as dynamic (will age out).
287    pub fn dynamic(mut self) -> Self {
288        self.permanent = false;
289        self
290    }
291
292    /// Add to interface's own FDB (sets NTF_SELF flag).
293    ///
294    /// This is typically used for entries on the bridge port itself
295    /// rather than entries forwarded to the master bridge.
296    pub fn self_(mut self) -> Self {
297        self.self_flag = true;
298        self
299    }
300
301    /// Write the add message to the builder with resolved interface indices.
302    pub(crate) fn write_add(
303        &self,
304        builder: &mut MessageBuilder,
305        ifindex: u32,
306        master_idx: Option<u32>,
307    ) {
308        let state = if self.permanent {
309            nud::PERMANENT
310        } else {
311            nud::REACHABLE
312        };
313
314        let mut ntf_flags: u8 = 0;
315        if self.self_flag {
316            ntf_flags |= ntf::SELF;
317        }
318
319        let ndmsg = NdMsg::new()
320            .with_family(AF_BRIDGE)
321            .with_ifindex(ifindex as i32)
322            .with_state(state)
323            .with_flags(ntf_flags);
324
325        builder.append(&ndmsg);
326
327        // NDA_LLADDR - MAC address (required)
328        builder.append_attr(NdaAttr::Lladdr as u16, &self.mac);
329
330        // NDA_MASTER - bridge interface
331        if let Some(master) = master_idx {
332            builder.append_attr_u32(NdaAttr::Master as u16, master);
333        }
334
335        // NDA_VLAN
336        if let Some(vlan) = self.vlan {
337            builder.append_attr_u16(NdaAttr::Vlan as u16, vlan);
338        }
339
340        // NDA_DST - remote IP for VXLAN
341        if let Some(ref dst) = self.dst {
342            match dst {
343                IpAddr::V4(v4) => {
344                    builder.append_attr(NdaAttr::Dst as u16, &v4.octets());
345                }
346                IpAddr::V6(v6) => {
347                    builder.append_attr(NdaAttr::Dst as u16, &v6.octets());
348                }
349            }
350        }
351
352        // NDA_VNI
353        if let Some(vni) = self.vni {
354            builder.append_attr_u32(NdaAttr::Vni as u16, vni);
355        }
356    }
357
358    /// Write the delete message to the builder with resolved interface index.
359    pub(crate) fn write_delete(&self, builder: &mut MessageBuilder, ifindex: u32) {
360        let ndmsg = NdMsg::new()
361            .with_family(AF_BRIDGE)
362            .with_ifindex(ifindex as i32);
363
364        builder.append(&ndmsg);
365
366        // NDA_LLADDR - MAC address (required for delete)
367        builder.append_attr(NdaAttr::Lladdr as u16, &self.mac);
368
369        // NDA_VLAN - needed if VLAN filtering is enabled
370        if let Some(vlan) = self.vlan {
371            builder.append_attr_u16(NdaAttr::Vlan as u16, vlan);
372        }
373    }
374}
375
376// ============================================================================
377// Connection Methods
378// ============================================================================
379
380impl Connection<Route> {
381    /// Get all FDB entries for a bridge.
382    ///
383    /// Returns entries where the master device matches the specified bridge,
384    /// or entries directly on the bridge interface itself.
385    ///
386    /// # Example
387    ///
388    /// ```ignore
389    /// let entries = conn.get_fdb("br0").await?;
390    /// for entry in &entries {
391    ///     println!("{} on ifindex {} vlan={:?}",
392    ///         entry.mac_str(), entry.ifindex, entry.vlan);
393    /// }
394    /// ```
395    pub async fn get_fdb(&self, bridge: impl Into<InterfaceRef>) -> Result<Vec<FdbEntry>> {
396        let bridge_idx = self.resolve_interface(&bridge.into()).await?;
397        self.get_fdb_by_index(bridge_idx).await
398    }
399
400    /// Get all FDB entries for a bridge by interface index.
401    ///
402    /// Use this method when operating in a network namespace to avoid
403    /// reading `/sys/class/net/` from the wrong namespace.
404    pub async fn get_fdb_by_index(&self, bridge_idx: u32) -> Result<Vec<FdbEntry>> {
405        // Query neighbors with AF_BRIDGE family to get FDB entries
406        let neighbors = self.get_bridge_neighbors().await?;
407
408        Ok(neighbors
409            .iter()
410            .filter(|n| n.master() == Some(bridge_idx) || n.ifindex() == bridge_idx)
411            .filter_map(FdbEntry::from_neighbor)
412            .collect())
413    }
414
415    /// Get all bridge neighbor entries (AF_BRIDGE FDB dump).
416    async fn get_bridge_neighbors(&self) -> Result<Vec<NeighborMessage>> {
417        use super::message::NLMSG_HDRLEN;
418        use super::parse::FromNetlink;
419
420        let ndmsg = NdMsg::new().with_family(AF_BRIDGE);
421        let mut builder = MessageBuilder::new(NlMsgType::RTM_GETNEIGH, NLM_F_REQUEST | NLM_F_DUMP);
422        builder.append(&ndmsg);
423
424        let responses = self.send_dump(builder).await?;
425
426        let mut parsed = Vec::new();
427        for response in responses {
428            if response.len() < NLMSG_HDRLEN {
429                continue;
430            }
431            let payload = &response[NLMSG_HDRLEN..];
432            if let Ok(msg) = NeighborMessage::from_bytes(payload) {
433                parsed.push(msg);
434            }
435        }
436        Ok(parsed)
437    }
438
439    /// Get FDB entries for a specific bridge port.
440    ///
441    /// # Example
442    ///
443    /// ```ignore
444    /// let entries = conn.get_fdb_for_port("br0", "veth0").await?;
445    /// ```
446    pub async fn get_fdb_for_port(
447        &self,
448        bridge: impl Into<InterfaceRef>,
449        port: impl Into<InterfaceRef>,
450    ) -> Result<Vec<FdbEntry>> {
451        let bridge_idx = self.resolve_interface(&bridge.into()).await?;
452        let port_idx = self.resolve_interface(&port.into()).await?;
453
454        let neighbors = self.get_bridge_neighbors().await?;
455
456        Ok(neighbors
457            .iter()
458            .filter(|n| n.ifindex() == port_idx)
459            .filter(|n| n.master() == Some(bridge_idx))
460            .filter_map(FdbEntry::from_neighbor)
461            .collect())
462    }
463
464    /// Resolve FDB entry interface references.
465    async fn resolve_fdb_interfaces(&self, entry: &FdbEntryBuilder) -> Result<(u32, Option<u32>)> {
466        let ifindex = match entry.device_ref() {
467            Some(iface) => self.resolve_interface(iface).await?,
468            None => {
469                return Err(Error::InvalidMessage(
470                    "device name or ifindex required".into(),
471                ));
472            }
473        };
474
475        let master_idx = match entry.master_ref() {
476            Some(iface) => Some(self.resolve_interface(iface).await?),
477            None => None,
478        };
479
480        Ok((ifindex, master_idx))
481    }
482
483    /// Add an FDB entry.
484    ///
485    /// # Example
486    ///
487    /// ```ignore
488    /// use nlink::netlink::fdb::FdbEntryBuilder;
489    ///
490    /// let mac = FdbEntryBuilder::parse_mac("aa:bb:cc:dd:ee:ff")?;
491    /// conn.add_fdb(
492    ///     FdbEntryBuilder::new(mac)
493    ///         .dev("veth0")
494    ///         .master("br0")
495    ///         .vlan(100)
496    /// ).await?;
497    ///
498    /// // Namespace-safe version using interface index
499    /// conn.add_fdb(
500    ///     FdbEntryBuilder::new(mac)
501    ///         .ifindex(5)
502    ///         .master_ifindex(3)
503    /// ).await?;
504    /// ```
505    pub async fn add_fdb(&self, entry: FdbEntryBuilder) -> Result<()> {
506        let (ifindex, master_idx) = self.resolve_fdb_interfaces(&entry).await?;
507        let mut builder = MessageBuilder::new(
508            NlMsgType::RTM_NEWNEIGH,
509            NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL,
510        );
511        entry.write_add(&mut builder, ifindex, master_idx);
512        self.send_ack(builder)
513            .await
514            .map_err(|e| e.with_context("add_fdb"))
515    }
516
517    /// Replace an FDB entry (add or update).
518    ///
519    /// If the entry exists, it will be updated. Otherwise, it will be created.
520    pub async fn replace_fdb(&self, entry: FdbEntryBuilder) -> Result<()> {
521        let (ifindex, master_idx) = self.resolve_fdb_interfaces(&entry).await?;
522        let mut builder = MessageBuilder::new(
523            NlMsgType::RTM_NEWNEIGH,
524            NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_REPLACE,
525        );
526        entry.write_add(&mut builder, ifindex, master_idx);
527        self.send_ack(builder)
528            .await
529            .map_err(|e| e.with_context("replace_fdb"))
530    }
531
532    /// Delete an FDB entry by device name, MAC address, and optional VLAN.
533    ///
534    /// # Example
535    ///
536    /// ```ignore
537    /// // Delete entry without VLAN
538    /// conn.del_fdb("veth0", [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff], None).await?;
539    ///
540    /// // Delete entry with specific VLAN
541    /// conn.del_fdb("veth0", [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff], Some(100)).await?;
542    /// ```
543    pub async fn del_fdb(
544        &self,
545        dev: impl Into<InterfaceRef>,
546        mac: [u8; 6],
547        vlan: Option<u16>,
548    ) -> Result<()> {
549        let ifindex = self.resolve_interface(&dev.into()).await?;
550        self.del_fdb_by_index(ifindex, mac, vlan).await
551    }
552
553    /// Delete an FDB entry by interface index, MAC address, and optional VLAN.
554    ///
555    /// Use this method when operating in a network namespace.
556    pub async fn del_fdb_by_index(
557        &self,
558        ifindex: u32,
559        mac: [u8; 6],
560        vlan: Option<u16>,
561    ) -> Result<()> {
562        let mut entry = FdbEntryBuilder::new(mac).ifindex(ifindex);
563        if let Some(v) = vlan {
564            entry = entry.vlan(v);
565        }
566        let mut builder = MessageBuilder::new(NlMsgType::RTM_DELNEIGH, NLM_F_REQUEST | NLM_F_ACK);
567        entry.write_delete(&mut builder, ifindex);
568        self.send_ack(builder)
569            .await
570            .map_err(|e| e.with_context("del_fdb"))
571    }
572
573    /// Flush all dynamic FDB entries for a bridge.
574    ///
575    /// Permanent (static) entries are not removed.
576    ///
577    /// # Example
578    ///
579    /// ```ignore
580    /// conn.flush_fdb("br0").await?;
581    /// ```
582    pub async fn flush_fdb(&self, bridge: impl Into<InterfaceRef>) -> Result<()> {
583        let entries = self.get_fdb(bridge).await?;
584
585        for entry in entries {
586            // Only flush dynamic entries
587            if entry.is_dynamic()
588                && let Err(e) = self
589                    .del_fdb_by_index(entry.ifindex, entry.mac, entry.vlan)
590                    .await
591            {
592                // Ignore "not found" errors (race condition with aging)
593                if !e.is_not_found() {
594                    return Err(e);
595                }
596            }
597        }
598
599        Ok(())
600    }
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606
607    #[test]
608    fn test_parse_mac() {
609        let mac = FdbEntryBuilder::parse_mac("aa:bb:cc:dd:ee:ff").unwrap();
610        assert_eq!(mac, [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
611    }
612
613    #[test]
614    fn test_parse_mac_uppercase() {
615        let mac = FdbEntryBuilder::parse_mac("AA:BB:CC:DD:EE:FF").unwrap();
616        assert_eq!(mac, [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
617    }
618
619    #[test]
620    fn test_parse_mac_invalid() {
621        assert!(FdbEntryBuilder::parse_mac("invalid").is_err());
622        assert!(FdbEntryBuilder::parse_mac("aa:bb:cc:dd:ee").is_err());
623        assert!(FdbEntryBuilder::parse_mac("aa:bb:cc:dd:ee:ff:gg").is_err());
624    }
625
626    #[test]
627    fn test_fdb_entry_mac_str() {
628        let entry = FdbEntry {
629            ifindex: 1,
630            mac: [0x00, 0x11, 0x22, 0x33, 0x44, 0x55],
631            vlan: None,
632            dst: None,
633            vni: None,
634            state: NeighborState::Permanent,
635            flags: 0,
636            master: None,
637        };
638        assert_eq!(entry.mac_str(), "00:11:22:33:44:55");
639    }
640
641    #[test]
642    fn test_fdb_entry_flags() {
643        let entry = FdbEntry {
644            ifindex: 1,
645            mac: [0; 6],
646            vlan: None,
647            dst: None,
648            vni: None,
649            state: NeighborState::Permanent,
650            flags: ntf::SELF | ntf::MASTER,
651            master: None,
652        };
653        assert!(entry.is_self());
654        assert!(entry.is_master());
655        assert!(!entry.is_extern_learn());
656    }
657
658    #[test]
659    fn test_fdb_entry_permanent() {
660        let permanent = FdbEntry {
661            ifindex: 1,
662            mac: [0; 6],
663            vlan: None,
664            dst: None,
665            vni: None,
666            state: NeighborState::Permanent,
667            flags: 0,
668            master: None,
669        };
670        assert!(permanent.is_permanent());
671        assert!(!permanent.is_dynamic());
672
673        let dynamic = FdbEntry {
674            ifindex: 1,
675            mac: [0; 6],
676            vlan: None,
677            dst: None,
678            vni: None,
679            state: NeighborState::Reachable,
680            flags: 0,
681            master: None,
682        };
683        assert!(!dynamic.is_permanent());
684        assert!(dynamic.is_dynamic());
685    }
686
687    #[test]
688    fn test_builder_default() {
689        let builder = FdbEntryBuilder::new([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
690        assert!(builder.permanent); // default is permanent
691        assert!(!builder.self_flag);
692    }
693
694    #[test]
695    fn test_builder_chain() {
696        let builder = FdbEntryBuilder::new([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff])
697            .dev("veth0")
698            .master("br0")
699            .vlan(100)
700            .dynamic()
701            .self_();
702
703        assert_eq!(builder.dev, Some(InterfaceRef::Name("veth0".to_string())));
704        assert_eq!(builder.master, Some(InterfaceRef::Name("br0".to_string())));
705        assert_eq!(builder.vlan, Some(100));
706        assert!(!builder.permanent);
707        assert!(builder.self_flag);
708    }
709
710    #[test]
711    fn test_builder_ifindex() {
712        let builder = FdbEntryBuilder::new([0; 6]).ifindex(5).master_ifindex(3);
713
714        assert_eq!(builder.dev, Some(InterfaceRef::Index(5)));
715        assert_eq!(builder.master, Some(InterfaceRef::Index(3)));
716    }
717}