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

veridian_kernel/net/
ipv6.rs

1//! IPv6 protocol implementation
2//!
3//! Provides IPv6 packet construction, parsing, address utilities, NDP (Neighbor
4//! Discovery Protocol) cache management, and dual-stack configuration for
5//! simultaneous IPv4/IPv6 networking.
6
7#![allow(dead_code)] // Phase 7 network stack -- functions called as stack matures
8
9use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
10use core::sync::atomic::{AtomicU64, Ordering};
11
12use spin::RwLock;
13
14use super::{Ipv6Address, MacAddress};
15use crate::{error::KernelError, sync::once_lock::GlobalState};
16
17// ============================================================================
18// Constants
19// ============================================================================
20
21/// IPv6 header size in bytes (fixed, unlike IPv4)
22pub const IPV6_HEADER_SIZE: usize = 40;
23
24/// IPv6 version number
25pub const IPV6_VERSION: u8 = 6;
26
27/// Default hop limit (equivalent to IPv4 TTL)
28pub const DEFAULT_HOP_LIMIT: u8 = 64;
29
30/// IPv6 minimum MTU (all links must support at least 1280 bytes)
31pub const IPV6_MIN_MTU: usize = 1280;
32
33/// Next header protocol numbers
34pub const NEXT_HEADER_HOP_BY_HOP: u8 = 0;
35pub const NEXT_HEADER_TCP: u8 = 6;
36pub const NEXT_HEADER_UDP: u8 = 17;
37pub const NEXT_HEADER_ICMPV6: u8 = 58;
38pub const NEXT_HEADER_NO_NEXT: u8 = 59;
39pub const NEXT_HEADER_FRAGMENT: u8 = 44;
40
41/// ICMPv6 NDP message types
42pub const ICMPV6_ROUTER_SOLICIT: u8 = 133;
43pub const ICMPV6_ROUTER_ADVERT: u8 = 134;
44pub const ICMPV6_NEIGHBOR_SOLICIT: u8 = 135;
45pub const ICMPV6_NEIGHBOR_ADVERT: u8 = 136;
46pub const ICMPV6_REDIRECT: u8 = 137;
47
48/// NDP option types
49pub const NDP_OPT_SOURCE_LINK_ADDR: u8 = 1;
50pub const NDP_OPT_TARGET_LINK_ADDR: u8 = 2;
51pub const NDP_OPT_PREFIX_INFO: u8 = 3;
52pub const NDP_OPT_MTU: u8 = 5;
53
54/// NDP cache limits
55const NDP_CACHE_MAX: usize = 128;
56
57/// NDP entry max age in ticks (approximately 30 seconds in REACHABLE state)
58const NDP_REACHABLE_TIME: u64 = 30;
59
60/// NDP stale entry timeout in ticks (approximately 10 minutes)
61const NDP_STALE_TIMEOUT: u64 = 600;
62
63/// EtherType for IPv6
64pub const ETHERTYPE_IPV6: u16 = 0x86DD;
65
66// ============================================================================
67// IPv6 Header
68// ============================================================================
69
70/// IPv6 packet header (40 bytes fixed)
71///
72/// Layout:
73///   - version_tc_flow (4 bytes): 4-bit version, 8-bit traffic class, 20-bit
74///     flow label
75///   - payload_length (2 bytes): length of payload after this header
76///     (big-endian)
77///   - next_header (1 byte): identifies the type of header immediately
78///     following
79///   - hop_limit (1 byte): decremented by 1 at each forwarding node
80///   - source (16 bytes): source IPv6 address
81///   - destination (16 bytes): destination IPv6 address
82#[derive(Debug, Clone, Copy)]
83#[repr(C)]
84pub struct Ipv6Header {
85    /// Version (4 bits), Traffic Class (8 bits), Flow Label (20 bits) --
86    /// network byte order
87    pub version_tc_flow: u32,
88    /// Payload length in bytes (big-endian, does not include this header)
89    pub payload_length: u16,
90    /// Next header protocol number (TCP=6, UDP=17, ICMPv6=58)
91    pub next_header: u8,
92    /// Hop limit (TTL equivalent)
93    pub hop_limit: u8,
94    /// Source IPv6 address (16 bytes)
95    pub source: [u8; 16],
96    /// Destination IPv6 address (16 bytes)
97    pub destination: [u8; 16],
98}
99
100impl Ipv6Header {
101    /// Create a new IPv6 header with default values
102    pub fn new(src: &Ipv6Address, dst: &Ipv6Address, next_header: u8) -> Self {
103        // version=6, traffic_class=0, flow_label=0
104        let version_tc_flow: u32 = (IPV6_VERSION as u32) << 28;
105
106        Self {
107            version_tc_flow,
108            payload_length: 0,
109            next_header,
110            hop_limit: DEFAULT_HOP_LIMIT,
111            source: src.0,
112            destination: dst.0,
113        }
114    }
115
116    /// Get the IP version field (should always be 6)
117    pub fn version(&self) -> u8 {
118        ((self.version_tc_flow >> 28) & 0x0F) as u8
119    }
120
121    /// Get the traffic class field
122    pub fn traffic_class(&self) -> u8 {
123        ((self.version_tc_flow >> 20) & 0xFF) as u8
124    }
125
126    /// Get the flow label field
127    pub fn flow_label(&self) -> u32 {
128        self.version_tc_flow & 0x000F_FFFF
129    }
130
131    /// Serialize header to bytes (big-endian)
132    pub fn to_bytes(&self) -> [u8; IPV6_HEADER_SIZE] {
133        let mut bytes = [0u8; IPV6_HEADER_SIZE];
134
135        // Version + Traffic Class + Flow Label (4 bytes, big-endian)
136        let vtf_be = self.version_tc_flow.to_be_bytes();
137        bytes[0..4].copy_from_slice(&vtf_be);
138
139        // Payload Length (2 bytes, big-endian)
140        bytes[4..6].copy_from_slice(&self.payload_length.to_be_bytes());
141
142        // Next Header
143        bytes[6] = self.next_header;
144
145        // Hop Limit
146        bytes[7] = self.hop_limit;
147
148        // Source address (16 bytes)
149        bytes[8..24].copy_from_slice(&self.source);
150
151        // Destination address (16 bytes)
152        bytes[24..40].copy_from_slice(&self.destination);
153
154        bytes
155    }
156}
157
158// ============================================================================
159// IPv6 Packet Parsing and Construction
160// ============================================================================
161
162/// Parse an IPv6 packet from raw bytes.
163///
164/// Returns the parsed header and a slice of the payload data.
165pub fn parse_ipv6(data: &[u8]) -> Result<(Ipv6Header, &[u8]), KernelError> {
166    if data.len() < IPV6_HEADER_SIZE {
167        return Err(KernelError::InvalidArgument {
168            name: "ipv6_packet",
169            value: "too_short",
170        });
171    }
172
173    // Parse version/traffic class/flow label (4 bytes, big-endian)
174    let version_tc_flow = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
175
176    let version = (version_tc_flow >> 28) as u8;
177    if version != IPV6_VERSION {
178        return Err(KernelError::InvalidArgument {
179            name: "ipv6_version",
180            value: "not_ipv6",
181        });
182    }
183
184    let payload_length = u16::from_be_bytes([data[4], data[5]]);
185    let next_header = data[6];
186    let hop_limit = data[7];
187
188    let mut source = [0u8; 16];
189    let mut destination = [0u8; 16];
190    source.copy_from_slice(&data[8..24]);
191    destination.copy_from_slice(&data[24..40]);
192
193    let header = Ipv6Header {
194        version_tc_flow,
195        payload_length,
196        next_header,
197        hop_limit,
198        source,
199        destination,
200    };
201
202    // Extract payload (bounded by payload_length and available data)
203    let payload_end = IPV6_HEADER_SIZE + (payload_length as usize);
204    let actual_end = payload_end.min(data.len());
205    let payload = &data[IPV6_HEADER_SIZE..actual_end];
206
207    Ok((header, payload))
208}
209
210/// Build an IPv6 packet with the given parameters.
211///
212/// Returns a complete IPv6 packet (header + payload) as a `Vec<u8>`.
213pub fn build_ipv6(
214    src: &Ipv6Address,
215    dst: &Ipv6Address,
216    next_header: u8,
217    payload: &[u8],
218) -> Vec<u8> {
219    let mut header = Ipv6Header::new(src, dst, next_header);
220    header.payload_length = payload.len() as u16;
221
222    let header_bytes = header.to_bytes();
223    let mut packet = Vec::with_capacity(IPV6_HEADER_SIZE + payload.len());
224    packet.extend_from_slice(&header_bytes);
225    packet.extend_from_slice(payload);
226
227    packet
228}
229
230// ============================================================================
231// IPv6 Address Utilities
232// ============================================================================
233
234/// Check if an IPv6 address is link-local (fe80::/10)
235pub fn is_link_local(addr: &Ipv6Address) -> bool {
236    addr.0[0] == 0xfe && (addr.0[1] & 0xc0) == 0x80
237}
238
239/// Check if an IPv6 address is multicast (ff00::/8)
240pub fn is_multicast(addr: &Ipv6Address) -> bool {
241    addr.0[0] == 0xff
242}
243
244/// Check if an IPv6 address is the loopback address (::1)
245pub fn is_loopback(addr: &Ipv6Address) -> bool {
246    *addr == Ipv6Address::LOCALHOST
247}
248
249/// Check if an IPv6 address is the unspecified address (::)
250pub fn is_unspecified(addr: &Ipv6Address) -> bool {
251    *addr == Ipv6Address::UNSPECIFIED
252}
253
254/// Check if an IPv6 address is a global unicast address (2000::/3)
255pub fn is_global_unicast(addr: &Ipv6Address) -> bool {
256    (addr.0[0] & 0xe0) == 0x20
257}
258
259/// Check if an IPv6 address is a unique local address (fc00::/7)
260pub fn is_unique_local(addr: &Ipv6Address) -> bool {
261    (addr.0[0] & 0xfe) == 0xfc
262}
263
264/// Check if an IPv6 address is an IPv4-mapped IPv6 address (::ffff:0:0/96)
265pub fn is_ipv4_mapped(addr: &Ipv6Address) -> bool {
266    addr.0[0..10] == [0; 10] && addr.0[10] == 0xff && addr.0[11] == 0xff
267}
268
269/// Compute the solicited-node multicast address for a given unicast address.
270///
271/// Format: ff02::1:ffXX:XXXX where XX:XXXX are the last 3 bytes of the unicast
272/// address.
273pub fn solicited_node_multicast(addr: &Ipv6Address) -> Ipv6Address {
274    Ipv6Address([
275        0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, addr.0[13],
276        addr.0[14], addr.0[15],
277    ])
278}
279
280/// Generate a link-local address from a MAC address using EUI-64.
281///
282/// Converts a 48-bit MAC to a 64-bit interface ID by inserting FF:FE
283/// and flipping the universal/local bit, then prepending fe80::/10.
284pub fn link_local_from_mac(mac: &MacAddress) -> Ipv6Address {
285    let mut addr = [0u8; 16];
286
287    // fe80::/10 prefix
288    addr[0] = 0xfe;
289    addr[1] = 0x80;
290    // bytes 2..7 are zero (padding)
291
292    // EUI-64: insert FF:FE in the middle of the MAC, flip U/L bit
293    addr[8] = mac.0[0] ^ 0x02; // flip universal/local bit
294    addr[9] = mac.0[1];
295    addr[10] = mac.0[2];
296    addr[11] = 0xff;
297    addr[12] = 0xfe;
298    addr[13] = mac.0[3];
299    addr[14] = mac.0[4];
300    addr[15] = mac.0[5];
301
302    Ipv6Address(addr)
303}
304
305/// Format an IPv6 address as a human-readable string.
306///
307/// Produces the colon-hex notation (e.g., "fe80:0:0:0:200:ff:fe00:1").
308/// Does not apply zero-compression (::) for simplicity.
309pub fn format_ipv6(addr: &Ipv6Address) -> String {
310    let b = &addr.0;
311    format!(
312        "{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x}",
313        u16::from_be_bytes([b[0], b[1]]),
314        u16::from_be_bytes([b[2], b[3]]),
315        u16::from_be_bytes([b[4], b[5]]),
316        u16::from_be_bytes([b[6], b[7]]),
317        u16::from_be_bytes([b[8], b[9]]),
318        u16::from_be_bytes([b[10], b[11]]),
319        u16::from_be_bytes([b[12], b[13]]),
320        u16::from_be_bytes([b[14], b[15]]),
321    )
322}
323
324/// Format an IPv6 address with zero-compression (::).
325///
326/// Finds the longest run of consecutive zero groups and replaces it with "::".
327pub fn format_ipv6_compressed(addr: &Ipv6Address) -> String {
328    let b = &addr.0;
329    let groups: [u16; 8] = [
330        u16::from_be_bytes([b[0], b[1]]),
331        u16::from_be_bytes([b[2], b[3]]),
332        u16::from_be_bytes([b[4], b[5]]),
333        u16::from_be_bytes([b[6], b[7]]),
334        u16::from_be_bytes([b[8], b[9]]),
335        u16::from_be_bytes([b[10], b[11]]),
336        u16::from_be_bytes([b[12], b[13]]),
337        u16::from_be_bytes([b[14], b[15]]),
338    ];
339
340    // Find the longest run of consecutive zero groups
341    let mut best_start = 0usize;
342    let mut best_len = 0usize;
343    let mut cur_start = 0usize;
344    let mut cur_len = 0usize;
345
346    for (i, &group) in groups.iter().enumerate() {
347        if group == 0 {
348            if cur_len == 0 {
349                cur_start = i;
350            }
351            cur_len += 1;
352            if cur_len > best_len {
353                best_start = cur_start;
354                best_len = cur_len;
355            }
356        } else {
357            cur_len = 0;
358        }
359    }
360
361    // No compression worthwhile if fewer than 2 consecutive zero groups
362    if best_len < 2 {
363        return format_ipv6(addr);
364    }
365
366    let mut parts: Vec<String> = Vec::new();
367    let mut i = 0usize;
368    let mut compressed = false;
369
370    while i < 8 {
371        if i == best_start && !compressed {
372            // Insert the :: marker
373            if i == 0 {
374                parts.push(String::new()); // leading empty for ::
375            }
376            parts.push(String::new()); // the :: itself
377            if best_start + best_len == 8 {
378                parts.push(String::new()); // trailing empty for ::
379            }
380            i += best_len;
381            compressed = true;
382        } else {
383            parts.push(format!("{:x}", groups[i]));
384            i += 1;
385        }
386    }
387
388    parts.join(":")
389}
390
391/// Convert a multicast IPv6 address to its Ethernet multicast MAC address.
392///
393/// Multicast MAC = 33:33:XX:XX:XX:XX where XX are the low 4 bytes of the IPv6
394/// address.
395pub fn multicast_mac(addr: &Ipv6Address) -> MacAddress {
396    MacAddress([0x33, 0x33, addr.0[12], addr.0[13], addr.0[14], addr.0[15]])
397}
398
399// ============================================================================
400// NDP (Neighbor Discovery Protocol)
401// ============================================================================
402
403/// NDP neighbor cache entry state (RFC 4861 Section 7.3.2)
404#[derive(Debug, Clone, Copy, PartialEq, Eq)]
405pub enum NdpState {
406    /// Address resolution is in progress; waiting for NA response
407    Incomplete,
408    /// Positive confirmation that the path is working
409    Reachable,
410    /// No positive confirmation received recently; still usable
411    Stale,
412    /// Stale entry waiting for upper-layer reachability confirmation
413    Delay,
414    /// Actively probing with Neighbor Solicitation messages
415    Probe,
416}
417
418/// NDP neighbor cache entry
419#[derive(Debug, Clone)]
420pub struct NdpEntry {
421    /// Resolved MAC address (may be unset in Incomplete state)
422    pub mac: MacAddress,
423    /// Current state of this entry
424    pub state: NdpState,
425    /// Tick count when this entry was created or last confirmed
426    pub timestamp: u64,
427    /// Number of NS probes sent (for Incomplete/Probe states)
428    pub probe_count: u8,
429}
430
431/// NDP neighbor cache
432pub struct NdpCache {
433    entries: BTreeMap<Ipv6Address, NdpEntry>,
434}
435
436impl Default for NdpCache {
437    fn default() -> Self {
438        Self::new()
439    }
440}
441
442impl NdpCache {
443    /// Create a new empty NDP cache
444    pub const fn new() -> Self {
445        Self {
446            entries: BTreeMap::new(),
447        }
448    }
449
450    /// Look up a neighbor's MAC address.
451    ///
452    /// Returns `Some(MacAddress)` if the entry is in Reachable or Stale state.
453    pub fn lookup(&self, addr: &Ipv6Address) -> Option<MacAddress> {
454        if let Some(entry) = self.entries.get(addr) {
455            match entry.state {
456                NdpState::Reachable | NdpState::Stale | NdpState::Delay | NdpState::Probe => {
457                    Some(entry.mac)
458                }
459                NdpState::Incomplete => None,
460            }
461        } else {
462            None
463        }
464    }
465
466    /// Insert or update a neighbor cache entry.
467    pub fn update(&mut self, addr: Ipv6Address, mac: MacAddress, state: NdpState) {
468        let now = current_tick();
469
470        // Evict oldest if at capacity
471        if self.entries.len() >= NDP_CACHE_MAX && !self.entries.contains_key(&addr) {
472            let oldest_key = self
473                .entries
474                .iter()
475                .min_by_key(|(_, e)| e.timestamp)
476                .map(|(k, _)| *k);
477            if let Some(key) = oldest_key {
478                self.entries.remove(&key);
479            }
480        }
481
482        self.entries.insert(
483            addr,
484            NdpEntry {
485                mac,
486                state,
487                timestamp: now,
488                probe_count: 0,
489            },
490        );
491    }
492
493    /// Mark an entry as Incomplete (address resolution started).
494    pub fn mark_incomplete(&mut self, addr: Ipv6Address) {
495        let now = current_tick();
496        self.entries.insert(
497            addr,
498            NdpEntry {
499                mac: MacAddress::ZERO,
500                state: NdpState::Incomplete,
501                timestamp: now,
502                probe_count: 1,
503            },
504        );
505    }
506
507    /// Transition a Reachable entry to Stale if its reachable time has expired.
508    pub fn age_entries(&mut self) {
509        let now = current_tick();
510        for entry in self.entries.values_mut() {
511            match entry.state {
512                NdpState::Reachable => {
513                    if now.wrapping_sub(entry.timestamp) > NDP_REACHABLE_TIME {
514                        entry.state = NdpState::Stale;
515                    }
516                }
517                NdpState::Stale => {
518                    if now.wrapping_sub(entry.timestamp) > NDP_STALE_TIMEOUT {
519                        entry.state = NdpState::Incomplete;
520                    }
521                }
522                _ => {}
523            }
524        }
525    }
526
527    /// Remove an entry from the cache.
528    pub fn remove(&mut self, addr: &Ipv6Address) {
529        self.entries.remove(addr);
530    }
531
532    /// Get all cache entries for display.
533    pub fn get_entries(&self) -> Vec<(Ipv6Address, MacAddress, NdpState)> {
534        self.entries
535            .iter()
536            .map(|(addr, entry)| (*addr, entry.mac, entry.state))
537            .collect()
538    }
539
540    /// Clear the entire cache.
541    pub fn flush(&mut self) {
542        self.entries.clear();
543    }
544
545    /// Number of entries in the cache.
546    pub fn len(&self) -> usize {
547        self.entries.len()
548    }
549
550    /// Check if the cache is empty.
551    pub fn is_empty(&self) -> bool {
552        self.entries.is_empty()
553    }
554}
555
556// ============================================================================
557// NDP Message Construction
558// ============================================================================
559
560/// Build a Neighbor Solicitation message.
561///
562/// Used to discover the link-layer address of a neighbor or verify
563/// reachability. Includes the ICMPv6 header (type 135) + target address +
564/// source link-layer option.
565pub fn ndp_solicit(src: &Ipv6Address, target: &Ipv6Address, src_mac: &MacAddress) -> Vec<u8> {
566    // ICMPv6 Neighbor Solicitation:
567    //   Type (1) + Code (1) + Checksum (2) + Reserved (4) + Target Address (16)
568    //   + Source Link-Layer Address Option (8)
569    let mut msg = Vec::with_capacity(32);
570
571    // ICMPv6 header
572    msg.push(ICMPV6_NEIGHBOR_SOLICIT); // Type
573    msg.push(0); // Code
574    msg.extend_from_slice(&[0u8; 2]); // Checksum (filled later)
575    msg.extend_from_slice(&[0u8; 4]); // Reserved
576
577    // Target address
578    msg.extend_from_slice(&target.0);
579
580    // Source Link-Layer Address option (Type=1, Length=1 (in units of 8 bytes))
581    msg.push(NDP_OPT_SOURCE_LINK_ADDR);
582    msg.push(1); // Length in 8-byte units
583    msg.extend_from_slice(&src_mac.0);
584
585    // Compute and fill checksum
586    let dst = solicited_node_multicast(target);
587    let checksum = compute_icmpv6_checksum(&src.0, &dst.0, &msg);
588    msg[2] = (checksum >> 8) as u8;
589    msg[3] = (checksum & 0xff) as u8;
590
591    msg
592}
593
594/// Build a Neighbor Advertisement message.
595///
596/// Sent in response to a Neighbor Solicitation, or unsolicited to announce
597/// changes.
598pub fn ndp_advertise(
599    src: &Ipv6Address,
600    dst: &Ipv6Address,
601    target: &Ipv6Address,
602    mac: &MacAddress,
603    solicited: bool,
604    override_flag: bool,
605) -> Vec<u8> {
606    // ICMPv6 Neighbor Advertisement:
607    //   Type (1) + Code (1) + Checksum (2) + Flags+Reserved (4) + Target Address
608    // (16)
609    //   + Target Link-Layer Address Option (8)
610    let mut msg = Vec::with_capacity(32);
611
612    // ICMPv6 header
613    msg.push(ICMPV6_NEIGHBOR_ADVERT); // Type
614    msg.push(0); // Code
615    msg.extend_from_slice(&[0u8; 2]); // Checksum (filled later)
616
617    // Flags: R (Router) = 0, S (Solicited), O (Override)
618    let mut flags: u8 = 0;
619    if solicited {
620        flags |= 0x40; // S flag
621    }
622    if override_flag {
623        flags |= 0x20; // O flag
624    }
625    msg.push(flags);
626    msg.extend_from_slice(&[0u8; 3]); // Rest of reserved field
627
628    // Target address
629    msg.extend_from_slice(&target.0);
630
631    // Target Link-Layer Address option (Type=2, Length=1)
632    msg.push(NDP_OPT_TARGET_LINK_ADDR);
633    msg.push(1); // Length in 8-byte units
634    msg.extend_from_slice(&mac.0);
635
636    // Compute and fill checksum
637    let checksum = compute_icmpv6_checksum(&src.0, &dst.0, &msg);
638    msg[2] = (checksum >> 8) as u8;
639    msg[3] = (checksum & 0xff) as u8;
640
641    msg
642}
643
644/// Build a Router Solicitation message.
645///
646/// Sent by hosts at startup to request Router Advertisement from routers.
647pub fn ndp_router_solicit(src: &Ipv6Address, src_mac: &MacAddress) -> Vec<u8> {
648    // ICMPv6 Router Solicitation:
649    //   Type (1) + Code (1) + Checksum (2) + Reserved (4)
650    //   + Source Link-Layer Address Option (8)
651    let mut msg = Vec::with_capacity(16);
652
653    msg.push(ICMPV6_ROUTER_SOLICIT); // Type
654    msg.push(0); // Code
655    msg.extend_from_slice(&[0u8; 2]); // Checksum (filled later)
656    msg.extend_from_slice(&[0u8; 4]); // Reserved
657
658    // Source Link-Layer Address option
659    if !is_unspecified(src) {
660        msg.push(NDP_OPT_SOURCE_LINK_ADDR);
661        msg.push(1); // Length in 8-byte units
662        msg.extend_from_slice(&src_mac.0);
663    }
664
665    // Compute and fill checksum
666    let dst = Ipv6Address::ALL_ROUTERS_LINK_LOCAL;
667    let checksum = compute_icmpv6_checksum(&src.0, &dst.0, &msg);
668    msg[2] = (checksum >> 8) as u8;
669    msg[3] = (checksum & 0xff) as u8;
670
671    msg
672}
673
674/// Handle an incoming NDP message.
675///
676/// Processes Neighbor Solicitations, Neighbor Advertisements,
677/// Router Solicitations, and Router Advertisements.
678///
679/// Returns an optional response packet (ICMPv6 payload, not wrapped in IPv6).
680pub fn handle_ndp(
681    src_addr: &Ipv6Address,
682    dst_addr: &Ipv6Address,
683    data: &[u8],
684) -> Result<Option<Vec<u8>>, KernelError> {
685    if data.len() < 4 {
686        return Err(KernelError::InvalidArgument {
687            name: "ndp_message",
688            value: "too_short",
689        });
690    }
691
692    let icmp_type = data[0];
693
694    match icmp_type {
695        ICMPV6_NEIGHBOR_SOLICIT => handle_neighbor_solicitation(src_addr, dst_addr, data),
696        ICMPV6_NEIGHBOR_ADVERT => handle_neighbor_advertisement(src_addr, data),
697        ICMPV6_ROUTER_SOLICIT => {
698            // We are not a router -- ignore
699            Ok(None)
700        }
701        ICMPV6_ROUTER_ADVERT => handle_router_advertisement(src_addr, data),
702        _ => {
703            // Unknown NDP type, ignore
704            Ok(None)
705        }
706    }
707}
708
709/// Process an incoming Neighbor Solicitation.
710fn handle_neighbor_solicitation(
711    src_addr: &Ipv6Address,
712    _dst_addr: &Ipv6Address,
713    data: &[u8],
714) -> Result<Option<Vec<u8>>, KernelError> {
715    // NS format: Type(1) + Code(1) + Checksum(2) + Reserved(4) + Target(16) +
716    // Options...
717    if data.len() < 24 {
718        return Err(KernelError::InvalidArgument {
719            name: "ndp_ns",
720            value: "too_short",
721        });
722    }
723
724    let mut target = [0u8; 16];
725    target.copy_from_slice(&data[8..24]);
726    let target_addr = Ipv6Address(target);
727
728    // Extract source link-layer address option if present
729    let source_mac = parse_link_layer_option(&data[24..], NDP_OPT_SOURCE_LINK_ADDR);
730
731    // Update NDP cache with sender's info
732    if let Some(mac) = source_mac {
733        if !is_unspecified(src_addr) {
734            IPV6_STATE.with_mut(|state| {
735                let mut s = state.write();
736                s.ndp_cache.update(*src_addr, mac, NdpState::Stale);
737            });
738        }
739    }
740
741    // Check if the target address is one of our addresses
742    let is_our_addr = IPV6_STATE
743        .with(|state| {
744            let s = state.read();
745            s.config
746                .ipv6_addresses
747                .iter()
748                .any(|a| a.address == target_addr)
749        })
750        .unwrap_or(false);
751
752    if !is_our_addr {
753        return Ok(None);
754    }
755
756    // Build Neighbor Advertisement response
757    let our_mac = get_interface_mac();
758    let reply_dst = if is_unspecified(src_addr) {
759        Ipv6Address::ALL_NODES_LINK_LOCAL
760    } else {
761        *src_addr
762    };
763
764    let na = ndp_advertise(
765        &target_addr,
766        &reply_dst,
767        &target_addr,
768        &our_mac,
769        !is_unspecified(src_addr), // solicited flag
770        true,                      // override flag
771    );
772
773    Ok(Some(na))
774}
775
776/// Process an incoming Neighbor Advertisement.
777fn handle_neighbor_advertisement(
778    _src_addr: &Ipv6Address,
779    data: &[u8],
780) -> Result<Option<Vec<u8>>, KernelError> {
781    // NA format: Type(1) + Code(1) + Checksum(2) + Flags(1) + Reserved(3) +
782    // Target(16) + Options
783    if data.len() < 24 {
784        return Err(KernelError::InvalidArgument {
785            name: "ndp_na",
786            value: "too_short",
787        });
788    }
789
790    let _flags = data[4];
791
792    let mut target = [0u8; 16];
793    target.copy_from_slice(&data[8..24]);
794    let target_addr = Ipv6Address(target);
795
796    // Extract target link-layer address option
797    let target_mac = parse_link_layer_option(&data[24..], NDP_OPT_TARGET_LINK_ADDR);
798
799    // Update NDP cache
800    if let Some(mac) = target_mac {
801        IPV6_STATE.with_mut(|state| {
802            let mut s = state.write();
803            s.ndp_cache.update(target_addr, mac, NdpState::Reachable);
804        });
805    }
806
807    Ok(None)
808}
809
810/// Process an incoming Router Advertisement.
811fn handle_router_advertisement(
812    src_addr: &Ipv6Address,
813    data: &[u8],
814) -> Result<Option<Vec<u8>>, KernelError> {
815    // RA format: Type(1) + Code(1) + Checksum(2) + Hop Limit(1) + Flags(1)
816    //            + Router Lifetime(2) + Reachable Time(4) + Retrans Timer(4) +
817    //              Options...
818    if data.len() < 16 {
819        return Err(KernelError::InvalidArgument {
820            name: "ndp_ra",
821            value: "too_short",
822        });
823    }
824
825    let cur_hop_limit = data[4];
826    let _flags = data[5];
827    let router_lifetime = u16::from_be_bytes([data[6], data[7]]);
828
829    // Update hop limit if specified
830    if cur_hop_limit != 0 {
831        IPV6_STATE.with_mut(|state| {
832            let mut s = state.write();
833            s.hop_limit = cur_hop_limit;
834        });
835    }
836
837    // Update NDP cache for the router
838    let source_mac = parse_link_layer_option(&data[16..], NDP_OPT_SOURCE_LINK_ADDR);
839    if let Some(mac) = source_mac {
840        IPV6_STATE.with_mut(|state| {
841            let mut s = state.write();
842            s.ndp_cache.update(*src_addr, mac, NdpState::Reachable);
843        });
844    }
845
846    // Parse prefix information options for SLAAC
847    parse_prefix_options(&data[16..], router_lifetime);
848
849    Ok(None)
850}
851
852/// Parse NDP options to find a link-layer address option of the given type.
853fn parse_link_layer_option(options: &[u8], opt_type: u8) -> Option<MacAddress> {
854    let mut offset = 0;
855    while offset + 2 <= options.len() {
856        let otype = options[offset];
857        let olen = options[offset + 1] as usize;
858        if olen == 0 {
859            break; // Prevent infinite loop on malformed options
860        }
861        let opt_bytes = olen * 8;
862        if offset + opt_bytes > options.len() {
863            break;
864        }
865        if otype == opt_type && opt_bytes >= 8 {
866            let mut mac_bytes = [0u8; 6];
867            mac_bytes.copy_from_slice(&options[offset + 2..offset + 8]);
868            return Some(MacAddress(mac_bytes));
869        }
870        offset += opt_bytes;
871    }
872    None
873}
874
875/// Parse prefix information options from a Router Advertisement.
876fn parse_prefix_options(options: &[u8], _router_lifetime: u16) {
877    let mut offset = 0;
878    while offset + 2 <= options.len() {
879        let otype = options[offset];
880        let olen = options[offset + 1] as usize;
881        if olen == 0 {
882            break;
883        }
884        let opt_bytes = olen * 8;
885        if offset + opt_bytes > options.len() {
886            break;
887        }
888
889        if otype == NDP_OPT_PREFIX_INFO && opt_bytes >= 32 {
890            let prefix_len = options[offset + 2];
891            let flags = options[offset + 3];
892            let autonomous = (flags & 0x40) != 0;
893
894            if autonomous && prefix_len == 64 {
895                let mut prefix = [0u8; 16];
896                prefix.copy_from_slice(&options[offset + 16..offset + 32]);
897
898                // Perform SLAAC: combine prefix with EUI-64 interface ID
899                let our_mac = get_interface_mac();
900                let ll = link_local_from_mac(&our_mac);
901                // Copy interface ID from link-local (bytes 8..16)
902                let mut addr_bytes = prefix;
903                addr_bytes[8..16].copy_from_slice(&ll.0[8..16]);
904                let new_addr = Ipv6Address(addr_bytes);
905
906                IPV6_STATE.with_mut(|state| {
907                    let mut s = state.write();
908                    let exists = s
909                        .config
910                        .ipv6_addresses
911                        .iter()
912                        .any(|a| a.address == new_addr);
913                    if !exists {
914                        s.config.ipv6_addresses.push(Ipv6InterfaceAddr {
915                            address: new_addr,
916                            prefix_len,
917                            scope: Ipv6Scope::Global,
918                        });
919                        println!(
920                            "[IPv6] SLAAC: configured global address {}",
921                            format_ipv6_compressed(&new_addr)
922                        );
923                    }
924                });
925            }
926        }
927
928        offset += opt_bytes;
929    }
930}
931
932// ============================================================================
933// ICMPv6 Checksum
934// ============================================================================
935
936/// Compute ICMPv6 checksum using the IPv6 pseudo-header.
937///
938/// The pseudo-header includes: source address (16), destination address (16),
939/// upper-layer packet length (4), and next header (4, zero-padded).
940pub fn compute_icmpv6_checksum(src: &[u8; 16], dst: &[u8; 16], data: &[u8]) -> u16 {
941    let mut sum: u32 = 0;
942
943    // Pseudo-header: source address
944    for chunk in src.chunks(2) {
945        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
946    }
947
948    // Pseudo-header: destination address
949    for chunk in dst.chunks(2) {
950        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
951    }
952
953    // Pseudo-header: upper-layer packet length (32-bit)
954    let length = data.len() as u32;
955    sum += length >> 16;
956    sum += length & 0xFFFF;
957
958    // Pseudo-header: next header (ICMPv6 = 58), zero-padded to 32 bits
959    sum += NEXT_HEADER_ICMPV6 as u32;
960
961    // ICMPv6 data (with checksum field zeroed -- caller must have zeroed bytes
962    // [2..4])
963    for chunk in data.chunks(2) {
964        if chunk.len() == 2 {
965            sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
966        } else {
967            sum += (chunk[0] as u32) << 8;
968        }
969    }
970
971    // Fold 32-bit sum to 16 bits
972    while sum >> 16 != 0 {
973        sum = (sum & 0xFFFF) + (sum >> 16);
974    }
975
976    !(sum as u16)
977}
978
979// ============================================================================
980// Dual-Stack Configuration
981// ============================================================================
982
983/// IPv6 address scope
984#[derive(Debug, Clone, Copy, PartialEq, Eq)]
985pub enum Ipv6Scope {
986    /// Link-local (fe80::/10)
987    LinkLocal,
988    /// Global unicast (2000::/3)
989    Global,
990    /// Site-local (deprecated, fec0::/10)
991    SiteLocal,
992}
993
994/// IPv6 interface address with prefix length and scope
995#[derive(Debug, Clone)]
996pub struct Ipv6InterfaceAddr {
997    /// The IPv6 address
998    pub address: Ipv6Address,
999    /// Prefix length (e.g., 64, 128)
1000    pub prefix_len: u8,
1001    /// Address scope
1002    pub scope: Ipv6Scope,
1003}
1004
1005/// Dual-stack network configuration
1006#[derive(Debug, Clone)]
1007pub struct DualStackConfig {
1008    /// Whether IPv4 is enabled on this interface
1009    pub ipv4_enabled: bool,
1010    /// Whether IPv6 is enabled on this interface
1011    pub ipv6_enabled: bool,
1012    /// Whether to prefer IPv6 for dual-stack connections
1013    pub prefer_ipv6: bool,
1014    /// List of configured IPv6 addresses on this interface
1015    pub ipv6_addresses: Vec<Ipv6InterfaceAddr>,
1016}
1017
1018impl DualStackConfig {
1019    /// Create a new dual-stack configuration with defaults
1020    pub fn new() -> Self {
1021        Self {
1022            ipv4_enabled: true,
1023            ipv6_enabled: true,
1024            prefer_ipv6: true,
1025            ipv6_addresses: Vec::new(),
1026        }
1027    }
1028
1029    /// Get the primary link-local address, if any
1030    pub fn link_local_addr(&self) -> Option<&Ipv6InterfaceAddr> {
1031        self.ipv6_addresses
1032            .iter()
1033            .find(|a| a.scope == Ipv6Scope::LinkLocal)
1034    }
1035
1036    /// Get the primary global address, if any
1037    pub fn global_addr(&self) -> Option<&Ipv6InterfaceAddr> {
1038        self.ipv6_addresses
1039            .iter()
1040            .find(|a| a.scope == Ipv6Scope::Global)
1041    }
1042
1043    /// Get all addresses of a given scope
1044    pub fn addresses_by_scope(&self, scope: Ipv6Scope) -> Vec<&Ipv6InterfaceAddr> {
1045        self.ipv6_addresses
1046            .iter()
1047            .filter(|a| a.scope == scope)
1048            .collect()
1049    }
1050}
1051
1052impl Default for DualStackConfig {
1053    fn default() -> Self {
1054        Self::new()
1055    }
1056}
1057
1058// ============================================================================
1059// Global IPv6 State
1060// ============================================================================
1061
1062/// Complete IPv6 subsystem state
1063pub struct Ipv6State {
1064    /// Dual-stack configuration
1065    pub config: DualStackConfig,
1066    /// NDP neighbor cache
1067    pub ndp_cache: NdpCache,
1068    /// Default hop limit for outgoing packets
1069    pub hop_limit: u8,
1070}
1071
1072/// Global IPv6 state (protected by RwLock for concurrent access)
1073static IPV6_STATE: GlobalState<RwLock<Ipv6State>> = GlobalState::new();
1074
1075/// Monotonic tick counter for NDP cache aging
1076static NDP_TICK: AtomicU64 = AtomicU64::new(0);
1077
1078/// Get the current NDP tick value
1079fn current_tick() -> u64 {
1080    NDP_TICK.load(Ordering::Relaxed)
1081}
1082
1083/// Advance the NDP tick counter (called periodically by timer)
1084pub fn tick() {
1085    NDP_TICK.fetch_add(1, Ordering::Relaxed);
1086}
1087
1088// ============================================================================
1089// Public API
1090// ============================================================================
1091
1092/// Initialize the IPv6 subsystem.
1093///
1094/// Creates the global state with a link-local address derived from the
1095/// primary network interface's MAC address.
1096pub fn init() -> Result<(), KernelError> {
1097    println!("[IPv6] Initializing IPv6 subsystem...");
1098
1099    let our_mac = get_interface_mac();
1100    let link_local = link_local_from_mac(&our_mac);
1101
1102    let mut config = DualStackConfig::new();
1103    config.ipv6_addresses.push(Ipv6InterfaceAddr {
1104        address: link_local,
1105        prefix_len: 10,
1106        scope: Ipv6Scope::LinkLocal,
1107    });
1108
1109    let state = Ipv6State {
1110        config,
1111        ndp_cache: NdpCache::new(),
1112        hop_limit: DEFAULT_HOP_LIMIT,
1113    };
1114
1115    IPV6_STATE
1116        .init(RwLock::new(state))
1117        .map_err(|_| KernelError::AlreadyExists {
1118            resource: "ipv6_state",
1119            id: 0,
1120        })?;
1121
1122    println!(
1123        "[IPv6] Link-local address: {}",
1124        format_ipv6_compressed(&link_local)
1125    );
1126    println!("[IPv6] IPv6 subsystem initialized");
1127
1128    Ok(())
1129}
1130
1131/// Look up a neighbor's MAC address in the NDP cache.
1132pub fn ndp_lookup(addr: &Ipv6Address) -> Option<MacAddress> {
1133    IPV6_STATE
1134        .with(|state| {
1135            let s = state.read();
1136            s.ndp_cache.lookup(addr)
1137        })
1138        .flatten()
1139}
1140
1141/// Get the current dual-stack configuration.
1142pub fn get_config() -> Option<DualStackConfig> {
1143    IPV6_STATE.with(|state| {
1144        let s = state.read();
1145        s.config.clone()
1146    })
1147}
1148
1149/// Get NDP cache entries for display.
1150pub fn get_ndp_entries() -> Vec<(Ipv6Address, MacAddress, NdpState)> {
1151    IPV6_STATE
1152        .with(|state| {
1153            let s = state.read();
1154            s.ndp_cache.get_entries()
1155        })
1156        .unwrap_or_default()
1157}
1158
1159/// Flush the NDP cache.
1160pub fn flush_ndp_cache() {
1161    IPV6_STATE.with_mut(|state| {
1162        let mut s = state.write();
1163        s.ndp_cache.flush();
1164    });
1165}
1166
1167/// Get the current hop limit.
1168pub fn get_hop_limit() -> u8 {
1169    IPV6_STATE
1170        .with(|state| {
1171            let s = state.read();
1172            s.hop_limit
1173        })
1174        .unwrap_or(DEFAULT_HOP_LIMIT)
1175}
1176
1177/// Send an IPv6 packet.
1178///
1179/// Wraps the payload in an IPv6 header, resolves the destination MAC via NDP
1180/// (or uses multicast MAC), and transmits via the Ethernet layer.
1181pub fn send(
1182    src: &Ipv6Address,
1183    dst: &Ipv6Address,
1184    next_header: u8,
1185    payload: &[u8],
1186) -> Result<(), KernelError> {
1187    let packet = build_ipv6(src, dst, next_header, payload);
1188
1189    // Resolve destination MAC address
1190    let dst_mac = if is_multicast(dst) {
1191        multicast_mac(dst)
1192    } else {
1193        ndp_lookup(dst).unwrap_or_else(|| {
1194            // Start NDP resolution and use multicast for now
1195            let src_mac = get_interface_mac();
1196            let ns = ndp_solicit(src, dst, &src_mac);
1197            let sol_dst = solicited_node_multicast(dst);
1198            let ns_packet = build_ipv6(src, &sol_dst, NEXT_HEADER_ICMPV6, &ns);
1199            let sol_mac = multicast_mac(&sol_dst);
1200
1201            // Transmit NS packet
1202            let frame =
1203                super::ethernet::construct_frame(sol_mac, src_mac, ETHERTYPE_IPV6, &ns_packet);
1204            let pkt = super::Packet::from_bytes(&frame);
1205            super::device::with_device_mut("eth0", |dev| {
1206                let _ = dev.transmit(&pkt);
1207            });
1208
1209            // Mark as incomplete in the cache
1210            IPV6_STATE.with_mut(|state| {
1211                let mut s = state.write();
1212                s.ndp_cache.mark_incomplete(*dst);
1213            });
1214
1215            // Use broadcast for now; the response will update the cache
1216            MacAddress::BROADCAST
1217        })
1218    };
1219
1220    let src_mac = get_interface_mac();
1221    let frame = super::ethernet::construct_frame(dst_mac, src_mac, ETHERTYPE_IPV6, &packet);
1222    let pkt = super::Packet::from_bytes(&frame);
1223    super::device::with_device_mut("eth0", |dev| {
1224        let _ = dev.transmit(&pkt);
1225    });
1226
1227    super::update_stats_tx(IPV6_HEADER_SIZE + payload.len());
1228
1229    Ok(())
1230}
1231
1232/// Get the source address appropriate for a given destination.
1233///
1234/// If the destination is link-local, returns our link-local address.
1235/// Otherwise, returns a global address if available.
1236pub fn select_source_address(dst: &Ipv6Address) -> Option<Ipv6Address> {
1237    IPV6_STATE
1238        .with(|state| {
1239            let s = state.read();
1240            if is_link_local(dst) || is_multicast(dst) {
1241                s.config.link_local_addr().map(|a| a.address)
1242            } else {
1243                s.config
1244                    .global_addr()
1245                    .or_else(|| s.config.link_local_addr())
1246                    .map(|a| a.address)
1247            }
1248        })
1249        .flatten()
1250}
1251
1252/// Process an incoming IPv6 packet (dispatched from the Ethernet layer).
1253pub fn process_packet(data: &[u8]) -> Result<(), KernelError> {
1254    let (header, payload) = parse_ipv6(data)?;
1255
1256    let src = Ipv6Address(header.source);
1257    let dst = Ipv6Address(header.destination);
1258
1259    match header.next_header {
1260        NEXT_HEADER_ICMPV6 => {
1261            super::icmpv6::handle_icmpv6(&src, &dst, payload)?;
1262        }
1263        NEXT_HEADER_TCP => {
1264            let src_ip = super::IpAddress::V6(src);
1265            let dst_ip = super::IpAddress::V6(dst);
1266            let _ = super::tcp::process_packet(src_ip, dst_ip, payload);
1267        }
1268        NEXT_HEADER_UDP => {
1269            let src_ip = super::IpAddress::V6(src);
1270            let dst_ip = super::IpAddress::V6(dst);
1271            let _ = super::udp::process_packet(src_ip, dst_ip, payload);
1272        }
1273        _ => {
1274            // Unknown next header -- silently drop
1275        }
1276    }
1277
1278    Ok(())
1279}
1280
1281// ============================================================================
1282// Internal Helpers
1283// ============================================================================
1284
1285/// Get the MAC address of the primary network interface.
1286fn get_interface_mac() -> MacAddress {
1287    super::device::with_device("eth0", |dev| dev.mac_address()).unwrap_or(MacAddress::ZERO)
1288}
1289
1290// ============================================================================
1291// IPv6 Statistics
1292// ============================================================================
1293
1294/// IPv6 statistics
1295#[derive(Debug, Clone, Copy, Default)]
1296pub struct Ipv6Stats {
1297    /// Number of IPv6 addresses configured
1298    pub addresses_configured: usize,
1299    /// Number of NDP cache entries
1300    pub ndp_cache_entries: usize,
1301    /// Current hop limit
1302    pub hop_limit: u8,
1303    /// Whether dual-stack is active
1304    pub dual_stack_active: bool,
1305}
1306
1307/// Get IPv6 statistics.
1308pub fn get_stats() -> Ipv6Stats {
1309    IPV6_STATE
1310        .with(|state| {
1311            let s = state.read();
1312            Ipv6Stats {
1313                addresses_configured: s.config.ipv6_addresses.len(),
1314                ndp_cache_entries: s.ndp_cache.len(),
1315                hop_limit: s.hop_limit,
1316                dual_stack_active: s.config.ipv4_enabled && s.config.ipv6_enabled,
1317            }
1318        })
1319        .unwrap_or_default()
1320}
1321
1322// ============================================================================
1323// Tests
1324// ============================================================================
1325
1326#[cfg(test)]
1327mod tests {
1328    use super::*;
1329
1330    #[test]
1331    fn test_ipv6_header_roundtrip() {
1332        let src = Ipv6Address([
1333            0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0x02, 0x00, 0x00, 0xff, 0xfe, 0x00, 0x00, 0x01,
1334        ]);
1335        let dst = Ipv6Address([
1336            0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0x02, 0x00, 0x00, 0xff, 0xfe, 0x00, 0x00, 0x02,
1337        ]);
1338        let payload = b"Hello IPv6!";
1339        let packet = build_ipv6(&src, &dst, NEXT_HEADER_TCP, payload);
1340
1341        let (header, parsed_payload) = parse_ipv6(&packet).unwrap();
1342        assert_eq!(header.version(), IPV6_VERSION);
1343        assert_eq!(header.next_header, NEXT_HEADER_TCP);
1344        assert_eq!(header.hop_limit, DEFAULT_HOP_LIMIT);
1345        assert_eq!(header.source, src.0);
1346        assert_eq!(header.destination, dst.0);
1347        assert_eq!(parsed_payload, payload);
1348    }
1349
1350    #[test]
1351    fn test_ipv6_parse_too_short() {
1352        let short = [0u8; 10];
1353        assert!(parse_ipv6(&short).is_err());
1354    }
1355
1356    #[test]
1357    fn test_ipv6_parse_wrong_version() {
1358        let mut packet = [0u8; IPV6_HEADER_SIZE];
1359        // Set version to 4 instead of 6
1360        packet[0] = 0x40;
1361        assert!(parse_ipv6(&packet).is_err());
1362    }
1363
1364    #[test]
1365    fn test_is_link_local() {
1366        let ll = Ipv6Address([0xfe, 0x80, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8]);
1367        assert!(is_link_local(&ll));
1368
1369        let global = Ipv6Address([0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
1370        assert!(!is_link_local(&global));
1371    }
1372
1373    #[test]
1374    fn test_is_multicast() {
1375        let mc = Ipv6Address([0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
1376        assert!(is_multicast(&mc));
1377        assert!(!is_multicast(&Ipv6Address::LOCALHOST));
1378    }
1379
1380    #[test]
1381    fn test_is_loopback() {
1382        assert!(is_loopback(&Ipv6Address::LOCALHOST));
1383        assert!(!is_loopback(&Ipv6Address::UNSPECIFIED));
1384    }
1385
1386    #[test]
1387    fn test_is_unspecified() {
1388        assert!(is_unspecified(&Ipv6Address::UNSPECIFIED));
1389        assert!(!is_unspecified(&Ipv6Address::LOCALHOST));
1390    }
1391
1392    #[test]
1393    fn test_is_global_unicast() {
1394        let global = Ipv6Address([0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
1395        assert!(is_global_unicast(&global));
1396        assert!(!is_global_unicast(&Ipv6Address::LOCALHOST));
1397    }
1398
1399    #[test]
1400    fn test_solicited_node_multicast() {
1401        let addr = Ipv6Address([
1402            0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0x02, 0x00, 0x00, 0xff, 0xfe, 0xab, 0xcd, 0xef,
1403        ]);
1404        let sol = solicited_node_multicast(&addr);
1405        assert_eq!(sol.0[0], 0xff);
1406        assert_eq!(sol.0[1], 0x02);
1407        assert_eq!(sol.0[11], 0x01);
1408        assert_eq!(sol.0[12], 0xff);
1409        assert_eq!(sol.0[13], 0xab);
1410        assert_eq!(sol.0[14], 0xcd);
1411        assert_eq!(sol.0[15], 0xef);
1412    }
1413
1414    #[test]
1415    fn test_link_local_from_mac() {
1416        let mac = MacAddress([0x52, 0x54, 0x00, 0x12, 0x34, 0x56]);
1417        let ll = link_local_from_mac(&mac);
1418        assert_eq!(ll.0[0], 0xfe);
1419        assert_eq!(ll.0[1], 0x80);
1420        assert_eq!(ll.0[8], 0x52 ^ 0x02);
1421        assert_eq!(ll.0[9], 0x54);
1422        assert_eq!(ll.0[10], 0x00);
1423        assert_eq!(ll.0[11], 0xff);
1424        assert_eq!(ll.0[12], 0xfe);
1425        assert_eq!(ll.0[13], 0x12);
1426        assert_eq!(ll.0[14], 0x34);
1427        assert_eq!(ll.0[15], 0x56);
1428        assert!(is_link_local(&ll));
1429    }
1430
1431    #[test]
1432    fn test_multicast_mac() {
1433        let mc = Ipv6Address([0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
1434        let mac = multicast_mac(&mc);
1435        assert_eq!(mac.0[0], 0x33);
1436        assert_eq!(mac.0[1], 0x33);
1437        assert_eq!(mac.0[2], 0x00);
1438        assert_eq!(mac.0[3], 0x00);
1439        assert_eq!(mac.0[4], 0x00);
1440        assert_eq!(mac.0[5], 0x01);
1441    }
1442
1443    #[test]
1444    fn test_format_ipv6() {
1445        let addr = Ipv6Address::LOCALHOST;
1446        let formatted = format_ipv6(&addr);
1447        assert_eq!(formatted, "0:0:0:0:0:0:0:1");
1448    }
1449
1450    #[test]
1451    fn test_format_ipv6_compressed_loopback() {
1452        let addr = Ipv6Address::LOCALHOST;
1453        let formatted = format_ipv6_compressed(&addr);
1454        assert_eq!(formatted, "::1");
1455    }
1456
1457    #[test]
1458    fn test_format_ipv6_compressed_unspecified() {
1459        let addr = Ipv6Address::UNSPECIFIED;
1460        let formatted = format_ipv6_compressed(&addr);
1461        assert_eq!(formatted, "::");
1462    }
1463
1464    #[test]
1465    fn test_icmpv6_checksum() {
1466        let src = [0u8; 16];
1467        let dst = [0u8; 16];
1468        let data = [0u8; 4];
1469        let cksum = compute_icmpv6_checksum(&src, &dst, &data);
1470        assert_ne!(cksum, 0);
1471    }
1472
1473    #[test]
1474    fn test_ndp_cache_basic() {
1475        let mut cache = NdpCache::new();
1476        let addr = Ipv6Address([0xfe, 0x80, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8]);
1477        let mac = MacAddress([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
1478
1479        assert!(cache.lookup(&addr).is_none());
1480
1481        cache.update(addr, mac, NdpState::Reachable);
1482        assert_eq!(cache.lookup(&addr), Some(mac));
1483        assert_eq!(cache.len(), 1);
1484
1485        cache.flush();
1486        assert!(cache.is_empty());
1487    }
1488
1489    #[test]
1490    fn test_ndp_cache_incomplete() {
1491        let mut cache = NdpCache::new();
1492        let addr = Ipv6Address([0xfe, 0x80, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8]);
1493
1494        cache.mark_incomplete(addr);
1495        assert!(cache.lookup(&addr).is_none());
1496        assert_eq!(cache.len(), 1);
1497    }
1498
1499    #[test]
1500    fn test_dual_stack_config() {
1501        let mut config = DualStackConfig::new();
1502        assert!(config.ipv4_enabled);
1503        assert!(config.ipv6_enabled);
1504        assert!(config.prefer_ipv6);
1505        assert!(config.link_local_addr().is_none());
1506
1507        config.ipv6_addresses.push(Ipv6InterfaceAddr {
1508            address: Ipv6Address([0xfe, 0x80, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8]),
1509            prefix_len: 10,
1510            scope: Ipv6Scope::LinkLocal,
1511        });
1512
1513        assert!(config.link_local_addr().is_some());
1514        assert!(config.global_addr().is_none());
1515    }
1516
1517    #[test]
1518    fn test_is_ipv4_mapped() {
1519        let mapped = Ipv6Address([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 192, 168, 1, 1]);
1520        assert!(is_ipv4_mapped(&mapped));
1521        assert!(!is_ipv4_mapped(&Ipv6Address::LOCALHOST));
1522    }
1523}