Skip to main content

rovs_openflow/
ndp.rs

1//! NDP (Neighbor Discovery Protocol) packet parsing and construction.
2//!
3//! This module provides utilities for parsing ICMPv6 Neighbor Solicitation
4//! messages and constructing Neighbor Advertisement replies, enabling
5//! NDP proxy functionality in OpenFlow controllers.
6
7use std::net::Ipv6Addr;
8
9/// Ethernet header size.
10pub const ETH_HEADER_LEN: usize = 14;
11/// IPv6 header size (fixed part).
12pub const IPV6_HEADER_LEN: usize = 40;
13/// ICMPv6 header size (type + code + checksum).
14pub const ICMPV6_HEADER_LEN: usize = 4;
15/// Neighbor Solicitation/Advertisement body size (reserved + target).
16pub const ND_BODY_LEN: usize = 20;
17
18/// Ethertype for IPv6.
19pub const ETHERTYPE_IPV6: u16 = 0x86dd;
20/// IPv6 next header value for ICMPv6.
21pub const IPPROTO_ICMPV6: u8 = 58;
22/// ICMPv6 type for Neighbor Solicitation.
23pub const ICMPV6_NEIGHBOR_SOLICITATION: u8 = 135;
24/// ICMPv6 type for Neighbor Advertisement.
25pub const ICMPV6_NEIGHBOR_ADVERTISEMENT: u8 = 136;
26/// NDP option type for Source Link-Layer Address.
27pub const NDP_OPT_SOURCE_LL_ADDR: u8 = 1;
28/// NDP option type for Target Link-Layer Address.
29pub const NDP_OPT_TARGET_LL_ADDR: u8 = 2;
30
31/// Parsed Ethernet frame.
32#[derive(Debug, Clone)]
33pub struct EthernetFrame {
34    /// Destination MAC address.
35    pub dst_mac: [u8; 6],
36    /// Source MAC address.
37    pub src_mac: [u8; 6],
38    /// Ethertype.
39    pub ethertype: u16,
40    /// Payload offset in original packet.
41    pub payload_offset: usize,
42}
43
44impl EthernetFrame {
45    /// Parse an Ethernet frame header.
46    pub fn parse(data: &[u8]) -> Option<Self> {
47        if data.len() < ETH_HEADER_LEN {
48            return None;
49        }
50
51        let dst_mac: [u8; 6] = data[0..6].try_into().ok()?;
52        let src_mac: [u8; 6] = data[6..12].try_into().ok()?;
53        let ethertype = u16::from_be_bytes([data[12], data[13]]);
54
55        Some(Self {
56            dst_mac,
57            src_mac,
58            ethertype,
59            payload_offset: ETH_HEADER_LEN,
60        })
61    }
62
63    /// Encode an Ethernet frame header.
64    pub fn encode(&self, buf: &mut Vec<u8>) {
65        buf.extend_from_slice(&self.dst_mac);
66        buf.extend_from_slice(&self.src_mac);
67        buf.extend_from_slice(&self.ethertype.to_be_bytes());
68    }
69}
70
71/// Parsed IPv6 header.
72#[derive(Debug, Clone)]
73pub struct Ipv6Header {
74    /// Traffic class.
75    pub traffic_class: u8,
76    /// Flow label.
77    pub flow_label: u32,
78    /// Payload length.
79    pub payload_len: u16,
80    /// Next header (protocol).
81    pub next_header: u8,
82    /// Hop limit.
83    pub hop_limit: u8,
84    /// Source address.
85    pub src_addr: Ipv6Addr,
86    /// Destination address.
87    pub dst_addr: Ipv6Addr,
88}
89
90impl Ipv6Header {
91    /// Parse an IPv6 header.
92    pub fn parse(data: &[u8]) -> Option<Self> {
93        if data.len() < IPV6_HEADER_LEN {
94            return None;
95        }
96
97        // Version (4 bits) + Traffic Class (8 bits) + Flow Label (20 bits)
98        let version = (data[0] >> 4) & 0x0f;
99        if version != 6 {
100            return None;
101        }
102
103        let traffic_class = ((data[0] & 0x0f) << 4) | ((data[1] >> 4) & 0x0f);
104        let flow_label =
105            ((data[1] as u32 & 0x0f) << 16) | ((data[2] as u32) << 8) | (data[3] as u32);
106        let payload_len = u16::from_be_bytes([data[4], data[5]]);
107        let next_header = data[6];
108        let hop_limit = data[7];
109
110        let src_bytes: [u8; 16] = data[8..24].try_into().ok()?;
111        let dst_bytes: [u8; 16] = data[24..40].try_into().ok()?;
112
113        Some(Self {
114            traffic_class,
115            flow_label,
116            payload_len,
117            next_header,
118            hop_limit,
119            src_addr: Ipv6Addr::from(src_bytes),
120            dst_addr: Ipv6Addr::from(dst_bytes),
121        })
122    }
123
124    /// Encode an IPv6 header.
125    pub fn encode(&self, buf: &mut Vec<u8>) {
126        // Version (6) + Traffic Class high 4 bits
127        buf.push(0x60 | ((self.traffic_class >> 4) & 0x0f));
128        // Traffic Class low 4 bits + Flow Label high 4 bits
129        buf.push(((self.traffic_class & 0x0f) << 4) | ((self.flow_label >> 16) as u8 & 0x0f));
130        // Flow Label middle 8 bits
131        buf.push((self.flow_label >> 8) as u8);
132        // Flow Label low 8 bits
133        buf.push(self.flow_label as u8);
134        // Payload length
135        buf.extend_from_slice(&self.payload_len.to_be_bytes());
136        // Next header
137        buf.push(self.next_header);
138        // Hop limit
139        buf.push(self.hop_limit);
140        // Source address
141        buf.extend_from_slice(&self.src_addr.octets());
142        // Destination address
143        buf.extend_from_slice(&self.dst_addr.octets());
144    }
145}
146
147/// Parsed ICMPv6 Neighbor Solicitation.
148#[derive(Debug, Clone)]
149pub struct NeighborSolicitation {
150    /// Target IPv6 address being queried.
151    pub target_addr: Ipv6Addr,
152    /// Source link-layer address option (if present).
153    pub source_ll_addr: Option<[u8; 6]>,
154}
155
156impl NeighborSolicitation {
157    /// Parse an ICMPv6 Neighbor Solicitation from the ICMPv6 payload.
158    /// Expects data starting at the ICMPv6 header (type, code, checksum).
159    pub fn parse(data: &[u8]) -> Option<Self> {
160        // Minimum: ICMPv6 header (4) + reserved (4) + target (16) = 24 bytes
161        if data.len() < ICMPV6_HEADER_LEN + ND_BODY_LEN {
162            return None;
163        }
164
165        let icmp_type = data[0];
166        if icmp_type != ICMPV6_NEIGHBOR_SOLICITATION {
167            return None;
168        }
169
170        // Skip: type(1) + code(1) + checksum(2) + reserved(4) = 8 bytes
171        let target_bytes: [u8; 16] = data[8..24].try_into().ok()?;
172        let target_addr = Ipv6Addr::from(target_bytes);
173
174        // Parse options (if any)
175        let mut source_ll_addr = None;
176        let mut offset = 24;
177
178        while offset + 2 <= data.len() {
179            let opt_type = data[offset];
180            let opt_len = data[offset + 1] as usize * 8; // Length in units of 8 bytes
181
182            if opt_len == 0 {
183                break; // Invalid option length
184            }
185
186            if offset + opt_len > data.len() {
187                break; // Truncated option
188            }
189
190            if opt_type == NDP_OPT_SOURCE_LL_ADDR && opt_len >= 8 {
191                // Source Link-Layer Address option
192                source_ll_addr = Some(data[offset + 2..offset + 8].try_into().ok()?);
193            }
194
195            offset += opt_len;
196        }
197
198        Some(Self {
199            target_addr,
200            source_ll_addr,
201        })
202    }
203}
204
205/// Neighbor Advertisement builder.
206#[derive(Debug, Clone)]
207pub struct NeighborAdvertisement {
208    /// Target IPv6 address.
209    pub target_addr: Ipv6Addr,
210    /// Target link-layer address to include in option.
211    pub target_ll_addr: [u8; 6],
212    /// Router flag.
213    pub is_router: bool,
214    /// Solicited flag (response to NS).
215    pub is_solicited: bool,
216    /// Override flag.
217    pub is_override: bool,
218}
219
220impl NeighborAdvertisement {
221    /// Create a new Neighbor Advertisement.
222    pub fn new(target_addr: Ipv6Addr, target_ll_addr: [u8; 6]) -> Self {
223        Self {
224            target_addr,
225            target_ll_addr,
226            is_router: false,
227            is_solicited: true,
228            is_override: true,
229        }
230    }
231
232    /// Set the router flag.
233    pub fn router(mut self, is_router: bool) -> Self {
234        self.is_router = is_router;
235        self
236    }
237
238    /// Set the solicited flag.
239    pub fn solicited(mut self, is_solicited: bool) -> Self {
240        self.is_solicited = is_solicited;
241        self
242    }
243
244    /// Set the override flag.
245    pub fn override_flag(mut self, is_override: bool) -> Self {
246        self.is_override = is_override;
247        self
248    }
249
250    /// Encode the ICMPv6 Neighbor Advertisement (without checksum).
251    /// Returns the ICMPv6 message body that needs checksum calculation.
252    pub fn encode_icmpv6(&self) -> Vec<u8> {
253        let mut buf = Vec::with_capacity(32);
254
255        // ICMPv6 type
256        buf.push(ICMPV6_NEIGHBOR_ADVERTISEMENT);
257        // Code
258        buf.push(0);
259        // Checksum placeholder (will be filled in later)
260        buf.push(0);
261        buf.push(0);
262
263        // Flags: R(1) + S(1) + O(1) + reserved(29 bits)
264        let mut flags: u32 = 0;
265        if self.is_router {
266            flags |= 0x8000_0000;
267        }
268        if self.is_solicited {
269            flags |= 0x4000_0000;
270        }
271        if self.is_override {
272            flags |= 0x2000_0000;
273        }
274        buf.extend_from_slice(&flags.to_be_bytes());
275
276        // Target address
277        buf.extend_from_slice(&self.target_addr.octets());
278
279        // Target Link-Layer Address option
280        buf.push(NDP_OPT_TARGET_LL_ADDR); // Type
281        buf.push(1); // Length (in units of 8 bytes)
282        buf.extend_from_slice(&self.target_ll_addr);
283
284        buf
285    }
286}
287
288/// Calculate ICMPv6 checksum.
289///
290/// The checksum is computed over a pseudo-header and the ICMPv6 message.
291pub fn icmpv6_checksum(src: &Ipv6Addr, dst: &Ipv6Addr, icmpv6_data: &[u8]) -> u16 {
292    let mut sum: u32 = 0;
293
294    // Pseudo-header
295    for chunk in src.octets().chunks(2) {
296        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
297    }
298    for chunk in dst.octets().chunks(2) {
299        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
300    }
301    // Upper-layer packet length
302    sum += icmpv6_data.len() as u32;
303    // Next header (ICMPv6 = 58)
304    sum += IPPROTO_ICMPV6 as u32;
305
306    // ICMPv6 data
307    let mut i = 0;
308    while i + 1 < icmpv6_data.len() {
309        sum += u16::from_be_bytes([icmpv6_data[i], icmpv6_data[i + 1]]) as u32;
310        i += 2;
311    }
312    if i < icmpv6_data.len() {
313        sum += (icmpv6_data[i] as u32) << 8;
314    }
315
316    // Fold 32-bit sum to 16 bits
317    while sum >> 16 != 0 {
318        sum = (sum & 0xffff) + (sum >> 16);
319    }
320
321    !sum as u16
322}
323
324/// Build a complete Neighbor Advertisement reply packet.
325///
326/// Given a parsed Neighbor Solicitation and the MAC/IPv6 to advertise,
327/// constructs a complete Ethernet + IPv6 + ICMPv6 NA packet.
328#[allow(clippy::missing_panics_doc)]
329pub fn build_na_reply(
330    ns_eth: &EthernetFrame,
331    ns_ipv6: &Ipv6Header,
332    ns: &NeighborSolicitation,
333    our_mac: [u8; 6],
334    our_ipv6: Ipv6Addr,
335) -> Vec<u8> {
336    // Determine destination: unicast to NS sender, or multicast if unspecified
337    let dst_mac = ns.source_ll_addr.unwrap_or(ns_eth.src_mac);
338    let dst_ipv6 = if ns_ipv6.src_addr.is_unspecified() {
339        // All-nodes multicast
340        "ff02::1".parse().unwrap()
341    } else {
342        ns_ipv6.src_addr
343    };
344
345    // Build NA
346    let na = NeighborAdvertisement::new(our_ipv6, our_mac)
347        .solicited(!ns_ipv6.src_addr.is_unspecified());
348
349    let mut icmpv6_data = na.encode_icmpv6();
350
351    // Calculate and insert checksum
352    let checksum = icmpv6_checksum(&our_ipv6, &dst_ipv6, &icmpv6_data);
353    icmpv6_data[2] = (checksum >> 8) as u8;
354    icmpv6_data[3] = checksum as u8;
355
356    // Build IPv6 header
357    let ipv6 = Ipv6Header {
358        traffic_class: 0,
359        flow_label: 0,
360        payload_len: icmpv6_data.len() as u16,
361        next_header: IPPROTO_ICMPV6,
362        hop_limit: 255,
363        src_addr: our_ipv6,
364        dst_addr: dst_ipv6,
365    };
366
367    // Build Ethernet header
368    let eth = EthernetFrame {
369        dst_mac,
370        src_mac: our_mac,
371        ethertype: ETHERTYPE_IPV6,
372        payload_offset: 0,
373    };
374
375    // Assemble packet
376    let mut packet = Vec::with_capacity(ETH_HEADER_LEN + IPV6_HEADER_LEN + icmpv6_data.len());
377    eth.encode(&mut packet);
378    ipv6.encode(&mut packet);
379    packet.extend_from_slice(&icmpv6_data);
380
381    packet
382}
383
384/// Parse a potential NDP Neighbor Solicitation from raw packet data.
385///
386/// Returns parsed components if the packet is a valid NS, None otherwise.
387pub fn parse_neighbor_solicitation(
388    data: &[u8],
389) -> Option<(EthernetFrame, Ipv6Header, NeighborSolicitation)> {
390    let eth = EthernetFrame::parse(data)?;
391    if eth.ethertype != ETHERTYPE_IPV6 {
392        return None;
393    }
394
395    let ipv6 = Ipv6Header::parse(&data[eth.payload_offset..])?;
396    if ipv6.next_header != IPPROTO_ICMPV6 {
397        return None;
398    }
399
400    let icmpv6_offset = eth.payload_offset + IPV6_HEADER_LEN;
401    let ns = NeighborSolicitation::parse(&data[icmpv6_offset..])?;
402
403    Some((eth, ipv6, ns))
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    #[test]
411    fn parse_ethernet_frame() {
412        let data = [
413            0x33, 0x33, 0xff, 0x00, 0x01, 0x00, // dst MAC (solicited-node multicast)
414            0x02, 0x00, 0x00, 0x00, 0x01, 0x00, // src MAC
415            0x86, 0xdd, // ethertype (IPv6)
416            0x00, // payload start
417        ];
418
419        let eth = EthernetFrame::parse(&data).unwrap();
420        assert_eq!(eth.ethertype, ETHERTYPE_IPV6);
421        assert_eq!(eth.src_mac, [0x02, 0x00, 0x00, 0x00, 0x01, 0x00]);
422    }
423
424    #[test]
425    fn icmpv6_checksum_calculation() {
426        // Simple test vector
427        let src: Ipv6Addr = "fe80::1".parse().unwrap();
428        let dst: Ipv6Addr = "fe80::2".parse().unwrap();
429        let data = [136, 0, 0, 0, 0x60, 0, 0, 0]; // NA with flags
430
431        let cksum = icmpv6_checksum(&src, &dst, &data);
432        // Just verify it produces a non-zero result
433        assert_ne!(cksum, 0);
434    }
435
436    #[test]
437    fn build_neighbor_advertisement() {
438        let na = NeighborAdvertisement::new(
439            "fd00::100".parse().unwrap(),
440            [0x02, 0x00, 0x00, 0x00, 0x99, 0x00],
441        );
442
443        let icmpv6 = na.encode_icmpv6();
444        assert_eq!(icmpv6[0], ICMPV6_NEIGHBOR_ADVERTISEMENT);
445        assert_eq!(icmpv6.len(), 32); // 4 header + 4 flags + 16 target + 8 option
446    }
447}