Skip to main content

wireforge_core/
igmp.rs

1//! IGMP (Internet Group Management Protocol) packet parser and builder.
2//! Supports IGMPv1 (RFC 1112), IGMPv2 (RFC 2236), and IGMPv3 basic (RFC 3376).
3
4use alloc::vec;
5use alloc::vec::Vec;
6use core::net::Ipv4Addr;
7
8use crate::util::{internet_checksum, read_u16be, write_u16be};
9
10pub const IGMP_MIN_LEN: usize = 8;
11
12// Message type constants
13const TYPE_QUERY: u8 = 0x11;
14const TYPE_REPORT_V1: u8 = 0x12;
15const TYPE_REPORT_V2: u8 = 0x16;
16const TYPE_LEAVE_V2: u8 = 0x17;
17const TYPE_REPORT_V3: u8 = 0x22;
18
19/// IGMP protocol version.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum IgmpVersion { V1, V2, V3 }
22
23/// Parsed IGMP message variant.
24#[derive(Debug, Clone)]
25pub enum IgmpMessage<'a> {
26    MembershipQuery {
27        version: IgmpVersion,
28        max_response: u8,
29        group_address: Ipv4Addr,
30        /// v3: Suppress Router-Side Processing flag
31        supress_router_side: bool,
32        /// v3: Querier's Robustness Variable
33        robustness: u8,
34        /// v3: Querier's Query Interval Code
35        query_interval: u8,
36        /// v3: Source addresses (N × 4 bytes)
37        sources: &'a [u8],
38    },
39    MembershipReportV1 {
40        group_address: Ipv4Addr,
41    },
42    MembershipReportV2 {
43        max_response: u8,
44        group_address: Ipv4Addr,
45    },
46    LeaveGroup {
47        max_response: u8,
48        group_address: Ipv4Addr,
49    },
50    MembershipReportV3 {
51        group_records: &'a [u8],
52    },
53    Unknown {
54        type_: u8,
55        data: &'a [u8],
56    },
57}
58
59/// Zero-copy IGMP packet parser.
60#[derive(Debug, Clone)]
61pub struct IgmpPacket<'a> {
62    buf: &'a [u8],
63}
64
65impl<'a> IgmpPacket<'a> {
66    pub fn new(buf: &'a [u8]) -> Option<Self> {
67        if buf.len() < IGMP_MIN_LEN { return None; }
68        Some(Self { buf })
69    }
70
71    #[inline] pub fn type_(&self) -> u8 { self.buf[0] }
72    #[inline] pub fn max_response(&self) -> u8 { self.buf[1] }
73    #[inline] pub fn checksum(&self) -> u16 { read_u16be(&self.buf[2..4]) }
74    #[inline] pub fn group_address(&self) -> Ipv4Addr {
75        let b = &self.buf[4..8];
76        Ipv4Addr::new(b[0], b[1], b[2], b[3])
77    }
78    #[inline] pub fn payload(&self) -> &'a [u8] { &self.buf[IGMP_MIN_LEN..] }
79
80    pub fn verify_checksum(&self) -> bool {
81        internet_checksum(self.buf) == 0
82    }
83
84    pub fn message(&self) -> IgmpMessage<'a> {
85        let payload = self.payload();
86        match self.type_() {
87            TYPE_QUERY => {
88                let grp = self.group_address();
89                if payload.len() >= 4 {
90                    // v3 query has extra fields
91                    let sr_flags = payload[0];
92                    IgmpMessage::MembershipQuery {
93                        version: IgmpVersion::V3,
94                        max_response: self.max_response(),
95                        group_address: grp,
96                        supress_router_side: (sr_flags & 0x08) != 0,
97                        robustness: sr_flags & 0x07,
98                        query_interval: payload[1],
99                        sources: &payload[4..],
100                    }
101                } else if grp == Ipv4Addr::UNSPECIFIED {
102                    IgmpMessage::MembershipQuery {
103                        version: IgmpVersion::V1,
104                        max_response: 0,
105                        group_address: grp,
106                        supress_router_side: false,
107                        robustness: 0,
108                        query_interval: 0,
109                        sources: &[],
110                    }
111                } else {
112                    IgmpMessage::MembershipQuery {
113                        version: IgmpVersion::V2,
114                        max_response: self.max_response(),
115                        group_address: grp,
116                        supress_router_side: false,
117                        robustness: 0,
118                        query_interval: 0,
119                        sources: &[],
120                    }
121                }
122            }
123            TYPE_REPORT_V1 => IgmpMessage::MembershipReportV1 {
124                group_address: self.group_address(),
125            },
126            TYPE_REPORT_V2 => IgmpMessage::MembershipReportV2 {
127                max_response: self.max_response(),
128                group_address: self.group_address(),
129            },
130            TYPE_LEAVE_V2 => IgmpMessage::LeaveGroup {
131                max_response: self.max_response(),
132                group_address: self.group_address(),
133            },
134            TYPE_REPORT_V3 => IgmpMessage::MembershipReportV3 {
135                group_records: payload,
136            },
137            t => IgmpMessage::Unknown {
138                type_: t,
139                data: payload,
140            },
141        }
142    }
143}
144
145// ---------------------------------------------------------------------------
146// Builder
147// ---------------------------------------------------------------------------
148
149pub struct IgmpPacketBuilder {
150    buf: Vec<u8>,
151    payload: Option<Vec<u8>>,
152}
153
154impl Default for IgmpPacketBuilder {
155    fn default() -> Self { Self::new(TYPE_QUERY, 0) }
156}
157
158impl IgmpPacketBuilder {
159    pub fn new(type_: u8, max_response: u8) -> Self {
160        let mut buf = vec![0u8; IGMP_MIN_LEN];
161        buf[0] = type_;
162        buf[1] = max_response;
163        Self { buf, payload: None }
164    }
165
166    pub fn query_v1() -> Self { Self::new(TYPE_QUERY, 0) }
167    pub fn query_v2(max_resp: u8) -> Self { Self::new(TYPE_QUERY, max_resp) }
168    pub fn report_v2(addr: Ipv4Addr) -> Self {
169        let mut s = Self::new(TYPE_REPORT_V2, 0);
170        s.buf[4..8].copy_from_slice(&addr.octets());
171        s
172    }
173    pub fn leave_v2(addr: Ipv4Addr) -> Self {
174        let mut s = Self::new(TYPE_LEAVE_V2, 0);
175        s.buf[4..8].copy_from_slice(&addr.octets());
176        s
177    }
178
179    pub fn group_address(mut self, addr: Ipv4Addr) -> Self {
180        self.buf[4..8].copy_from_slice(&addr.octets());
181        self
182    }
183
184    pub fn max_response(mut self, v: u8) -> Self {
185        self.buf[1] = v;
186        self
187    }
188
189    pub fn payload(mut self, data: &[u8]) -> Self {
190        self.payload = Some(data.to_vec());
191        self
192    }
193
194    pub fn build(mut self) -> Vec<u8> {
195        self.buf[2] = 0; self.buf[3] = 0;
196        let mut pkt = self.buf;
197        if let Some(p) = self.payload {
198            pkt.extend_from_slice(&p);
199        }
200        let csum = internet_checksum(&pkt);
201        write_u16be(&mut pkt[2..4], csum);
202        pkt
203    }
204}
205
206// ---------------------------------------------------------------------------
207// Tests
208// ---------------------------------------------------------------------------
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn parse_v1_query() {
216        let data = &[0x11u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
217        let pkt = IgmpPacket::new(data).unwrap();
218        assert_eq!(pkt.type_(), TYPE_QUERY);
219        match pkt.message() {
220            IgmpMessage::MembershipQuery { version, group_address, .. } => {
221                assert_eq!(version, IgmpVersion::V1);
222                assert_eq!(group_address, Ipv4Addr::UNSPECIFIED);
223            }
224            _ => panic!("expected MembershipQuery"),
225        }
226    }
227
228    #[test]
229    fn parse_v2_query() {
230        let mut data = vec![0x11u8, 100, 0x00, 0x00];
231        data.extend_from_slice(&[224u8, 0, 0, 1]); // group=224.0.0.1
232        let pkt = IgmpPacket::new(&data).unwrap();
233        assert_eq!(pkt.max_response(), 100);
234        match pkt.message() {
235            IgmpMessage::MembershipQuery { version, max_response, .. } => {
236                assert_eq!(version, IgmpVersion::V2);
237                assert_eq!(max_response, 100);
238            }
239            _ => panic!("expected MembershipQuery v2"),
240        }
241    }
242
243    #[test]
244    fn parse_v2_report() {
245        let data = &[0x16u8, 0x00, 0x00, 0x00, 224, 0, 0, 1];
246        let pkt = IgmpPacket::new(data).unwrap();
247        match pkt.message() {
248            IgmpMessage::MembershipReportV2 { group_address, .. } => {
249                assert_eq!(group_address, Ipv4Addr::new(224, 0, 0, 1));
250            }
251            _ => panic!("expected MembershipReportV2"),
252        }
253    }
254
255    #[test]
256    fn parse_v2_leave() {
257        let data = &[0x17u8, 0x00, 0x00, 0x00, 224, 0, 0, 1];
258        let pkt = IgmpPacket::new(data).unwrap();
259        match pkt.message() {
260            IgmpMessage::LeaveGroup { group_address, .. } => {
261                assert_eq!(group_address, Ipv4Addr::new(224, 0, 0, 1));
262            }
263            _ => panic!("expected LeaveGroup"),
264        }
265    }
266
267    #[test]
268    fn parse_too_short() {
269        assert!(IgmpPacket::new(&[]).is_none());
270        assert!(IgmpPacket::new(&[0u8; 7]).is_none());
271    }
272
273    #[test]
274    fn verify_checksum() {
275        let pkt = IgmpPacketBuilder::query_v1().build();
276        let parsed = IgmpPacket::new(&pkt).unwrap();
277        assert!(parsed.verify_checksum());
278    }
279
280    #[test]
281    fn build_roundtrip() {
282        let addr = Ipv4Addr::new(224, 0, 0, 1);
283        let pkt = IgmpPacketBuilder::report_v2(addr)
284            .payload(&[0xAA, 0xBB])
285            .build();
286        let parsed = IgmpPacket::new(&pkt).unwrap();
287        assert_eq!(parsed.type_(), TYPE_REPORT_V2);
288        assert_eq!(parsed.group_address(), addr);
289        assert_eq!(parsed.payload(), &[0xAA, 0xBB]);
290        assert!(parsed.verify_checksum());
291    }
292}