Skip to main content

vcl_protocol/
ip_packet.rs

1//! # VCL IP Packet Parser
2//!
3//! Full parsing of IPv4/IPv6 packets captured from the TUN interface.
4//! Extracts transport layer headers (TCP, UDP, ICMP) for routing decisions.
5//!
6//! ## Example
7//!
8//! ```rust
9//! use vcl_protocol::ip_packet::{ParsedPacket, TransportProtocol};
10//!
11//! // Minimal IPv4/TCP packet
12//! let raw = vec![
13//!     0x45, 0x00, 0x00, 0x28,
14//!     0x00, 0x01, 0x00, 0x00,
15//!     0x40, 0x06, 0x00, 0x00,
16//!     192, 168, 1, 1,
17//!     10, 0, 0, 1,
18//!     0x00, 0x50, 0x1F, 0x90, // src=80, dst=8080
19//!     0x00, 0x00, 0x00, 0x01,
20//!     0x00, 0x00, 0x00, 0x01,
21//!     0x50, 0x02, 0x20, 0x00,
22//!     0x00, 0x00, 0x00, 0x00,
23//! ];
24//!
25//! let packet = ParsedPacket::parse(raw).unwrap();
26//! assert!(matches!(packet.transport, TransportProtocol::Tcp { .. }));
27//! ```
28
29use crate::error::VCLError;
30use etherparse::{
31    Ipv4HeaderSlice, Ipv6HeaderSlice,
32    TcpHeaderSlice, UdpHeaderSlice,
33};
34use tracing::debug;
35
36/// IP version of a parsed packet.
37#[derive(Debug, Clone, PartialEq)]
38pub enum IpVersion {
39    V4,
40    V6,
41}
42
43/// Transport layer protocol extracted from an IP packet.
44#[derive(Debug, Clone, PartialEq)]
45pub enum TransportProtocol {
46    /// TCP segment with source/destination ports and flags.
47    Tcp {
48        src_port: u16,
49        dst_port: u16,
50        syn: bool,
51        ack: bool,
52        fin: bool,
53        rst: bool,
54        payload_offset: usize,
55    },
56    /// UDP datagram with source/destination ports.
57    Udp {
58        src_port: u16,
59        dst_port: u16,
60        payload_offset: usize,
61    },
62    /// ICMP message with type and code.
63    Icmp {
64        icmp_type: u8,
65        code: u8,
66    },
67    /// ICMPv6 message.
68    Icmpv6 {
69        icmp_type: u8,
70        code: u8,
71    },
72    /// Any other protocol (GRE, ESP, etc).
73    Other {
74        protocol_number: u8,
75    },
76}
77
78impl TransportProtocol {
79    /// Returns the source port if this is TCP or UDP.
80    pub fn src_port(&self) -> Option<u16> {
81        match self {
82            TransportProtocol::Tcp { src_port, .. } => Some(*src_port),
83            TransportProtocol::Udp { src_port, .. } => Some(*src_port),
84            _ => None,
85        }
86    }
87
88    /// Returns the destination port if this is TCP or UDP.
89    pub fn dst_port(&self) -> Option<u16> {
90        match self {
91            TransportProtocol::Tcp { dst_port, .. } => Some(*dst_port),
92            TransportProtocol::Udp { dst_port, .. } => Some(*dst_port),
93            _ => None,
94        }
95    }
96
97    /// Returns `true` if this is a TCP SYN (connection initiation).
98    pub fn is_syn(&self) -> bool {
99        matches!(self, TransportProtocol::Tcp { syn: true, ack: false, .. })
100    }
101
102    /// Returns `true` if this is a TCP FIN (connection close).
103    pub fn is_fin(&self) -> bool {
104        matches!(self, TransportProtocol::Tcp { fin: true, .. })
105    }
106
107    /// Returns `true` if this is a TCP RST (connection reset).
108    pub fn is_rst(&self) -> bool {
109        matches!(self, TransportProtocol::Tcp { rst: true, .. })
110    }
111
112    /// Returns the protocol number (6=TCP, 17=UDP, 1=ICMP, 58=ICMPv6).
113    pub fn protocol_number(&self) -> u8 {
114        match self {
115            TransportProtocol::Tcp { .. }   => 6,
116            TransportProtocol::Udp { .. }   => 17,
117            TransportProtocol::Icmp { .. }  => 1,
118            TransportProtocol::Icmpv6 { .. } => 58,
119            TransportProtocol::Other { protocol_number } => *protocol_number,
120        }
121    }
122}
123
124/// A fully parsed IP packet with IP and transport layer information.
125#[derive(Debug, Clone)]
126pub struct ParsedPacket {
127    /// Raw bytes of the original packet.
128    pub raw: Vec<u8>,
129    /// IP version.
130    pub ip_version: IpVersion,
131    /// Source IP address as string.
132    pub src_ip: String,
133    /// Destination IP address as string.
134    pub dst_ip: String,
135    /// TTL (IPv4) or hop limit (IPv6).
136    pub ttl: u8,
137    /// Total packet length in bytes.
138    pub total_len: usize,
139    /// Byte offset where the IP payload starts (after IP header).
140    pub ip_payload_offset: usize,
141    /// Parsed transport layer.
142    pub transport: TransportProtocol,
143}
144
145impl ParsedPacket {
146    /// Parse a raw IP packet (IPv4 or IPv6) into a [`ParsedPacket`].
147    ///
148    /// # Errors
149    /// Returns [`VCLError::InvalidPacket`] if:
150    /// - The packet is empty or too short
151    /// - The IP header is malformed
152    pub fn parse(raw: Vec<u8>) -> Result<Self, VCLError> {
153        if raw.is_empty() {
154            return Err(VCLError::InvalidPacket("Empty packet".to_string()));
155        }
156        match raw[0] >> 4 {
157            4 => Self::parse_ipv4(raw),
158            6 => Self::parse_ipv6(raw),
159            v => Err(VCLError::InvalidPacket(format!("Unknown IP version: {}", v))),
160        }
161    }
162
163    fn parse_ipv4(raw: Vec<u8>) -> Result<Self, VCLError> {
164        let header = Ipv4HeaderSlice::from_slice(&raw)
165            .map_err(|e| VCLError::InvalidPacket(format!("IPv4 header error: {}", e)))?;
166
167        let src_ip = format!(
168            "{}.{}.{}.{}",
169            header.source()[0], header.source()[1],
170            header.source()[2], header.source()[3]
171        );
172        let dst_ip = format!(
173            "{}.{}.{}.{}",
174            header.destination()[0], header.destination()[1],
175            header.destination()[2], header.destination()[3]
176        );
177        let ttl = header.ttl();
178        let protocol = header.protocol().0;
179        let ip_payload_offset = (header.ihl() as usize) * 4;
180        let total_len = raw.len();
181
182        let transport = parse_transport(protocol, &raw, ip_payload_offset)?;
183
184        debug!(
185            src = %src_ip, dst = %dst_ip,
186            protocol, ttl, total_len,
187            "IPv4 packet parsed"
188        );
189
190        Ok(ParsedPacket {
191            raw,
192            ip_version: IpVersion::V4,
193            src_ip,
194            dst_ip,
195            ttl,
196            total_len,
197            ip_payload_offset,
198            transport,
199        })
200    }
201
202    fn parse_ipv6(raw: Vec<u8>) -> Result<Self, VCLError> {
203        let header = Ipv6HeaderSlice::from_slice(&raw)
204            .map_err(|e| VCLError::InvalidPacket(format!("IPv6 header error: {}", e)))?;
205
206        let src_ip = format!("{}", header.source_addr());
207        let dst_ip = format!("{}", header.destination_addr());
208        let ttl = header.hop_limit();
209        let protocol = header.next_header().0;
210        let ip_payload_offset = 40; // IPv6 fixed header size
211        let total_len = raw.len();
212
213        let transport = parse_transport(protocol, &raw, ip_payload_offset)?;
214
215        debug!(
216            src = %src_ip, dst = %dst_ip,
217            protocol, ttl, total_len,
218            "IPv6 packet parsed"
219        );
220
221        Ok(ParsedPacket {
222            raw,
223            ip_version: IpVersion::V6,
224            src_ip,
225            dst_ip,
226            ttl,
227            total_len,
228            ip_payload_offset,
229            transport,
230        })
231    }
232
233    /// Returns `true` if this packet is IPv4.
234    pub fn is_ipv4(&self) -> bool {
235        self.ip_version == IpVersion::V4
236    }
237
238    /// Returns `true` if this packet is IPv6.
239    pub fn is_ipv6(&self) -> bool {
240        self.ip_version == IpVersion::V6
241    }
242
243    /// Returns `true` if the destination IP matches `ip`.
244    pub fn is_destined_for(&self, ip: &str) -> bool {
245        self.dst_ip == ip
246    }
247
248    /// Returns `true` if the source IP matches `ip`.
249    pub fn is_from(&self, ip: &str) -> bool {
250        self.src_ip == ip
251    }
252
253    /// Returns the payload slice — bytes after the IP header.
254    pub fn ip_payload(&self) -> &[u8] {
255        if self.ip_payload_offset < self.raw.len() {
256            &self.raw[self.ip_payload_offset..]
257        } else {
258            &[]
259        }
260    }
261
262    /// Returns `true` if this is a DNS query (UDP dst port 53).
263    pub fn is_dns(&self) -> bool {
264        matches!(&self.transport, TransportProtocol::Udp { dst_port: 53, .. })
265    }
266
267    /// Returns `true` if this is an ICMP echo request (ping).
268    pub fn is_ping(&self) -> bool {
269        matches!(&self.transport,
270            TransportProtocol::Icmp { icmp_type: 8, .. } |
271            TransportProtocol::Icmpv6 { icmp_type: 128, .. }
272        )
273    }
274
275    /// Returns a human-readable summary of this packet.
276    pub fn summary(&self) -> String {
277        match &self.transport {
278            TransportProtocol::Tcp { src_port, dst_port, syn, fin, rst, .. } => {
279                let flags = if *syn { " SYN" } else if *fin { " FIN" } else if *rst { " RST" } else { "" };
280                format!("TCP {}:{} → {}:{}{} ({} bytes)",
281                    self.src_ip, src_port, self.dst_ip, dst_port, flags, self.total_len)
282            }
283            TransportProtocol::Udp { src_port, dst_port, .. } => {
284                format!("UDP {}:{} → {}:{} ({} bytes)",
285                    self.src_ip, src_port, self.dst_ip, dst_port, self.total_len)
286            }
287            TransportProtocol::Icmp { icmp_type, code } => {
288                format!("ICMP {} → {} type={} code={} ({} bytes)",
289                    self.src_ip, self.dst_ip, icmp_type, code, self.total_len)
290            }
291            TransportProtocol::Icmpv6 { icmp_type, code } => {
292                format!("ICMPv6 {} → {} type={} code={} ({} bytes)",
293                    self.src_ip, self.dst_ip, icmp_type, code, self.total_len)
294            }
295            TransportProtocol::Other { protocol_number } => {
296                format!("Proto#{} {} → {} ({} bytes)",
297                    protocol_number, self.src_ip, self.dst_ip, self.total_len)
298            }
299        }
300    }
301}
302
303fn parse_transport(
304    protocol: u8,
305    raw: &[u8],
306    offset: usize,
307) -> Result<TransportProtocol, VCLError> {
308    match protocol {
309        6 => parse_tcp(raw, offset),
310        17 => parse_udp(raw, offset),
311        1 => parse_icmp(raw, offset),
312        58 => parse_icmpv6(raw, offset),
313        p => Ok(TransportProtocol::Other { protocol_number: p }),
314    }
315}
316
317fn parse_tcp(raw: &[u8], offset: usize) -> Result<TransportProtocol, VCLError> {
318    if offset >= raw.len() {
319        return Ok(TransportProtocol::Other { protocol_number: 6 });
320    }
321    let tcp = TcpHeaderSlice::from_slice(&raw[offset..])
322        .map_err(|e| VCLError::InvalidPacket(format!("TCP header error: {}", e)))?;
323    let payload_offset = offset + (tcp.data_offset() as usize) * 4;
324    Ok(TransportProtocol::Tcp {
325        src_port: tcp.source_port(),
326        dst_port: tcp.destination_port(),
327        syn: tcp.syn(),
328        ack: tcp.ack(),
329        fin: tcp.fin(),
330        rst: tcp.rst(),
331        payload_offset,
332    })
333}
334
335fn parse_udp(raw: &[u8], offset: usize) -> Result<TransportProtocol, VCLError> {
336    if offset >= raw.len() {
337        return Ok(TransportProtocol::Other { protocol_number: 17 });
338    }
339    let udp = UdpHeaderSlice::from_slice(&raw[offset..])
340        .map_err(|e| VCLError::InvalidPacket(format!("UDP header error: {}", e)))?;
341    let payload_offset = offset + 8; // UDP header is always 8 bytes
342    Ok(TransportProtocol::Udp {
343        src_port: udp.source_port(),
344        dst_port: udp.destination_port(),
345        payload_offset,
346    })
347}
348
349fn parse_icmp(raw: &[u8], offset: usize) -> Result<TransportProtocol, VCLError> {
350    if offset + 2 > raw.len() {
351        return Ok(TransportProtocol::Other { protocol_number: 1 });
352    }
353    Ok(TransportProtocol::Icmp {
354        icmp_type: raw[offset],
355        code: raw[offset + 1],
356    })
357}
358
359fn parse_icmpv6(raw: &[u8], offset: usize) -> Result<TransportProtocol, VCLError> {
360    if offset + 2 > raw.len() {
361        return Ok(TransportProtocol::Other { protocol_number: 58 });
362    }
363    Ok(TransportProtocol::Icmpv6 {
364        icmp_type: raw[offset],
365        code: raw[offset + 1],
366    })
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    fn ipv4_tcp_packet() -> Vec<u8> {
374        vec![
375            // IPv4 header (20 bytes)
376            0x45, 0x00, 0x00, 0x28, // version=4 ihl=5 total=40
377            0x00, 0x01, 0x00, 0x00, // id=1 flags=0
378            0x40, 0x06, 0x00, 0x00, // ttl=64 proto=TCP(6) checksum=0
379            192, 168, 1, 1,          // src
380            10,  0,   0, 1,          // dst
381            // TCP header (20 bytes)
382            0x00, 0x50,              // src_port=80
383            0x1F, 0x90,              // dst_port=8080
384            0x00, 0x00, 0x00, 0x01, // seq
385            0x00, 0x00, 0x00, 0x01, // ack
386            0x50, 0x02,              // data_offset=5 flags=SYN
387            0x20, 0x00,              // window
388            0x00, 0x00, 0x00, 0x00, // checksum+urgent
389        ]
390    }
391
392    fn ipv4_udp_packet() -> Vec<u8> {
393        vec![
394            // IPv4 header (20 bytes)
395            0x45, 0x00, 0x00, 0x1C,
396            0x00, 0x01, 0x00, 0x00,
397            0x40, 0x11, 0x00, 0x00, // proto=UDP(17)
398            10, 0, 0, 1,
399            8, 8, 8, 8,             // dst=8.8.8.8
400            // UDP header (8 bytes)
401            0x04, 0x00,             // src_port=1024
402            0x00, 0x35,             // dst_port=53 (DNS)
403            0x00, 0x08,             // length=8
404            0x00, 0x00,             // checksum
405        ]
406    }
407
408    fn ipv4_icmp_packet() -> Vec<u8> {
409        vec![
410            // IPv4 header (20 bytes)
411            0x45, 0x00, 0x00, 0x1C,
412            0x00, 0x01, 0x00, 0x00,
413            0x40, 0x01, 0x00, 0x00, // proto=ICMP(1)
414            10, 0, 0, 1,
415            10, 0, 0, 2,
416            // ICMP (4 bytes)
417            0x08, 0x00,             // type=8 (echo request), code=0
418            0x00, 0x00,             // checksum
419        ]
420    }
421
422    #[test]
423    fn test_parse_tcp() {
424        let pkt = ParsedPacket::parse(ipv4_tcp_packet()).unwrap();
425        assert!(pkt.is_ipv4());
426        assert_eq!(pkt.src_ip, "192.168.1.1");
427        assert_eq!(pkt.dst_ip, "10.0.0.1");
428        assert_eq!(pkt.ttl, 64);
429        assert!(matches!(pkt.transport, TransportProtocol::Tcp {
430            src_port: 80, dst_port: 8080, syn: true, ..
431        }));
432        assert!(pkt.transport.is_syn());
433        assert!(!pkt.transport.is_fin());
434        assert_eq!(pkt.transport.src_port(), Some(80));
435        assert_eq!(pkt.transport.dst_port(), Some(8080));
436        assert_eq!(pkt.transport.protocol_number(), 6);
437    }
438
439    #[test]
440    fn test_parse_udp_dns() {
441        let pkt = ParsedPacket::parse(ipv4_udp_packet()).unwrap();
442        assert!(pkt.is_ipv4());
443        assert_eq!(pkt.dst_ip, "8.8.8.8");
444        assert!(matches!(pkt.transport, TransportProtocol::Udp {
445            src_port: 1024, dst_port: 53, ..
446        }));
447        assert!(pkt.is_dns());
448        assert_eq!(pkt.transport.protocol_number(), 17);
449    }
450
451    #[test]
452    fn test_parse_icmp_ping() {
453        let pkt = ParsedPacket::parse(ipv4_icmp_packet()).unwrap();
454        assert!(pkt.is_ping());
455        assert!(matches!(pkt.transport, TransportProtocol::Icmp {
456            icmp_type: 8, code: 0
457        }));
458        assert_eq!(pkt.transport.protocol_number(), 1);
459    }
460
461    #[test]
462    fn test_parse_empty() {
463        assert!(ParsedPacket::parse(vec![]).is_err());
464    }
465
466    #[test]
467    fn test_parse_unknown_version() {
468        let raw = vec![0x30u8; 20];
469        assert!(ParsedPacket::parse(raw).is_err());
470    }
471
472    #[test]
473    fn test_is_destined_for() {
474        let pkt = ParsedPacket::parse(ipv4_tcp_packet()).unwrap();
475        assert!(pkt.is_destined_for("10.0.0.1"));
476        assert!(!pkt.is_destined_for("1.2.3.4"));
477    }
478
479    #[test]
480    fn test_is_from() {
481        let pkt = ParsedPacket::parse(ipv4_tcp_packet()).unwrap();
482        assert!(pkt.is_from("192.168.1.1"));
483        assert!(!pkt.is_from("1.2.3.4"));
484    }
485
486    #[test]
487    fn test_ip_payload() {
488        let pkt = ParsedPacket::parse(ipv4_tcp_packet()).unwrap();
489        // IPv4 IHL=5 → offset=20
490        assert_eq!(pkt.ip_payload_offset, 20);
491        assert!(!pkt.ip_payload().is_empty());
492    }
493
494    #[test]
495    fn test_summary_tcp() {
496        let pkt = ParsedPacket::parse(ipv4_tcp_packet()).unwrap();
497        let s = pkt.summary();
498        assert!(s.contains("TCP"));
499        assert!(s.contains("192.168.1.1"));
500        assert!(s.contains("10.0.0.1"));
501        assert!(s.contains("80"));
502        assert!(s.contains("8080"));
503        assert!(s.contains("SYN"));
504    }
505
506    #[test]
507    fn test_summary_udp() {
508        let pkt = ParsedPacket::parse(ipv4_udp_packet()).unwrap();
509        let s = pkt.summary();
510        assert!(s.contains("UDP"));
511        assert!(s.contains("53"));
512    }
513
514    #[test]
515    fn test_tcp_flags() {
516        let pkt = ParsedPacket::parse(ipv4_tcp_packet()).unwrap();
517        assert!(pkt.transport.is_syn());
518        assert!(!pkt.transport.is_fin());
519        assert!(!pkt.transport.is_rst());
520    }
521
522    #[test]
523    fn test_other_protocol() {
524        let mut raw = vec![0u8; 24];
525        raw[0] = 0x45; // IPv4, IHL=5
526        raw[9] = 0x2F; // GRE = 47
527        raw[12..16].copy_from_slice(&[10, 0, 0, 1]);
528        raw[16..20].copy_from_slice(&[10, 0, 0, 2]);
529        let pkt = ParsedPacket::parse(raw).unwrap();
530        assert!(matches!(pkt.transport, TransportProtocol::Other { protocol_number: 47 }));
531        assert_eq!(pkt.transport.protocol_number(), 47);
532    }
533
534    #[test]
535    fn test_is_not_dns_for_tcp() {
536        let pkt = ParsedPacket::parse(ipv4_tcp_packet()).unwrap();
537        assert!(!pkt.is_dns());
538    }
539
540    #[test]
541    fn test_is_not_ping_for_udp() {
542        let pkt = ParsedPacket::parse(ipv4_udp_packet()).unwrap();
543        assert!(!pkt.is_ping());
544    }
545}