⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/net/
bonding.rs

1//! NIC Bonding / Link Aggregation
2//!
3//! Provides bond interfaces that aggregate multiple physical NICs for
4//! redundancy (active-backup) or load distribution (round-robin).
5//!
6//! Supports ARP monitoring for link health detection and automatic failover.
7
8#![allow(dead_code)]
9
10use alloc::{collections::BTreeMap, string::String, vec::Vec};
11
12use spin::RwLock;
13
14use crate::{error::KernelError, sync::once_lock::GlobalState};
15
16// ============================================================================
17// Bond Mode
18// ============================================================================
19
20/// Bond operating mode
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum BondMode {
23    /// Mode 0: distribute packets across slaves in round-robin order
24    RoundRobin,
25    /// Mode 1: only one slave active at a time, failover on link down
26    ActiveBackup,
27}
28
29// ============================================================================
30// Error Types
31// ============================================================================
32
33/// Errors returned by bonding operations
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum BondError {
36    /// Bond with the given name already exists
37    BondAlreadyExists,
38    /// Bond with the given name was not found
39    BondNotFound,
40    /// Slave with the given name already exists in the bond
41    SlaveAlreadyExists,
42    /// Slave with the given name was not found in the bond
43    SlaveNotFound,
44    /// No slaves available for transmission
45    NoSlavesAvailable,
46    /// Bond manager not initialized
47    NotInitialized,
48}
49
50impl From<BondError> for KernelError {
51    fn from(_e: BondError) -> Self {
52        KernelError::InvalidArgument {
53            name: "bonding",
54            value: "operation failed",
55        }
56    }
57}
58
59// ============================================================================
60// Bond Slave
61// ============================================================================
62
63/// A slave (member) interface within a bond
64#[derive(Debug, Clone)]
65pub struct BondSlave {
66    /// Interface name (e.g., "eth0")
67    pub name: String,
68    /// Hardware MAC address
69    pub mac_address: [u8; 6],
70    /// Whether the physical link is up
71    pub link_up: bool,
72    /// Whether this slave is the active transmitter (ActiveBackup mode)
73    pub is_active: bool,
74    /// Transmitted packet count
75    pub tx_packets: u64,
76    /// Received packet count
77    pub rx_packets: u64,
78    /// Last successful ARP response timestamp (ms)
79    pub last_arp_reply_ms: u64,
80}
81
82impl BondSlave {
83    /// Create a new slave with link initially up
84    pub fn new(name: &str, mac_address: [u8; 6]) -> Self {
85        Self {
86            name: String::from(name),
87            mac_address,
88            link_up: true,
89            is_active: false,
90            tx_packets: 0,
91            rx_packets: 0,
92            last_arp_reply_ms: 0,
93        }
94    }
95}
96
97// ============================================================================
98// ARP Monitor
99// ============================================================================
100
101/// ARP-based link health monitor
102#[derive(Debug, Clone)]
103pub struct ArpMonitor {
104    /// Monitoring interval in milliseconds
105    pub interval_ms: u64,
106    /// ARP probe target IP addresses
107    pub targets: Vec<[u8; 4]>,
108    /// Timestamp of the last ARP probe (ms)
109    pub last_check: u64,
110}
111
112impl ArpMonitor {
113    /// Create a new ARP monitor with the given interval
114    pub fn new(interval_ms: u64) -> Self {
115        Self {
116            interval_ms,
117            targets: Vec::new(),
118            last_check: 0,
119        }
120    }
121
122    /// Add an ARP probe target IP address
123    pub fn add_target(&mut self, ip: [u8; 4]) {
124        if !self.targets.contains(&ip) {
125            self.targets.push(ip);
126        }
127    }
128
129    /// Check whether the monitoring interval has elapsed
130    ///
131    /// Returns `true` if it is time to send ARP probes, and updates
132    /// the internal timestamp.
133    pub fn tick(&mut self, now: u64) -> bool {
134        if self.interval_ms == 0 {
135            return false;
136        }
137        if now.saturating_sub(self.last_check) >= self.interval_ms {
138            self.last_check = now;
139            true
140        } else {
141            false
142        }
143    }
144}
145
146// ============================================================================
147// Bond Interface
148// ============================================================================
149
150/// A bond (link aggregation) interface
151#[derive(Debug, Clone)]
152pub struct BondInterface {
153    /// Bond interface name (e.g., "bond0")
154    pub name: String,
155    /// Operating mode
156    pub mode: BondMode,
157    /// Member interfaces
158    pub slaves: Vec<BondSlave>,
159    /// Bond-level MAC address (set from first slave added)
160    pub mac_address: [u8; 6],
161    /// Index of the currently active slave (ActiveBackup mode)
162    pub active_slave_index: usize,
163    /// Round-robin counter for TX distribution
164    pub rr_counter: usize,
165    /// ARP health monitor
166    pub arp_monitor: ArpMonitor,
167}
168
169impl BondInterface {
170    /// Create a new bond interface
171    pub fn new(name: &str, mode: BondMode) -> Self {
172        Self {
173            name: String::from(name),
174            mode,
175            slaves: Vec::new(),
176            mac_address: [0u8; 6],
177            active_slave_index: 0,
178            rr_counter: 0,
179            arp_monitor: ArpMonitor::new(0),
180        }
181    }
182
183    /// Add a slave interface to this bond
184    pub fn add_slave(&mut self, slave_name: &str, mac: [u8; 6]) -> Result<(), BondError> {
185        // Check for duplicate
186        if self.slaves.iter().any(|s| s.name == slave_name) {
187            return Err(BondError::SlaveAlreadyExists);
188        }
189
190        let mut slave = BondSlave::new(slave_name, mac);
191
192        // First slave provides the bond MAC address
193        if self.slaves.is_empty() {
194            self.mac_address = mac;
195        }
196
197        // In ActiveBackup mode, activate first link-up slave if none active
198        if self.mode == BondMode::ActiveBackup && !self.has_active_slave() && slave.link_up {
199            slave.is_active = true;
200            self.active_slave_index = self.slaves.len();
201        }
202
203        // In RoundRobin mode, all slaves are effectively active
204        if self.mode == BondMode::RoundRobin {
205            slave.is_active = true;
206        }
207
208        self.slaves.push(slave);
209        Ok(())
210    }
211
212    /// Remove a slave interface from this bond
213    pub fn remove_slave(&mut self, slave_name: &str) -> Result<(), BondError> {
214        let idx = self
215            .slaves
216            .iter()
217            .position(|s| s.name == slave_name)
218            .ok_or(BondError::SlaveNotFound)?;
219
220        let was_active = self.slaves[idx].is_active;
221        self.slaves.remove(idx);
222
223        // Fix active_slave_index after removal
224        if self.active_slave_index >= self.slaves.len() && !self.slaves.is_empty() {
225            self.active_slave_index = 0;
226        }
227
228        // If we removed the active slave in ActiveBackup, promote another
229        if was_active && self.mode == BondMode::ActiveBackup {
230            self.promote_next_slave();
231        }
232
233        Ok(())
234    }
235
236    /// Select the slave index to use for the next TX packet
237    pub fn select_tx_slave(&mut self) -> Option<usize> {
238        if self.slaves.is_empty() {
239            return None;
240        }
241
242        match self.mode {
243            BondMode::ActiveBackup => {
244                // Use the active slave if it is link-up
245                if self.active_slave_index < self.slaves.len()
246                    && self.slaves[self.active_slave_index].link_up
247                {
248                    Some(self.active_slave_index)
249                } else {
250                    None
251                }
252            }
253            BondMode::RoundRobin => {
254                // Scan up to slaves.len() times to find a link-up slave
255                let count = self.slaves.len();
256                for _ in 0..count {
257                    let idx = self.rr_counter % count;
258                    self.rr_counter = self.rr_counter.wrapping_add(1);
259                    if self.slaves[idx].link_up {
260                        return Some(idx);
261                    }
262                }
263                None
264            }
265        }
266    }
267
268    /// Handle a link state change on a slave interface
269    pub fn handle_link_change(&mut self, slave_name: &str, link_up: bool) {
270        let Some(idx) = self.slaves.iter().position(|s| s.name == slave_name) else {
271            return;
272        };
273
274        self.slaves[idx].link_up = link_up;
275
276        match self.mode {
277            BondMode::ActiveBackup => {
278                if !link_up && self.slaves[idx].is_active {
279                    // Active slave went down -- failover
280                    self.slaves[idx].is_active = false;
281                    self.promote_next_slave();
282                } else if link_up && !self.has_active_slave() {
283                    // No active slave, promote this one
284                    self.slaves[idx].is_active = true;
285                    self.active_slave_index = idx;
286                }
287            }
288            BondMode::RoundRobin => {
289                // RoundRobin just skips downed slaves during selection
290                self.slaves[idx].is_active = link_up;
291            }
292        }
293    }
294
295    /// Returns `true` if any slave is currently marked active
296    fn has_active_slave(&self) -> bool {
297        self.slaves.iter().any(|s| s.is_active)
298    }
299
300    /// Promote the next link-up slave to active (ActiveBackup mode)
301    fn promote_next_slave(&mut self) {
302        for (i, slave) in self.slaves.iter_mut().enumerate() {
303            if slave.link_up {
304                slave.is_active = true;
305                self.active_slave_index = i;
306                return;
307            }
308        }
309        // No link-up slave found -- bond is fully down
310    }
311
312    /// Return the number of slaves with link up
313    pub fn link_up_count(&self) -> usize {
314        self.slaves.iter().filter(|s| s.link_up).count()
315    }
316}
317
318// ============================================================================
319// Bond Manager (global state)
320// ============================================================================
321
322/// Manages all bond interfaces on the system
323#[derive(Default)]
324pub struct BondManager {
325    /// Map of bond name -> BondInterface
326    pub bonds: BTreeMap<String, BondInterface>,
327}
328
329impl BondManager {
330    /// Create a new empty bond manager
331    pub fn new() -> Self {
332        Self::default()
333    }
334}
335
336/// Global bond manager state
337static BOND_MANAGER: GlobalState<RwLock<BondManager>> = GlobalState::new();
338
339/// Initialize the bond manager
340pub fn init() -> Result<(), KernelError> {
341    BOND_MANAGER
342        .init(RwLock::new(BondManager::new()))
343        .map_err(|_| KernelError::AlreadyExists {
344            resource: "bond_manager",
345            id: 0,
346        })?;
347    Ok(())
348}
349
350/// Create a new bond interface
351pub fn create_bond(name: &str, mode: BondMode) -> Result<(), BondError> {
352    BOND_MANAGER
353        .with(|lock| {
354            let mut mgr = lock.write();
355            if mgr.bonds.contains_key(name) {
356                return Err(BondError::BondAlreadyExists);
357            }
358            mgr.bonds
359                .insert(String::from(name), BondInterface::new(name, mode));
360            Ok(())
361        })
362        .unwrap_or(Err(BondError::NotInitialized))
363}
364
365/// Add a slave interface to an existing bond
366pub fn add_slave(bond_name: &str, slave_name: &str, mac: [u8; 6]) -> Result<(), BondError> {
367    BOND_MANAGER
368        .with(|lock| {
369            let mut mgr = lock.write();
370            let bond = mgr
371                .bonds
372                .get_mut(bond_name)
373                .ok_or(BondError::BondNotFound)?;
374            bond.add_slave(slave_name, mac)
375        })
376        .unwrap_or(Err(BondError::NotInitialized))
377}
378
379/// Remove a slave interface from a bond
380pub fn remove_slave(bond_name: &str, slave_name: &str) -> Result<(), BondError> {
381    BOND_MANAGER
382        .with(|lock| {
383            let mut mgr = lock.write();
384            let bond = mgr
385                .bonds
386                .get_mut(bond_name)
387                .ok_or(BondError::BondNotFound)?;
388            bond.remove_slave(slave_name)
389        })
390        .unwrap_or(Err(BondError::NotInitialized))
391}
392
393/// Select the next slave for transmission on a bond
394pub fn select_tx_slave(bond_name: &str) -> Option<usize> {
395    BOND_MANAGER
396        .with(|lock| {
397            let mut mgr = lock.write();
398            let bond = mgr.bonds.get_mut(bond_name)?;
399            bond.select_tx_slave()
400        })
401        .flatten()
402}
403
404/// Handle a link state change on a slave interface
405pub fn handle_link_change(slave_name: &str, link_up: bool) {
406    BOND_MANAGER.with(|lock| {
407        let mut mgr = lock.write();
408        for bond in mgr.bonds.values_mut() {
409            bond.handle_link_change(slave_name, link_up);
410        }
411    });
412}
413
414// ============================================================================
415// Tests
416// ============================================================================
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    fn make_mac(last: u8) -> [u8; 6] {
423        [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, last]
424    }
425
426    // --- BondInterface unit tests (no global state needed) ---
427
428    #[test]
429    fn test_create_bond_interface() {
430        let bond = BondInterface::new("bond0", BondMode::ActiveBackup);
431        assert_eq!(bond.name, "bond0");
432        assert_eq!(bond.mode, BondMode::ActiveBackup);
433        assert!(bond.slaves.is_empty());
434        assert_eq!(bond.mac_address, [0u8; 6]);
435    }
436
437    #[test]
438    fn test_add_slave_sets_mac() {
439        let mut bond = BondInterface::new("bond0", BondMode::ActiveBackup);
440        let mac = make_mac(0x01);
441        bond.add_slave("eth0", mac).unwrap();
442
443        assert_eq!(bond.slaves.len(), 1);
444        assert_eq!(bond.mac_address, mac);
445        assert!(bond.slaves[0].is_active); // first slave becomes active
446    }
447
448    #[test]
449    fn test_add_duplicate_slave_fails() {
450        let mut bond = BondInterface::new("bond0", BondMode::RoundRobin);
451        bond.add_slave("eth0", make_mac(0x01)).unwrap();
452        let result = bond.add_slave("eth0", make_mac(0x02));
453        assert_eq!(result, Err(BondError::SlaveAlreadyExists));
454    }
455
456    #[test]
457    fn test_remove_slave() {
458        let mut bond = BondInterface::new("bond0", BondMode::RoundRobin);
459        bond.add_slave("eth0", make_mac(0x01)).unwrap();
460        bond.add_slave("eth1", make_mac(0x02)).unwrap();
461        assert_eq!(bond.slaves.len(), 2);
462
463        bond.remove_slave("eth0").unwrap();
464        assert_eq!(bond.slaves.len(), 1);
465        assert_eq!(bond.slaves[0].name, "eth1");
466    }
467
468    #[test]
469    fn test_remove_nonexistent_slave_fails() {
470        let mut bond = BondInterface::new("bond0", BondMode::ActiveBackup);
471        let result = bond.remove_slave("eth99");
472        assert_eq!(result, Err(BondError::SlaveNotFound));
473    }
474
475    #[test]
476    fn test_active_backup_failover() {
477        let mut bond = BondInterface::new("bond0", BondMode::ActiveBackup);
478        bond.add_slave("eth0", make_mac(0x01)).unwrap();
479        bond.add_slave("eth1", make_mac(0x02)).unwrap();
480
481        // eth0 should be active
482        assert!(bond.slaves[0].is_active);
483        assert!(!bond.slaves[1].is_active);
484        assert_eq!(bond.active_slave_index, 0);
485
486        // Simulate eth0 link down
487        bond.handle_link_change("eth0", false);
488
489        // eth1 should now be active
490        assert!(!bond.slaves[0].is_active);
491        assert!(bond.slaves[1].is_active);
492        assert_eq!(bond.active_slave_index, 1);
493    }
494
495    #[test]
496    fn test_active_backup_select_tx() {
497        let mut bond = BondInterface::new("bond0", BondMode::ActiveBackup);
498        bond.add_slave("eth0", make_mac(0x01)).unwrap();
499        bond.add_slave("eth1", make_mac(0x02)).unwrap();
500
501        assert_eq!(bond.select_tx_slave(), Some(0));
502
503        // Down the active slave
504        bond.handle_link_change("eth0", false);
505        assert_eq!(bond.select_tx_slave(), Some(1));
506
507        // Down all slaves
508        bond.handle_link_change("eth1", false);
509        assert_eq!(bond.select_tx_slave(), None);
510    }
511
512    #[test]
513    fn test_round_robin_selection() {
514        let mut bond = BondInterface::new("bond0", BondMode::RoundRobin);
515        bond.add_slave("eth0", make_mac(0x01)).unwrap();
516        bond.add_slave("eth1", make_mac(0x02)).unwrap();
517        bond.add_slave("eth2", make_mac(0x03)).unwrap();
518
519        // Should cycle through 0, 1, 2, 0, 1, 2 ...
520        assert_eq!(bond.select_tx_slave(), Some(0));
521        assert_eq!(bond.select_tx_slave(), Some(1));
522        assert_eq!(bond.select_tx_slave(), Some(2));
523        assert_eq!(bond.select_tx_slave(), Some(0));
524    }
525
526    #[test]
527    fn test_round_robin_skips_down_slave() {
528        let mut bond = BondInterface::new("bond0", BondMode::RoundRobin);
529        bond.add_slave("eth0", make_mac(0x01)).unwrap();
530        bond.add_slave("eth1", make_mac(0x02)).unwrap();
531        bond.add_slave("eth2", make_mac(0x03)).unwrap();
532
533        // Down eth1
534        bond.handle_link_change("eth1", false);
535
536        // Should skip eth1
537        assert_eq!(bond.select_tx_slave(), Some(0));
538        assert_eq!(bond.select_tx_slave(), Some(2));
539        assert_eq!(bond.select_tx_slave(), Some(0));
540    }
541
542    #[test]
543    fn test_link_up_count() {
544        let mut bond = BondInterface::new("bond0", BondMode::RoundRobin);
545        bond.add_slave("eth0", make_mac(0x01)).unwrap();
546        bond.add_slave("eth1", make_mac(0x02)).unwrap();
547        assert_eq!(bond.link_up_count(), 2);
548
549        bond.handle_link_change("eth0", false);
550        assert_eq!(bond.link_up_count(), 1);
551    }
552
553    #[test]
554    fn test_arp_monitor_tick() {
555        let mut mon = ArpMonitor::new(1000);
556        mon.add_target([192, 168, 1, 1]);
557
558        // First tick at time 0 fires immediately (0 - 0 >= 1000 is false,
559        // but the very first tick with last_check=0 and now=0 won't fire)
560        assert!(!mon.tick(0));
561        assert!(!mon.tick(500));
562        assert!(mon.tick(1000));
563        // After firing, last_check is updated to 1000
564        assert!(!mon.tick(1500));
565        assert!(mon.tick(2000));
566    }
567
568    #[test]
569    fn test_arp_monitor_zero_interval() {
570        let mut mon = ArpMonitor::new(0);
571        // Zero interval means monitoring is disabled
572        assert!(!mon.tick(0));
573        assert!(!mon.tick(1000));
574    }
575
576    #[test]
577    fn test_arp_monitor_no_duplicate_targets() {
578        let mut mon = ArpMonitor::new(1000);
579        mon.add_target([10, 0, 0, 1]);
580        mon.add_target([10, 0, 0, 1]);
581        assert_eq!(mon.targets.len(), 1);
582    }
583
584    #[test]
585    fn test_remove_active_slave_promotes_next() {
586        let mut bond = BondInterface::new("bond0", BondMode::ActiveBackup);
587        bond.add_slave("eth0", make_mac(0x01)).unwrap();
588        bond.add_slave("eth1", make_mac(0x02)).unwrap();
589
590        // eth0 is active
591        assert!(bond.slaves[0].is_active);
592
593        // Remove active slave
594        bond.remove_slave("eth0").unwrap();
595        assert_eq!(bond.slaves.len(), 1);
596        assert_eq!(bond.slaves[0].name, "eth1");
597        assert!(bond.slaves[0].is_active);
598    }
599
600    #[test]
601    fn test_all_slaves_down_then_recovery() {
602        let mut bond = BondInterface::new("bond0", BondMode::ActiveBackup);
603        bond.add_slave("eth0", make_mac(0x01)).unwrap();
604        bond.add_slave("eth1", make_mac(0x02)).unwrap();
605
606        // Down both
607        bond.handle_link_change("eth0", false);
608        bond.handle_link_change("eth1", false);
609        assert_eq!(bond.select_tx_slave(), None);
610        assert!(!bond.has_active_slave());
611
612        // Bring eth1 back up
613        bond.handle_link_change("eth1", true);
614        assert!(bond.slaves[1].is_active);
615        assert_eq!(bond.select_tx_slave(), Some(1));
616    }
617}