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, seal_internet_checksum, read_u16be, PacketBuilder};
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/// Parsed IGMP message variant.
20#[derive(Debug, Clone)]
21pub enum IgmpMessage<'a> {
22    /// IGMPv1 Membership Query (general query to 0.0.0.0).
23    MembershipQueryV1 {
24        group_address: Ipv4Addr,
25    },
26    /// IGMPv2 Membership Query (group-specific or general with max response time).
27    MembershipQueryV2 {
28        max_response: u8,
29        group_address: Ipv4Addr,
30    },
31    /// IGMPv3 Membership Query with robust-ness, interval, and source list.
32    MembershipQueryV3 {
33        max_response: u8,
34        group_address: Ipv4Addr,
35        /// Suppress Router-Side Processing flag
36        supress_router_side: bool,
37        /// Querier's Robustness Variable
38        robustness: u8,
39        /// Querier's Query Interval Code
40        query_interval: u8,
41        /// Source addresses (N × 4 bytes)
42        sources: &'a [u8],
43    },
44    MembershipReportV1 {
45        group_address: Ipv4Addr,
46    },
47    MembershipReportV2 {
48        max_response: u8,
49        group_address: Ipv4Addr,
50    },
51    LeaveGroup {
52        max_response: u8,
53        group_address: Ipv4Addr,
54    },
55    MembershipReportV3 {
56        group_records: &'a [u8],
57    },
58    Unknown {
59        type_: u8,
60        data: &'a [u8],
61    },
62}
63
64/// Zero-copy IGMP packet parser.
65#[derive(Debug, Clone)]
66pub struct IgmpPacket<'a> {
67    buf: &'a [u8],
68}
69
70impl<'a> IgmpPacket<'a> {
71    crate::parser_new!(IGMP_MIN_LEN);
72
73    /// Raw bytes backing this packet.
74    #[inline]
75    pub fn as_bytes(&self) -> &'a [u8] { self.buf }
76
77    #[inline] pub fn type_(&self) -> u8 { self.buf[0] }
78    #[inline] pub fn max_response(&self) -> u8 { self.buf[1] }
79    #[inline] pub fn checksum(&self) -> u16 { read_u16be(&self.buf[2..4]) }
80    #[inline] pub fn group_address(&self) -> Ipv4Addr {
81        let b = &self.buf[4..8];
82        Ipv4Addr::new(b[0], b[1], b[2], b[3])
83    }
84    #[inline] pub fn payload(&self) -> &'a [u8] { &self.buf[IGMP_MIN_LEN..] }
85
86    pub fn verify_checksum(&self) -> bool {
87        internet_checksum(self.buf) == 0
88    }
89
90    pub fn message(&self) -> IgmpMessage<'a> {
91        let payload = self.payload();
92        match self.type_() {
93            TYPE_QUERY => {
94                let grp = self.group_address();
95                let max_resp = self.max_response();
96                // v3 query: payload contains reserved(2) + #sources(2) + sources(N×4)
97                if payload.len() >= 4 && (payload.len() - 4).is_multiple_of(4) {
98                    let sr_flags = payload[0];
99                    IgmpMessage::MembershipQueryV3 {
100                        max_response: max_resp,
101                        group_address: grp,
102                        supress_router_side: (sr_flags & 0x08) != 0,
103                        robustness: sr_flags & 0x07,
104                        query_interval: payload[1],
105                        sources: &payload[4..],
106                    }
107                } else if grp == Ipv4Addr::UNSPECIFIED && max_resp == 0 {
108                    IgmpMessage::MembershipQueryV1 {
109                        group_address: grp,
110                    }
111                } else {
112                    IgmpMessage::MembershipQueryV2 {
113                        max_response: max_resp,
114                        group_address: grp,
115                    }
116                }
117            }
118            TYPE_REPORT_V1 => IgmpMessage::MembershipReportV1 {
119                group_address: self.group_address(),
120            },
121            TYPE_REPORT_V2 => IgmpMessage::MembershipReportV2 {
122                max_response: self.max_response(),
123                group_address: self.group_address(),
124            },
125            TYPE_LEAVE_V2 => IgmpMessage::LeaveGroup {
126                max_response: self.max_response(),
127                group_address: self.group_address(),
128            },
129            TYPE_REPORT_V3 => IgmpMessage::MembershipReportV3 {
130                group_records: payload,
131            },
132            t => IgmpMessage::Unknown {
133                type_: t,
134                data: payload,
135            },
136        }
137    }
138}
139
140// ---------------------------------------------------------------------------
141// Builder
142// ---------------------------------------------------------------------------
143
144pub struct IgmpPacketBuilder {
145    buf: Vec<u8>,
146    payload: Option<Vec<u8>>,
147}
148
149impl Default for IgmpPacketBuilder {
150    fn default() -> Self { Self::new(TYPE_QUERY, 0) }
151}
152
153impl IgmpPacketBuilder {
154    pub fn new(type_: u8, max_response: u8) -> Self {
155        let mut buf = vec![0u8; IGMP_MIN_LEN];
156        buf[0] = type_;
157        buf[1] = max_response;
158        Self { buf, payload: None }
159    }
160
161    pub fn query_v1() -> Self { Self::new(TYPE_QUERY, 0) }
162    pub fn query_v2(max_resp: u8) -> Self { Self::new(TYPE_QUERY, max_resp) }
163    pub fn report_v2(addr: Ipv4Addr) -> Self {
164        let mut s = Self::new(TYPE_REPORT_V2, 0);
165        s.buf[4..8].copy_from_slice(&addr.octets());
166        s
167    }
168    pub fn leave_v2(addr: Ipv4Addr) -> Self {
169        let mut s = Self::new(TYPE_LEAVE_V2, 0);
170        s.buf[4..8].copy_from_slice(&addr.octets());
171        s
172    }
173
174    pub fn group_address(mut self, addr: Ipv4Addr) -> Self {
175        self.buf[4..8].copy_from_slice(&addr.octets());
176        self
177    }
178
179    pub fn max_response(mut self, v: u8) -> Self {
180        self.buf[1] = v;
181        self
182    }
183
184    pub fn payload(mut self, data: &[u8]) -> Self {
185        self.payload = Some(data.to_vec());
186        self
187    }
188
189    pub fn build(self) -> Vec<u8> {
190        <Self as PacketBuilder>::build(self)
191    }
192}
193
194impl PacketBuilder for IgmpPacketBuilder {
195    type Output = Vec<u8>;
196
197    fn build(self) -> Vec<u8> {
198        let mut pkt = self.buf;
199        if let Some(p) = self.payload {
200            pkt.extend_from_slice(&p);
201        }
202        seal_internet_checksum(&mut pkt, 2);
203        pkt
204    }
205}
206
207// ---------------------------------------------------------------------------
208// Tests
209// ---------------------------------------------------------------------------
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn parse_v1_query() {
217        let data = &[0x11u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
218        let pkt = IgmpPacket::new(data).unwrap();
219        assert_eq!(pkt.type_(), TYPE_QUERY);
220        match pkt.message() {
221            IgmpMessage::MembershipQueryV1 { group_address } => {
222                assert_eq!(group_address, Ipv4Addr::UNSPECIFIED);
223            }
224            _ => panic!("expected MembershipQueryV1"),
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::MembershipQueryV2 { max_response, group_address } => {
236                assert_eq!(max_response, 100);
237                let _ = group_address;
238            }
239            _ => panic!("expected MembershipQueryV2"),
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}