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