Skip to main content

arcbox_packet/
packet.rs

1//! Zero-copy packet representation.
2//!
3//! This module provides packet structures that reference shared memory directly
4//! without copying data, enabling high-performance packet processing.
5
6use std::net::{Ipv4Addr, Ipv6Addr};
7use std::sync::atomic::{AtomicU32, Ordering};
8
9/// Network protocol identifier.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
11#[repr(u8)]
12pub enum Protocol {
13    /// Unknown or unsupported protocol.
14    #[default]
15    Unknown = 0,
16    /// Internet Control Message Protocol.
17    Icmp = 1,
18    /// Transmission Control Protocol.
19    Tcp = 6,
20    /// User Datagram Protocol.
21    Udp = 17,
22    /// ICMPv6.
23    Icmpv6 = 58,
24}
25
26impl From<u8> for Protocol {
27    fn from(value: u8) -> Self {
28        match value {
29            1 => Self::Icmp,
30            6 => Self::Tcp,
31            17 => Self::Udp,
32            58 => Self::Icmpv6,
33            _ => Self::Unknown,
34        }
35    }
36}
37
38/// IP version.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40#[repr(u8)]
41pub enum IpVersion {
42    /// Unknown version.
43    #[default]
44    Unknown = 0,
45    /// IPv4.
46    V4 = 4,
47    /// IPv6.
48    V6 = 6,
49}
50
51/// Pre-parsed packet metadata for fast path processing.
52///
53/// Storing parsed header offsets and protocol information avoids
54/// repeated parsing in the hot path.
55#[repr(C)]
56#[derive(Debug, Clone, Copy, Default)]
57pub struct PacketMetadata {
58    /// Offset to L2 (Ethernet) header from packet start.
59    pub l2_offset: u16,
60    /// Offset to L3 (IP) header from packet start.
61    pub l3_offset: u16,
62    /// Offset to L4 (TCP/UDP) header from packet start.
63    pub l4_offset: u16,
64    /// L4 protocol type.
65    pub protocol: Protocol,
66    /// IP version.
67    pub ip_version: IpVersion,
68    /// Cached flow hash for connection tracking lookups.
69    pub flow_hash: u64,
70    /// Source port (for TCP/UDP).
71    pub src_port: u16,
72    /// Destination port (for TCP/UDP).
73    pub dst_port: u16,
74    /// Packet flags (e.g., TCP flags).
75    pub flags: u8,
76    /// Padding for alignment.
77    _padding: [u8; 3],
78}
79
80impl PacketMetadata {
81    /// Creates empty metadata.
82    #[inline]
83    #[must_use]
84    pub const fn new() -> Self {
85        Self {
86            l2_offset: 0,
87            l3_offset: 0,
88            l4_offset: 0,
89            protocol: Protocol::Unknown,
90            ip_version: IpVersion::Unknown,
91            flow_hash: 0,
92            src_port: 0,
93            dst_port: 0,
94            flags: 0,
95            _padding: [0; 3],
96        }
97    }
98
99    /// Returns true if this is a TCP packet.
100    #[inline]
101    #[must_use]
102    pub const fn is_tcp(&self) -> bool {
103        matches!(self.protocol, Protocol::Tcp)
104    }
105
106    /// Returns true if this is a UDP packet.
107    #[inline]
108    #[must_use]
109    pub const fn is_udp(&self) -> bool {
110        matches!(self.protocol, Protocol::Udp)
111    }
112
113    /// Returns true if this is an ICMP packet.
114    #[inline]
115    #[must_use]
116    pub const fn is_icmp(&self) -> bool {
117        matches!(self.protocol, Protocol::Icmp | Protocol::Icmpv6)
118    }
119}
120
121/// Zero-copy packet referencing shared memory directly.
122///
123/// This structure holds a pointer to packet data in guest memory
124/// without copying the actual data. The reference count enables
125/// safe deferred release after processing.
126///
127/// # Safety
128///
129/// The data pointer must remain valid for the lifetime of this packet.
130/// The caller is responsible for ensuring the underlying memory is not
131/// deallocated or modified while the packet is in use.
132///
133/// # Cache Line Alignment
134///
135/// The structure is aligned to 64 bytes to prevent false sharing
136/// when packets are processed in parallel.
137#[repr(C, align(64))]
138pub struct ZeroCopyPacket {
139    /// Pointer to packet data in shared memory.
140    data: *const u8,
141    /// Packet data length in bytes.
142    len: u32,
143    /// Pre-parsed packet metadata.
144    metadata: PacketMetadata,
145    /// Reference count for deferred release.
146    refcount: AtomicU32,
147    /// VirtIO descriptor index for completion.
148    desc_idx: u16,
149    /// Flags.
150    flags: u16,
151    /// Timestamp when packet was received (microseconds).
152    timestamp: u64,
153}
154
155// Safety: The packet data pointer is read-only and can be shared.
156unsafe impl Send for ZeroCopyPacket {}
157unsafe impl Sync for ZeroCopyPacket {}
158
159impl Default for ZeroCopyPacket {
160    fn default() -> Self {
161        Self::empty()
162    }
163}
164
165impl ZeroCopyPacket {
166    /// Packet flag: needs checksum calculation.
167    pub const FLAG_NEEDS_CSUM: u16 = 1 << 0;
168    /// Packet flag: is a GSO packet.
169    pub const FLAG_GSO: u16 = 1 << 1;
170    /// Packet flag: from guest (TX direction).
171    pub const FLAG_FROM_GUEST: u16 = 1 << 2;
172    /// Packet flag: to guest (RX direction).
173    pub const FLAG_TO_GUEST: u16 = 1 << 3;
174
175    /// Creates an empty packet.
176    #[inline]
177    #[must_use]
178    pub const fn empty() -> Self {
179        Self {
180            data: std::ptr::null(),
181            len: 0,
182            metadata: PacketMetadata::new(),
183            refcount: AtomicU32::new(0),
184            desc_idx: 0,
185            flags: 0,
186            timestamp: 0,
187        }
188    }
189
190    /// Creates a new zero-copy packet from raw parts.
191    ///
192    /// # Safety
193    ///
194    /// The caller must ensure that:
195    /// - `data` points to valid memory for at least `len` bytes.
196    /// - The memory remains valid for the lifetime of the packet.
197    /// - The memory is not modified while the packet is in use.
198    #[inline]
199    #[must_use]
200    pub const unsafe fn from_raw_parts(data: *const u8, len: u32, desc_idx: u16) -> Self {
201        Self {
202            data,
203            len,
204            metadata: PacketMetadata::new(),
205            refcount: AtomicU32::new(1),
206            desc_idx,
207            flags: 0,
208            timestamp: 0,
209        }
210    }
211
212    /// Creates a packet from a slice (for testing/non-zero-copy paths).
213    ///
214    /// # Safety
215    ///
216    /// The slice must remain valid for the lifetime of the packet.
217    #[inline]
218    #[must_use]
219    pub const unsafe fn from_slice(data: &[u8], desc_idx: u16) -> Self {
220        Self {
221            data: data.as_ptr(),
222            len: data.len() as u32,
223            metadata: PacketMetadata::new(),
224            refcount: AtomicU32::new(1),
225            desc_idx,
226            flags: 0,
227            timestamp: 0,
228        }
229    }
230
231    /// Returns true if this packet is empty.
232    #[inline]
233    #[must_use]
234    pub const fn is_empty(&self) -> bool {
235        self.len == 0 || self.data.is_null()
236    }
237
238    /// Returns the packet data length.
239    #[inline]
240    #[must_use]
241    pub const fn len(&self) -> usize {
242        self.len as usize
243    }
244
245    /// Returns the packet data as a slice.
246    ///
247    /// # Safety
248    ///
249    /// The caller must ensure the underlying memory is still valid.
250    #[inline]
251    #[must_use]
252    pub unsafe fn as_slice(&self) -> &[u8] {
253        if self.data.is_null() {
254            &[]
255        } else {
256            // Safety: caller guarantees memory validity per function contract.
257            unsafe { std::slice::from_raw_parts(self.data, self.len as usize) }
258        }
259    }
260
261    /// Returns the raw data pointer.
262    #[inline]
263    #[must_use]
264    pub const fn data_ptr(&self) -> *const u8 {
265        self.data
266    }
267
268    /// Returns the descriptor index.
269    #[inline]
270    #[must_use]
271    pub const fn desc_idx(&self) -> u16 {
272        self.desc_idx
273    }
274
275    /// Returns a reference to the packet metadata.
276    #[inline]
277    #[must_use]
278    pub const fn metadata(&self) -> &PacketMetadata {
279        &self.metadata
280    }
281
282    /// Returns a mutable reference to the packet metadata.
283    #[inline]
284    #[must_use]
285    pub fn metadata_mut(&mut self) -> &mut PacketMetadata {
286        &mut self.metadata
287    }
288
289    /// Sets the packet metadata.
290    #[inline]
291    pub fn set_metadata(&mut self, metadata: PacketMetadata) {
292        self.metadata = metadata;
293    }
294
295    /// Returns the packet flags.
296    #[inline]
297    #[must_use]
298    pub const fn flags(&self) -> u16 {
299        self.flags
300    }
301
302    /// Sets packet flags.
303    #[inline]
304    pub fn set_flags(&mut self, flags: u16) {
305        self.flags = flags;
306    }
307
308    /// Adds a flag.
309    #[inline]
310    pub fn add_flag(&mut self, flag: u16) {
311        self.flags |= flag;
312    }
313
314    /// Checks if a flag is set.
315    #[inline]
316    #[must_use]
317    pub const fn has_flag(&self, flag: u16) -> bool {
318        self.flags & flag != 0
319    }
320
321    /// Returns the timestamp.
322    #[inline]
323    #[must_use]
324    pub const fn timestamp(&self) -> u64 {
325        self.timestamp
326    }
327
328    /// Sets the timestamp.
329    #[inline]
330    pub fn set_timestamp(&mut self, timestamp: u64) {
331        self.timestamp = timestamp;
332    }
333
334    /// Increments the reference count.
335    #[inline]
336    pub fn add_ref(&self) {
337        self.refcount.fetch_add(1, Ordering::AcqRel);
338    }
339
340    /// Decrements the reference count and returns true if it reached zero.
341    #[inline]
342    pub fn release(&self) -> bool {
343        self.refcount.fetch_sub(1, Ordering::AcqRel) == 1
344    }
345
346    /// Returns the current reference count.
347    #[inline]
348    #[must_use]
349    pub fn refcount(&self) -> u32 {
350        self.refcount.load(Ordering::Acquire)
351    }
352
353    /// Parses packet headers and populates metadata.
354    ///
355    /// # Safety
356    ///
357    /// The packet data must be valid and accessible.
358    pub unsafe fn parse_headers(&mut self) {
359        if self.len < 14 {
360            return; // Too short for Ethernet header
361        }
362
363        // Use raw pointer to avoid borrow conflict with metadata mutation.
364        // Safety: caller guarantees data validity per function contract.
365        let data = unsafe { std::slice::from_raw_parts(self.data, self.len as usize) };
366
367        // Ethernet header is 14 bytes
368        self.metadata.l2_offset = 0;
369        self.metadata.l3_offset = 14;
370
371        // Check EtherType
372        let ethertype = u16::from_be_bytes([data[12], data[13]]);
373
374        match ethertype {
375            0x0800 => {
376                // IPv4
377                self.metadata.ip_version = IpVersion::V4;
378                self.parse_ipv4(data, 14);
379            }
380            0x86DD => {
381                // IPv6
382                self.metadata.ip_version = IpVersion::V6;
383                self.parse_ipv6(data, 14);
384            }
385            0x8100 => {
386                // VLAN tagged - skip 4 bytes
387                self.metadata.l3_offset = 18;
388                if self.len >= 18 {
389                    let inner_ethertype = u16::from_be_bytes([data[16], data[17]]);
390                    match inner_ethertype {
391                        0x0800 => {
392                            self.metadata.ip_version = IpVersion::V4;
393                            self.parse_ipv4(data, 18);
394                        }
395                        0x86DD => {
396                            self.metadata.ip_version = IpVersion::V6;
397                            self.parse_ipv6(data, 18);
398                        }
399                        _ => {}
400                    }
401                }
402            }
403            _ => {}
404        }
405
406        // Calculate flow hash
407        self.metadata.flow_hash = self.calculate_flow_hash();
408    }
409
410    /// Parses IPv4 header.
411    fn parse_ipv4(&mut self, data: &[u8], offset: usize) {
412        if data.len() < offset + 20 {
413            return; // Too short for IPv4 header
414        }
415
416        let ihl = (data[offset] & 0x0F) as usize * 4;
417        self.metadata.l4_offset = (offset + ihl) as u16;
418        self.metadata.protocol = Protocol::from(data[offset + 9]);
419
420        // Parse L4 ports for TCP/UDP
421        let l4_offset = self.metadata.l4_offset as usize;
422        if data.len() >= l4_offset + 4 {
423            match self.metadata.protocol {
424                Protocol::Tcp | Protocol::Udp => {
425                    self.metadata.src_port =
426                        u16::from_be_bytes([data[l4_offset], data[l4_offset + 1]]);
427                    self.metadata.dst_port =
428                        u16::from_be_bytes([data[l4_offset + 2], data[l4_offset + 3]]);
429
430                    // TCP flags
431                    if self.metadata.protocol == Protocol::Tcp && data.len() >= l4_offset + 14 {
432                        self.metadata.flags = data[l4_offset + 13];
433                    }
434                }
435                _ => {}
436            }
437        }
438    }
439
440    /// Parses IPv6 header.
441    fn parse_ipv6(&mut self, data: &[u8], offset: usize) {
442        if data.len() < offset + 40 {
443            return; // Too short for IPv6 header
444        }
445
446        self.metadata.l4_offset = (offset + 40) as u16;
447        self.metadata.protocol = Protocol::from(data[offset + 6]); // Next Header
448
449        // Parse L4 ports for TCP/UDP
450        let l4_offset = self.metadata.l4_offset as usize;
451        if data.len() >= l4_offset + 4 {
452            match self.metadata.protocol {
453                Protocol::Tcp | Protocol::Udp => {
454                    self.metadata.src_port =
455                        u16::from_be_bytes([data[l4_offset], data[l4_offset + 1]]);
456                    self.metadata.dst_port =
457                        u16::from_be_bytes([data[l4_offset + 2], data[l4_offset + 3]]);
458
459                    // TCP flags
460                    if self.metadata.protocol == Protocol::Tcp && data.len() >= l4_offset + 14 {
461                        self.metadata.flags = data[l4_offset + 13];
462                    }
463                }
464                _ => {}
465            }
466        }
467    }
468
469    /// Calculates a flow hash for connection tracking.
470    fn calculate_flow_hash(&self) -> u64 {
471        // Simple FNV-1a hash of the 5-tuple
472        let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
473
474        hash ^= self.metadata.protocol as u64;
475        hash = hash.wrapping_mul(0x0100_0000_01b3);
476
477        hash ^= self.metadata.src_port as u64;
478        hash = hash.wrapping_mul(0x0100_0000_01b3);
479
480        hash ^= self.metadata.dst_port as u64;
481        hash = hash.wrapping_mul(0x0100_0000_01b3);
482
483        // Include IP addresses if we have them
484        unsafe {
485            let data = self.as_slice();
486            if self.metadata.ip_version == IpVersion::V4 {
487                let l3 = self.metadata.l3_offset as usize;
488                if data.len() >= l3 + 20 {
489                    // Source IP
490                    for i in 0..4 {
491                        hash ^= data[l3 + 12 + i] as u64;
492                        hash = hash.wrapping_mul(0x0100_0000_01b3);
493                    }
494                    // Dest IP
495                    for i in 0..4 {
496                        hash ^= data[l3 + 16 + i] as u64;
497                        hash = hash.wrapping_mul(0x0100_0000_01b3);
498                    }
499                }
500            } else if self.metadata.ip_version == IpVersion::V6 {
501                let l3 = self.metadata.l3_offset as usize;
502                if data.len() >= l3 + 40 {
503                    // Source IP (16 bytes)
504                    for i in 0..16 {
505                        hash ^= data[l3 + 8 + i] as u64;
506                        hash = hash.wrapping_mul(0x0100_0000_01b3);
507                    }
508                    // Dest IP (16 bytes)
509                    for i in 0..16 {
510                        hash ^= data[l3 + 24 + i] as u64;
511                        hash = hash.wrapping_mul(0x0100_0000_01b3);
512                    }
513                }
514            }
515        }
516
517        hash
518    }
519
520    /// Returns the source IPv4 address, if this is an IPv4 packet.
521    ///
522    /// # Safety
523    ///
524    /// The packet data must be valid.
525    #[must_use]
526    pub unsafe fn src_ipv4(&self) -> Option<Ipv4Addr> {
527        if self.metadata.ip_version != IpVersion::V4 {
528            return None;
529        }
530        // Safety: caller guarantees data validity per function contract.
531        let data = unsafe { self.as_slice() };
532        let l3 = self.metadata.l3_offset as usize;
533        if data.len() >= l3 + 20 {
534            Some(Ipv4Addr::new(
535                data[l3 + 12],
536                data[l3 + 13],
537                data[l3 + 14],
538                data[l3 + 15],
539            ))
540        } else {
541            None
542        }
543    }
544
545    /// Returns the destination IPv4 address, if this is an IPv4 packet.
546    ///
547    /// # Safety
548    ///
549    /// The packet data must be valid.
550    #[must_use]
551    pub unsafe fn dst_ipv4(&self) -> Option<Ipv4Addr> {
552        if self.metadata.ip_version != IpVersion::V4 {
553            return None;
554        }
555        // Safety: caller guarantees data validity per function contract.
556        let data = unsafe { self.as_slice() };
557        let l3 = self.metadata.l3_offset as usize;
558        if data.len() >= l3 + 20 {
559            Some(Ipv4Addr::new(
560                data[l3 + 16],
561                data[l3 + 17],
562                data[l3 + 18],
563                data[l3 + 19],
564            ))
565        } else {
566            None
567        }
568    }
569
570    /// Returns the source IPv6 address, if this is an IPv6 packet.
571    ///
572    /// # Safety
573    ///
574    /// The packet data must be valid.
575    #[must_use]
576    pub unsafe fn src_ipv6(&self) -> Option<Ipv6Addr> {
577        if self.metadata.ip_version != IpVersion::V6 {
578            return None;
579        }
580        // Safety: caller guarantees data validity per function contract.
581        let data = unsafe { self.as_slice() };
582        let l3 = self.metadata.l3_offset as usize;
583        if data.len() >= l3 + 40 {
584            let mut octets = [0u8; 16];
585            octets.copy_from_slice(&data[l3 + 8..l3 + 24]);
586            Some(Ipv6Addr::from(octets))
587        } else {
588            None
589        }
590    }
591
592    /// Returns the destination IPv6 address, if this is an IPv6 packet.
593    ///
594    /// # Safety
595    ///
596    /// The packet data must be valid.
597    #[must_use]
598    pub unsafe fn dst_ipv6(&self) -> Option<Ipv6Addr> {
599        if self.metadata.ip_version != IpVersion::V6 {
600            return None;
601        }
602        // Safety: caller guarantees data validity per function contract.
603        let data = unsafe { self.as_slice() };
604        let l3 = self.metadata.l3_offset as usize;
605        if data.len() >= l3 + 40 {
606            let mut octets = [0u8; 16];
607            octets.copy_from_slice(&data[l3 + 24..l3 + 40]);
608            Some(Ipv6Addr::from(octets))
609        } else {
610            None
611        }
612    }
613}
614
615impl std::fmt::Debug for ZeroCopyPacket {
616    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617        f.debug_struct("ZeroCopyPacket")
618            .field("data", &self.data)
619            .field("len", &self.len)
620            .field("metadata", &self.metadata)
621            .field("refcount", &self.refcount.load(Ordering::Relaxed))
622            .field("desc_idx", &self.desc_idx)
623            .field("flags", &self.flags)
624            .field("timestamp", &self.timestamp)
625            .finish()
626    }
627}
628
629#[cfg(test)]
630mod tests {
631    use super::*;
632
633    #[test]
634    fn test_packet_size() {
635        // Ensure packet fits in a cache line
636        assert!(std::mem::size_of::<ZeroCopyPacket>() <= 128);
637        // Ensure alignment
638        assert_eq!(std::mem::align_of::<ZeroCopyPacket>(), 64);
639    }
640
641    #[test]
642    fn test_empty_packet() {
643        let pkt = ZeroCopyPacket::empty();
644        assert!(pkt.is_empty());
645        assert_eq!(pkt.len(), 0);
646        assert_eq!(pkt.refcount(), 0);
647    }
648
649    #[test]
650    fn test_protocol_from() {
651        assert_eq!(Protocol::from(1), Protocol::Icmp);
652        assert_eq!(Protocol::from(6), Protocol::Tcp);
653        assert_eq!(Protocol::from(17), Protocol::Udp);
654        assert_eq!(Protocol::from(58), Protocol::Icmpv6);
655        assert_eq!(Protocol::from(255), Protocol::Unknown);
656    }
657
658    #[test]
659    fn test_metadata() {
660        let mut meta = PacketMetadata::new();
661        meta.protocol = Protocol::Tcp;
662        meta.src_port = 12345;
663        meta.dst_port = 80;
664
665        assert!(meta.is_tcp());
666        assert!(!meta.is_udp());
667        assert!(!meta.is_icmp());
668    }
669
670    #[test]
671    fn test_packet_from_slice() {
672        let data = [0u8; 64];
673        let pkt = unsafe { ZeroCopyPacket::from_slice(&data, 42) };
674
675        assert!(!pkt.is_empty());
676        assert_eq!(pkt.len(), 64);
677        assert_eq!(pkt.desc_idx(), 42);
678        assert_eq!(pkt.refcount(), 1);
679    }
680
681    #[test]
682    fn test_refcount() {
683        let data = [0u8; 64];
684        let pkt = unsafe { ZeroCopyPacket::from_slice(&data, 0) };
685
686        assert_eq!(pkt.refcount(), 1);
687
688        pkt.add_ref();
689        assert_eq!(pkt.refcount(), 2);
690
691        assert!(!pkt.release());
692        assert_eq!(pkt.refcount(), 1);
693
694        assert!(pkt.release());
695        assert_eq!(pkt.refcount(), 0);
696    }
697
698    #[test]
699    fn test_flags() {
700        let mut pkt = ZeroCopyPacket::empty();
701
702        assert_eq!(pkt.flags(), 0);
703        assert!(!pkt.has_flag(ZeroCopyPacket::FLAG_NEEDS_CSUM));
704
705        pkt.add_flag(ZeroCopyPacket::FLAG_NEEDS_CSUM);
706        assert!(pkt.has_flag(ZeroCopyPacket::FLAG_NEEDS_CSUM));
707        assert!(!pkt.has_flag(ZeroCopyPacket::FLAG_GSO));
708
709        pkt.add_flag(ZeroCopyPacket::FLAG_GSO);
710        assert!(pkt.has_flag(ZeroCopyPacket::FLAG_NEEDS_CSUM));
711        assert!(pkt.has_flag(ZeroCopyPacket::FLAG_GSO));
712    }
713}