Module 05 — Packet Header Structs & Parser
What you learn
How to define network packet header structs in C and parse raw bytes into them — layer by layer: Ethernet → IPv4/IPv6 → UDP/TCP → DNS.
This is the entry point of every packet in the DP application pipeline. Before any policy decision can be made, the packet headers must be parsed to extract the source/destination IP, protocol, port, and ultimately the domain name (DNS) or SNI (TLS).
Where this fits in the real application
rte_eth_rx_burst(port, queue, mbufs, BURST_SIZE)
│
│ mbuf → raw bytes
│ pkt = rte_pktmbuf_mtod(mbuf, uint8_t *)
│
├─► eth = (eth_hdr_t *) pkt
│
├─► ip4 = (ipv4_hdr_t *)(pkt + ETH_HDR_LEN) [if IPv4]
│ ip6 = (ipv6_hdr_t *)(pkt + ETH_HDR_LEN) [if IPv6]
│
├─► udp = (udp_hdr_t *)((uint8_t *)ip4 + IPV4_IHL_BYTES(ip4))
│ tcp = (tcp_hdr_t *)((uint8_t *)ip4 + IPV4_IHL_BYTES(ip4))
│
├─► DNS port 53 → dns_hdr + qname parsing → Module 06
│ TCP port 443 → TLS ClientHello parsing → Module 07
│
└─► policy decision (Module 22)
Files
| File | Purpose |
|---|---|
packet_structs.h |
All header structs, constants, inline helpers |
packet_parser.c |
Layer-by-layer parser + 3 sample packets + demo |
Makefile |
Build rules |
Build and run
make
./packet_parser
Expected output (abbreviated):
=== Module 05: Packet Structs ===
--- Struct sizes (must match protocol spec) ---
eth_hdr_t : 14 bytes (expected 14)
ipv4_hdr_t : 20 bytes (expected 20)
...
All sizes correct.
══════════════════════════════════════════
Packet: DNS A query (UDP/IPv4) — www.example.com (75 bytes)
══════════════════════════════════════════
[Ethernet]
dst_mac : ff:ff:ff:ff:ff:ff
src_mac : 00:11:22:33:44:55
ether_type : 0x0800 (IPv4)
[IPv4]
src_ip : 198.51.100.5
dst_ip : 8.8.8.8
protocol : 17 (UDP)
[UDP]
dst_port : 53 (DNS)
[DNS]
id : 0x1234
type : QUERY
[DNS Question]
qname : www.example.com
qtype : 1 (A)
qclass : 1 (IN)
Key concepts in the code
1. __attribute__((packed)) — the most critical attribute
Without it the compiler inserts padding to align fields:
/* Without packed — compiler might add padding: */
struct bad {
uint8_t a; /* offset 0 */
/* 3 bytes padding inserted! */
uint32_t b; /* offset 4, not 1 */
}; /* sizeof = 8, not 5 */
/* With packed — no padding: */
struct good {
uint8_t a; /* offset 0 */
uint32_t b; /* offset 1 */
} __attribute__((packed)); /* sizeof = 5 */
If you forget packed on ipv4_hdr_t, ip4->src_ip points to the
wrong offset in the packet and you read garbage IP addresses.
2. Network byte order (big-endian)
x86 CPUs store multi-byte integers least-significant byte first (little-endian). Network protocols store them most-significant byte first (big-endian).
/* Port 53 in memory, as it appears in the packet: */
uint8_t bytes[] = { 0x00, 0x35 }; /* 0x0035 = 53 in big-endian */
/* On x86, reading this as uint16_t directly: */
uint16_t raw = *(uint16_t *)bytes; /* reads as 0x3500 = 13568 — WRONG */
/* Correct way: */
uint16_t port = ntohs(*(uint16_t *)bytes); /* converts to 53 — RIGHT */
Every port number, IP address, length field, and DNS field must be
converted with ntohs() or ntohl() before comparison or arithmetic.
3. Zero-copy pointer overlay
“Parsing” in a dataplane means casting, not copying:
/* DPDK real app: */
uint8_t *pkt = rte_pktmbuf_mtod(mbuf, uint8_t *);
/* Overlay structs — no data copied, just pointer arithmetic: */
eth_hdr_t *eth = (eth_hdr_t *)pkt;
ipv4_hdr_t *ip4 = (ipv4_hdr_t *)(pkt + ETH_HDR_LEN);
udp_hdr_t *udp = (udp_hdr_t *)((uint8_t *)ip4 + IPV4_IHL_BYTES(ip4));
dns_hdr_t *dns = (dns_hdr_t *)((uint8_t *)udp + UDP_HDR_LEN);
This is why DPDK is fast — no memcpy per packet. The packet stays in
the NIC-allocated hugepage buffer from receive to transmit.
4. Variable-length IPv4 header
IPv4 has an options field that makes the header variable-length (20–60 bytes).
Always use IPV4_IHL_BYTES(ip4) — never hardcode 20.
#define IPV4_IHL_BYTES(hdr) (((hdr)->version_ihl & 0x0F) << 2)
/* IHL field is count of 32-bit words; multiply by 4 to get bytes */
5. read_u16_be — safe unaligned reads
static inline uint16_t read_u16_be(const uint8_t *p) {
return (uint16_t)((p[0] << 8) | p[1]);
}
Used in pkt_proc.h for TLS SNI length extraction — identical pattern.
6. DNS qname wire format
DNS does NOT use null-terminated strings. It uses length-prefixed labels:
"www.example.com" in DNS wire format:
\x03 w w w \x07 e x a m p l e \x03 c o m \x00
^3 ^7 ^3 ^end
rte_pktmbuf header access (what you’ll see in real code)
/* In the real DP application app (pkt_proc.h): */
struct rte_mbuf *mbuf = mbufs[i];
struct rte_ether_hdr *eth;
struct rte_ipv4_hdr *ip4;
struct rte_udp_hdr *udp;
eth = rte_pktmbuf_mtod(mbuf, struct rte_ether_hdr *);
if (ntohs(eth->ether_type) == RTE_ETHER_TYPE_IPV4) {
ip4 = (struct rte_ipv4_hdr *)((uint8_t *)eth + sizeof(*eth));
if (ip4->next_proto_id == IPPROTO_UDP) {
udp = (struct rte_udp_hdr *)((uint8_t *)ip4 +
rte_ipv4_hdr_len(ip4));
if (ntohs(udp->dst_port) == 53) {
/* DNS packet — parse qname, lookup policy */
}
}
}
Next module
Module 06 — DNS Parser: Build a complete DNS query parser — extract transaction ID, query type (A/AAAA), and the full domain name from the wire format.
Source files
| File | Download |
|---|---|
packet_structs.h |
packet_structs.h |
packet_parser.c |
packet_parser.c |
Makefile |
Makefile |