Skip to main content

microsandbox_network/packet/
frame.rs

1//! Unified frame parser wrapping `etherparse::SlicedPacket`.
2
3use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
4
5use etherparse::{IpNumber, SlicedPacket};
6
7//--------------------------------------------------------------------------------------------------
8// Constants
9//--------------------------------------------------------------------------------------------------
10
11/// DNS port.
12pub const DNS_PORT: u16 = 53;
13
14//--------------------------------------------------------------------------------------------------
15// Types
16//--------------------------------------------------------------------------------------------------
17
18/// A parsed ethernet frame with convenient accessors for policy matching.
19///
20/// Wraps `etherparse::SlicedPacket` for zero-copy header inspection.
21/// Supports Ethernet II, ARP, IPv4, IPv6, TCP, UDP, ICMPv4, and
22/// ICMPv6 (including NDP) via etherparse.
23pub struct ParsedFrame<'a> {
24    /// The raw frame bytes.
25    raw: &'a [u8],
26
27    /// Parsed header slices from etherparse.
28    sliced: SlicedPacket<'a>,
29}
30
31/// IP protocol number for policy matching.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum IpProtocol {
34    /// TCP (6).
35    Tcp,
36
37    /// UDP (17).
38    Udp,
39
40    /// ICMPv4 (1).
41    Icmpv4,
42
43    /// ICMPv6 (58).
44    Icmpv6,
45
46    /// Other protocol number.
47    Other(u8),
48}
49
50//--------------------------------------------------------------------------------------------------
51// Methods
52//--------------------------------------------------------------------------------------------------
53
54impl<'a> ParsedFrame<'a> {
55    /// Parse an ethernet frame. Returns `None` if the frame is malformed.
56    pub fn parse(raw: &'a [u8]) -> Option<Self> {
57        let sliced = SlicedPacket::from_ethernet(raw).ok()?;
58        Some(Self { raw, sliced })
59    }
60
61    /// Get the raw frame bytes.
62    pub fn raw(&self) -> &'a [u8] {
63        self.raw
64    }
65
66    /// Get the inner `SlicedPacket` for direct access to etherparse types.
67    pub fn sliced(&self) -> &SlicedPacket<'a> {
68        &self.sliced
69    }
70
71    /// Get the EtherType as a raw `u16`.
72    pub fn ethertype(&self) -> Option<u16> {
73        match &self.sliced.link {
74            Some(etherparse::LinkSlice::Ethernet2(header)) => Some(header.ether_type().0),
75            _ => None,
76        }
77    }
78
79    /// Returns `true` if the frame is an ARP packet.
80    pub fn is_arp(&self) -> bool {
81        matches!(&self.sliced.net, Some(etherparse::NetSlice::Arp(_)))
82    }
83
84    /// Returns `true` if the frame is an IPv6 NDP (Neighbor Discovery) message.
85    ///
86    /// NDP uses ICMPv6 types 133–137: Router Solicitation, Router Advertisement,
87    /// Neighbor Solicitation, Neighbor Advertisement, and Redirect.
88    pub fn is_ndp(&self) -> bool {
89        match &self.sliced.transport {
90            Some(etherparse::TransportSlice::Icmpv6(icmpv6)) => {
91                let icmp_type = icmpv6.type_u8();
92                (133..=137).contains(&icmp_type)
93            }
94            _ => false,
95        }
96    }
97
98    /// Get the source MAC address (6 bytes).
99    pub fn src_mac(&self) -> Option<[u8; 6]> {
100        match &self.sliced.link {
101            Some(etherparse::LinkSlice::Ethernet2(header)) => Some(header.source()),
102            _ => None,
103        }
104    }
105
106    /// Get the destination MAC address (6 bytes).
107    pub fn dst_mac(&self) -> Option<[u8; 6]> {
108        match &self.sliced.link {
109            Some(etherparse::LinkSlice::Ethernet2(header)) => Some(header.destination()),
110            _ => None,
111        }
112    }
113
114    /// Get the source IP address.
115    pub fn src_ip(&self) -> Option<IpAddr> {
116        match &self.sliced.net {
117            Some(etherparse::NetSlice::Ipv4(h)) => {
118                Some(IpAddr::V4(Ipv4Addr::from(h.header().source())))
119            }
120            Some(etherparse::NetSlice::Ipv6(h)) => {
121                Some(IpAddr::V6(Ipv6Addr::from(h.header().source())))
122            }
123            _ => None,
124        }
125    }
126
127    /// Get the destination IP address.
128    pub fn dst_ip(&self) -> Option<IpAddr> {
129        match &self.sliced.net {
130            Some(etherparse::NetSlice::Ipv4(h)) => {
131                Some(IpAddr::V4(Ipv4Addr::from(h.header().destination())))
132            }
133            Some(etherparse::NetSlice::Ipv6(h)) => {
134                Some(IpAddr::V6(Ipv6Addr::from(h.header().destination())))
135            }
136            _ => None,
137        }
138    }
139
140    /// Get the IP protocol.
141    pub fn protocol(&self) -> Option<IpProtocol> {
142        match &self.sliced.net {
143            Some(etherparse::NetSlice::Ipv4(h)) => {
144                Some(ip_number_to_protocol(h.header().protocol()))
145            }
146            Some(etherparse::NetSlice::Ipv6(h)) => {
147                // The payload's ip_number reflects the final next-header after
148                // all extension headers have been traversed.
149                Some(ip_number_to_protocol(h.payload().ip_number))
150            }
151            _ => None,
152        }
153    }
154
155    /// Get the source port (TCP or UDP).
156    pub fn src_port(&self) -> Option<u16> {
157        match &self.sliced.transport {
158            Some(etherparse::TransportSlice::Tcp(h)) => Some(h.source_port()),
159            Some(etherparse::TransportSlice::Udp(h)) => Some(h.source_port()),
160            _ => None,
161        }
162    }
163
164    /// Get the destination port (TCP or UDP).
165    pub fn dst_port(&self) -> Option<u16> {
166        match &self.sliced.transport {
167            Some(etherparse::TransportSlice::Tcp(h)) => Some(h.destination_port()),
168            Some(etherparse::TransportSlice::Udp(h)) => Some(h.destination_port()),
169            _ => None,
170        }
171    }
172
173    /// Returns `true` if this is a DNS packet (UDP or TCP to/from port 53).
174    pub fn is_dns(&self) -> bool {
175        self.dst_port() == Some(DNS_PORT) || self.src_port() == Some(DNS_PORT)
176    }
177
178    /// Get the transport-layer payload (e.g. DNS wire data).
179    ///
180    /// For TCP/UDP, returns the data after the transport header.
181    /// For other protocols, returns the IP payload.
182    pub fn payload(&self) -> &'a [u8] {
183        match &self.sliced.transport {
184            Some(etherparse::TransportSlice::Tcp(h)) => h.payload(),
185            Some(etherparse::TransportSlice::Udp(h)) => h.payload(),
186            _ => {
187                // Fall back to IP payload for non-TCP/UDP.
188                match &self.sliced.net {
189                    Some(etherparse::NetSlice::Ipv4(h)) => h.payload().payload,
190                    Some(etherparse::NetSlice::Ipv6(h)) => h.payload().payload,
191                    _ => &[],
192                }
193            }
194        }
195    }
196}
197
198//--------------------------------------------------------------------------------------------------
199// Functions
200//--------------------------------------------------------------------------------------------------
201
202fn ip_number_to_protocol(n: IpNumber) -> IpProtocol {
203    match n {
204        IpNumber::TCP => IpProtocol::Tcp,
205        IpNumber::UDP => IpProtocol::Udp,
206        IpNumber::ICMP => IpProtocol::Icmpv4,
207        IpNumber::IPV6_ICMP => IpProtocol::Icmpv6,
208        other => IpProtocol::Other(other.0),
209    }
210}
211
212//--------------------------------------------------------------------------------------------------
213// Tests
214//--------------------------------------------------------------------------------------------------
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    /// Build a minimal valid Ethernet + IPv4 + UDP frame.
221    fn build_udp_frame(
222        src_ip: [u8; 4],
223        dst_ip: [u8; 4],
224        src_port: u16,
225        dst_port: u16,
226        payload: &[u8],
227    ) -> Vec<u8> {
228        use etherparse::PacketBuilder;
229
230        let builder = PacketBuilder::ethernet2(
231            [0x02, 0x00, 0x00, 0x00, 0x00, 0x01], // src mac
232            [0x02, 0x00, 0x00, 0x00, 0x00, 0x02], // dst mac
233        )
234        .ipv4(src_ip, dst_ip, 64)
235        .udp(src_port, dst_port);
236
237        let mut buf = Vec::new();
238        builder.write(&mut buf, payload).unwrap();
239        buf
240    }
241
242    /// Build a minimal valid Ethernet + IPv4 + TCP frame.
243    fn build_tcp_frame(src_ip: [u8; 4], dst_ip: [u8; 4], src_port: u16, dst_port: u16) -> Vec<u8> {
244        use etherparse::PacketBuilder;
245
246        let builder = PacketBuilder::ethernet2(
247            [0x02, 0x00, 0x00, 0x00, 0x00, 0x01],
248            [0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
249        )
250        .ipv4(src_ip, dst_ip, 64)
251        .tcp(src_port, dst_port, 0, 65535);
252
253        let mut buf = Vec::new();
254        builder.write(&mut buf, &[]).unwrap();
255        buf
256    }
257
258    #[test]
259    fn test_parse_udp_frame() {
260        let frame = build_udp_frame([10, 0, 0, 1], [10, 0, 0, 2], 12345, 80, b"hello");
261        let parsed = ParsedFrame::parse(&frame).unwrap();
262
263        assert_eq!(
264            parsed.src_ip(),
265            Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))
266        );
267        assert_eq!(
268            parsed.dst_ip(),
269            Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)))
270        );
271        assert_eq!(parsed.src_port(), Some(12345));
272        assert_eq!(parsed.dst_port(), Some(80));
273        assert_eq!(parsed.protocol(), Some(IpProtocol::Udp));
274        assert!(!parsed.is_dns());
275        assert!(!parsed.is_arp());
276        assert_eq!(parsed.payload(), b"hello");
277    }
278
279    #[test]
280    fn test_parse_dns_frame() {
281        let frame = build_udp_frame([10, 0, 0, 1], [10, 0, 0, 2], 50000, DNS_PORT, &[0; 12]);
282        let parsed = ParsedFrame::parse(&frame).unwrap();
283
284        assert!(parsed.is_dns());
285        assert_eq!(parsed.dst_port(), Some(DNS_PORT));
286    }
287
288    #[test]
289    fn test_parse_tcp_frame() {
290        let frame = build_tcp_frame([192, 168, 1, 1], [93, 184, 216, 34], 45000, 443);
291        let parsed = ParsedFrame::parse(&frame).unwrap();
292
293        assert_eq!(parsed.protocol(), Some(IpProtocol::Tcp));
294        assert_eq!(parsed.src_port(), Some(45000));
295        assert_eq!(parsed.dst_port(), Some(443));
296    }
297
298    #[test]
299    fn test_parse_mac_addresses() {
300        let frame = build_udp_frame([10, 0, 0, 1], [10, 0, 0, 2], 1234, 5678, &[]);
301        let parsed = ParsedFrame::parse(&frame).unwrap();
302
303        assert_eq!(parsed.src_mac(), Some([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]));
304        assert_eq!(parsed.dst_mac(), Some([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]));
305    }
306
307    #[test]
308    fn test_parse_garbage_returns_none() {
309        let garbage = [0xff; 5];
310        assert!(ParsedFrame::parse(&garbage).is_none());
311    }
312}