Skip to main content

stackforge_core/layer/icmp/
extensions.rs

1//! ICMP Extensions Framework (RFC 4884)
2//!
3//! ICMP error messages can include extensions that provide additional diagnostic
4//! information. The extensions framework defines:
5//! - Extension Header (4 bytes: version, reserved, checksum)
6//! - Extension Objects (variable length: len, classnum, classtype, data)
7//!
8//! Common extension classes:
9//! - Class 1: MPLS Label Stack
10//! - Class 2: Interface Information (RFC 5837)
11//! - Class 3: Interface Identification
12
13use crate::layer::icmp::checksum::icmp_checksum;
14use std::net::{Ipv4Addr, Ipv6Addr};
15
16/// ICMP Extension class numbers (IANA registry)
17pub mod class_nums {
18    pub const MPLS: u8 = 1;
19    pub const INTERFACE_INFORMATION: u8 = 2;
20    pub const INTERFACE_IDENTIFICATION: u8 = 3;
21}
22
23/// Get human-readable name for extension class number
24pub fn class_name(classnum: u8) -> &'static str {
25    match classnum {
26        class_nums::MPLS => "MPLS",
27        class_nums::INTERFACE_INFORMATION => "Interface Information",
28        class_nums::INTERFACE_IDENTIFICATION => "Interface Identification",
29        _ => "unknown",
30    }
31}
32
33/// ICMP Extension Header offsets (RFC 4884)
34pub mod header_offsets {
35    pub const VERSION_RESERVED: usize = 0; // Version (4 bits) + Reserved (12 bits)
36    pub const CHECKSUM: usize = 2;
37}
38
39pub const EXTENSION_HEADER_LEN: usize = 4;
40
41/// ICMP Extension Object offsets
42pub mod object_offsets {
43    pub const LEN: usize = 0;
44    pub const CLASSNUM: usize = 2;
45    pub const CLASSTYPE: usize = 3;
46    pub const DATA: usize = 4;
47}
48
49pub const EXTENSION_OBJECT_MIN_LEN: usize = 4;
50
51/// Interface Information Object flags (RFC 5837)
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct InterfaceInfoFlags {
54    pub has_ifindex: bool,
55    pub has_ipaddr: bool,
56    pub has_ifname: bool,
57    pub has_mtu: bool,
58}
59
60impl InterfaceInfoFlags {
61    /// Parse flags from the classtype byte
62    pub fn from_byte(byte: u8) -> Self {
63        Self {
64            has_ifindex: (byte & 0b0001_0000) != 0,
65            has_ipaddr: (byte & 0b0000_1000) != 0,
66            has_ifname: (byte & 0b0000_0100) != 0,
67            has_mtu: (byte & 0b0000_0001) != 0,
68        }
69    }
70
71    /// Convert flags to byte representation
72    pub fn to_byte(&self) -> u8 {
73        let mut byte = 0u8;
74        if self.has_ifindex {
75            byte |= 0b0001_0000;
76        }
77        if self.has_ipaddr {
78            byte |= 0b0000_1000;
79        }
80        if self.has_ifname {
81            byte |= 0b0000_0100;
82        }
83        if self.has_mtu {
84            byte |= 0b0000_0001;
85        }
86        byte
87    }
88}
89
90/// Address Family Identifier (AFI) for interface IP addresses
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum Afi {
93    Ipv4 = 1,
94    Ipv6 = 2,
95}
96
97/// Parsed Interface Information extension data (RFC 5837)
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct InterfaceInformation {
100    pub flags: InterfaceInfoFlags,
101    pub ifindex: Option<u32>,
102    pub ip_addr: Option<IpAddr>,
103    pub ifname: Option<String>,
104    pub mtu: Option<u32>,
105}
106
107/// IP address enum for interface information
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum IpAddr {
110    V4(Ipv4Addr),
111    V6(Ipv6Addr),
112}
113
114/// Check if ICMP extensions are present in an ICMP error message.
115///
116/// RFC 4884 specifies that extensions are indicated by a length field in the
117/// ICMP header being non-zero. For error messages, the original datagram
118/// is padded to 128 bytes minimum, and extensions follow.
119pub fn has_extensions(icmp_type: u8, payload_len: usize) -> bool {
120    use crate::layer::icmp::error;
121
122    // Only error messages can have extensions
123    if !error::is_error_type(icmp_type) {
124        return false;
125    }
126
127    // Extensions require at least the minimum error payload (28 bytes)
128    // plus space for the extension header (4 bytes)
129    if payload_len < error::ERROR_MIN_PAYLOAD + EXTENSION_HEADER_LEN {
130        return false;
131    }
132
133    // Additional heuristic: check if there's enough data after the required
134    // error payload for a valid extension header + object
135    payload_len >= 128 + EXTENSION_HEADER_LEN + EXTENSION_OBJECT_MIN_LEN
136}
137
138/// Get the offset where ICMP extensions begin in an error message.
139///
140/// Returns None if extensions are not present.
141pub fn extension_offset(icmp_header_start: usize, icmp_payload: &[u8]) -> Option<usize> {
142    // Extensions start after the padded original datagram (128 bytes minimum)
143    // RFC 4884 Section 4.1: The original datagram is padded to 128 bytes
144    if icmp_payload.len() >= 128 + EXTENSION_HEADER_LEN {
145        Some(icmp_header_start + 8 + 128) // ICMP header (8) + padded payload (128)
146    } else {
147        None
148    }
149}
150
151/// Parse the ICMP Extension Header
152pub fn parse_extension_header(buf: &[u8], offset: usize) -> Option<(u8, u16)> {
153    if offset + EXTENSION_HEADER_LEN > buf.len() {
154        return None;
155    }
156
157    let version_reserved = u16::from_be_bytes([buf[offset], buf[offset + 1]]);
158    let version = ((version_reserved >> 12) & 0x0F) as u8;
159    let checksum = u16::from_be_bytes([buf[offset + 2], buf[offset + 3]]);
160
161    // Version should be 2 per RFC 4884
162    if version != 2 {
163        return None;
164    }
165
166    Some((version, checksum))
167}
168
169/// Verify the extension header checksum
170pub fn verify_extension_checksum(buf: &[u8], ext_start: usize, ext_len: usize) -> bool {
171    if ext_start + ext_len > buf.len() || ext_len < EXTENSION_HEADER_LEN {
172        return false;
173    }
174
175    // Create a copy with checksum field zeroed
176    let mut data = vec![0u8; ext_len];
177    data.copy_from_slice(&buf[ext_start..ext_start + ext_len]);
178    data[2] = 0;
179    data[3] = 0;
180
181    let computed = icmp_checksum(&data);
182    let stored = u16::from_be_bytes([buf[ext_start + 2], buf[ext_start + 3]]);
183
184    computed == stored
185}
186
187/// Parse an Extension Object header
188pub fn parse_extension_object(buf: &[u8], offset: usize) -> Option<(u16, u8, u8, usize)> {
189    if offset + EXTENSION_OBJECT_MIN_LEN > buf.len() {
190        return None;
191    }
192
193    let len = u16::from_be_bytes([buf[offset], buf[offset + 1]]);
194    let classnum = buf[offset + 2];
195    let classtype = buf[offset + 3];
196
197    // Length must be at least 4 bytes and a multiple of 4 (RFC 4884)
198    if len < 4 || len % 4 != 0 {
199        return None;
200    }
201
202    let data_offset = offset + EXTENSION_OBJECT_MIN_LEN;
203
204    Some((len, classnum, classtype, data_offset))
205}
206
207/// Parse Interface Information extension object (RFC 5837)
208pub fn parse_interface_information(
209    buf: &[u8],
210    data_offset: usize,
211    data_len: usize,
212) -> Option<InterfaceInformation> {
213    if data_len == 0 {
214        return None;
215    }
216
217    // First byte after the object header contains the flags
218    let flags_byte = buf[data_offset];
219    let flags = InterfaceInfoFlags::from_byte(flags_byte);
220
221    let mut offset = data_offset + 1; // Skip flags byte
222    let mut result = InterfaceInformation {
223        flags,
224        ifindex: None,
225        ip_addr: None,
226        ifname: None,
227        mtu: None,
228    };
229
230    // Parse conditional fields based on flags
231    if flags.has_ifindex {
232        if offset + 4 > buf.len() {
233            return None;
234        }
235        result.ifindex = Some(u32::from_be_bytes([
236            buf[offset],
237            buf[offset + 1],
238            buf[offset + 2],
239            buf[offset + 3],
240        ]));
241        offset += 4;
242    }
243
244    if flags.has_ipaddr {
245        if offset + 4 > buf.len() {
246            return None;
247        }
248        let afi = u16::from_be_bytes([buf[offset], buf[offset + 1]]);
249        offset += 4; // AFI (2) + Reserved (2)
250
251        match afi {
252            1 => {
253                // IPv4
254                if offset + 4 > buf.len() {
255                    return None;
256                }
257                let ip = Ipv4Addr::new(
258                    buf[offset],
259                    buf[offset + 1],
260                    buf[offset + 2],
261                    buf[offset + 3],
262                );
263                result.ip_addr = Some(IpAddr::V4(ip));
264                offset += 4;
265            },
266            2 => {
267                // IPv6
268                if offset + 16 > buf.len() {
269                    return None;
270                }
271                let mut octets = [0u8; 16];
272                octets.copy_from_slice(&buf[offset..offset + 16]);
273                result.ip_addr = Some(IpAddr::V6(Ipv6Addr::from(octets)));
274                offset += 16;
275            },
276            _ => return None, // Unknown AFI
277        }
278    }
279
280    if flags.has_ifname {
281        if offset + 1 > buf.len() {
282            return None;
283        }
284        let name_len = buf[offset] as usize;
285        offset += 1;
286
287        if offset + name_len > buf.len() {
288            return None;
289        }
290        let name_bytes = &buf[offset..offset + name_len];
291        if let Ok(name) = String::from_utf8(name_bytes.to_vec()) {
292            result.ifname = Some(name);
293        }
294        offset += name_len;
295    }
296
297    if flags.has_mtu {
298        if offset + 4 > buf.len() {
299            return None;
300        }
301        result.mtu = Some(u32::from_be_bytes([
302            buf[offset],
303            buf[offset + 1],
304            buf[offset + 2],
305            buf[offset + 3],
306        ]));
307    }
308
309    Some(result)
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn test_interface_info_flags() {
318        let flags = InterfaceInfoFlags {
319            has_ifindex: true,
320            has_ipaddr: true,
321            has_ifname: false,
322            has_mtu: true,
323        };
324
325        let byte = flags.to_byte();
326        assert_eq!(byte, 0b0001_1001);
327
328        let parsed = InterfaceInfoFlags::from_byte(byte);
329        assert_eq!(parsed, flags);
330    }
331
332    #[test]
333    fn test_parse_extension_header() {
334        // Version 2, reserved=0, checksum=0x1234
335        let buf = [0x20, 0x00, 0x12, 0x34];
336        let result = parse_extension_header(&buf, 0);
337        assert_eq!(result, Some((2, 0x1234)));
338    }
339
340    #[test]
341    fn test_parse_extension_object() {
342        // len=8, classnum=2, classtype=0x19
343        let buf = [0x00, 0x08, 0x02, 0x19, 0x00, 0x00, 0x00, 0x00];
344        let result = parse_extension_object(&buf, 0);
345        assert_eq!(result, Some((8, 2, 0x19, 4)));
346    }
347
348    #[test]
349    fn test_class_name() {
350        assert_eq!(class_name(class_nums::MPLS), "MPLS");
351        assert_eq!(
352            class_name(class_nums::INTERFACE_INFORMATION),
353            "Interface Information"
354        );
355        assert_eq!(class_name(99), "unknown");
356    }
357
358    #[test]
359    fn test_has_extensions() {
360        use crate::layer::icmp::types::types;
361
362        // Time exceeded with sufficient payload
363        assert!(has_extensions(types::TIME_EXCEEDED, 200));
364
365        // Echo request (not an error type)
366        assert!(!has_extensions(types::ECHO_REQUEST, 200));
367
368        // Error type but insufficient payload
369        assert!(!has_extensions(types::TIME_EXCEEDED, 30));
370    }
371}