xdp_util/packet.rs
1//! # UDP Packet Header Construction
2//!
3//! ## Purpose
4//!
5//! This module provides a utility to construct the headers for a UDP/IPv4/Ethernet
6//! packet. This is essential for applications using AF_XDP that need to manually
7//! build entire packets before sending them.
8//!
9//! ## How it works
10//!
11//! It uses the `etherparse` crate's `PacketBuilder` to efficiently layer the Ethernet II,
12//! IPv4, and UDP headers. The headers are written directly into a fixed-size `[u8; 42]`
13//! array to avoid heap allocations. A custom `HdrWrite` struct, which implements
14//! `std::io::Write`, acts as a temporary writer to capture the builder's output.
15//!
16//! ## Main components
17//!
18//! - `write_udp_header_for()`: The primary function that takes source/destination
19//! addresses and ports and returns a 42-byte array containing the packet header.
20//! - `HdrWrite`: A helper struct implementing `io::Write` to enable writing into a
21//! fixed-size buffer without extra allocations.
22
23use etherparse::PacketBuilder;
24use std::io;
25use std::net::Ipv4Addr;
26
27/// Constructs Ethernet, IPv4, and UDP headers for a packet.
28///
29/// This function uses `etherparse::PacketBuilder` to create the necessary headers
30/// and writes them into a 42-byte array.
31///
32/// # Arguments
33/// * `data` - The payload data, used to calculate the UDP payload length.
34/// * `src_addr` - Source IPv4 address.
35/// * `src_mac` - Source MAC address.
36/// * `src_port` - Source UDP port.
37/// * `dst_addr` - Destination IPv4 address.
38/// * `dst_mac` - Destination MAC address.
39/// * `dst_port` - Destination UDP port.
40///
41/// # Returns
42/// A `Result` containing a 42-byte array with the complete L2/L3/L4 headers,
43/// or an `io::Error` on failure.
44pub fn write_udp_header_for(
45 data: &[u8],
46 src_addr: Ipv4Addr,
47 src_mac: [u8; 6],
48 src_port: u16,
49 dst_addr: Ipv4Addr,
50 dst_mac: [u8; 6],
51 dst_port: u16,
52) -> io::Result<[u8; 42]> {
53 let mut hdr = [0u8; 42]; // 14 bytes for Ethernet header + 20 bytes for IPv4 header + 8 bytes for UDP header
54
55 let builder = PacketBuilder::
56 // Layer 2: Ethernet II header
57 ethernet2(src_mac, dst_mac)
58 // Layer 3: IPv4 header
59 .ipv4(src_addr.octets(), dst_addr.octets(), 64) // 64 is a common TTL
60 // Layer 4: UDP header
61 .udp(src_port, dst_port);
62
63 match builder.write(&mut HdrWrite(&mut hdr, 0), data) {
64 Ok(_) => Ok(hdr),
65 Err(e) => Err(io::Error::other(format!(
66 "Error writing packet header: {e}",
67 ))),
68 }
69}
70
71/// A helper struct that implements `std::io::Write` for a fixed-size byte array.
72///
73/// This is used to capture the output of `etherparse::PacketBuilder` without
74/// requiring heap allocations.
75pub struct HdrWrite<'a>(
76 /// The buffer to write into.
77 pub &'a mut [u8; 42],
78 /// The current write position within the buffer.
79 pub usize,
80);
81impl io::Write for HdrWrite<'_> {
82 /// Writes a buffer into the inner array, respecting the fixed size.
83 ///
84 /// It copies bytes from `buf` into the internal array. If `buf` is larger
85 /// than the remaining space, the data is truncated.
86 fn write(&mut self, buf: &[u8]) -> Result<usize, io::Error> {
87 if self.1 < 42 {
88 let len = buf.len().min(self.0.len() - self.1);
89 self.0[self.1..self.1 + len].copy_from_slice(&buf[..len]);
90 }
91 self.1 += buf.len();
92 Ok(buf.len())
93 }
94
95 /// A no-op flush implementation, as the write is immediate.
96 fn flush(&mut self) -> io::Result<()> {
97 Ok(())
98 }
99}
100
101//
102// ================================================================================================
103// UNITTESTS
104// ================================================================================================
105//
106#[cfg(test)]
107mod tests {
108 use super::HdrWrite;
109 use std::io::Write;
110 use std::net::Ipv4Addr;
111
112 #[test]
113 fn test_write_udp_header() {
114 let src_addr = Ipv4Addr::new(192, 168, 1, 1);
115 let dst_addr = Ipv4Addr::new(192, 168, 1, 2);
116 let src_mac = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55];
117 let dst_mac = [0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb];
118 let src_port = 12345;
119 let dst_port = 54321;
120 let data = b"Hello, XDP!";
121 let hdr = super::write_udp_header_for(
122 data, src_addr, src_mac, src_port, dst_addr, dst_mac, dst_port,
123 )
124 .unwrap();
125 assert_eq!(hdr.len(), 42);
126 let mut buf = [0u8; 42 + 11];
127 buf[..42].clone_from_slice(&hdr);
128 buf[42..].copy_from_slice(data);
129 match etherparse::SlicedPacket::from_ethernet(&buf) {
130 Ok(packet) => match packet.transport {
131 Some(etherparse::TransportSlice::Udp(_udp)) => {}
132 _ => panic!("Not udp packet"),
133 },
134 Err(e) => panic!("Failed to parse packet: {}", e),
135 };
136 }
137
138 #[test]
139 fn test_hdrwrite() {
140 let mut hdr = [0u8; 42];
141 let data = b"Test data";
142 let written = {
143 let mut writer = HdrWrite(&mut hdr, 0);
144 writer.write(data).unwrap()
145 };
146 assert_eq!(written, data.len());
147 assert_eq!(&hdr[..written], data);
148 let data = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaTest data";
149 let written = {
150 let mut writer = HdrWrite(&mut hdr, 0);
151 writer.write(data).unwrap()
152 };
153 assert_eq!(written, data.len());
154 assert_eq!(&hdr[..], &data[..42]);
155 }
156}