| name | ebpf-packet-parser |
| description | Generate verifier-safe packet parsing logic for eBPF programs including bounds checking, header extraction (Ethernet, IPv4/IPv6, TCP/UDP, ICMP), protocol identification, and proper pointer arithmetic. Use when implementing packet inspection or manipulation in CNFs. |
eBPF Packet Parser Skill
This skill generates production-ready, verifier-safe packet parsing code for eBPF programs used in CNFs.
What This Skill Does
Generates eBPF C code for:
- Safe packet data pointer management
- Bounds checking before header access (verifier requirement)
- Ethernet frame parsing
- IPv4 and IPv6 header extraction
- TCP, UDP, ICMP protocol parsing
- IPv4-Mapped IPv6 address handling (RFC4291)
- Port and flag extraction
- Packet type filtering (broadcast/multicast)
When to Use
- Implementing packet inspection in CNFs
- Adding protocol-specific processing (TCP, UDP, ICMP)
- Extracting flow information (5-tuple)
- Building packet filters or classifiers
- Implementing L3/L4 processing logic
- Need verifier-compliant parsing patterns
Supported Protocols
Layer 2
- Ethernet (802.3)
Layer 3
- IPv4
- IPv6
- ARP (basic support)
Layer 4
- TCP (with flag extraction)
- UDP
- ICMP/ICMPv6
Information to Gather
Ask the user:
- Context Type: What context? (
xdp_md,__sk_buff, etc.) - Protocols Needed: Which protocols to parse? (IPv4, IPv6, TCP, UDP, ICMP)
- Data Extraction: What data to extract? (IPs, ports, flags, payload)
- Filtering: Any packet filtering? (skip broadcast, specific protocols)
- Output Format: Store in struct, write to map, send to ringbuf?
Core Parsing Patterns
1. Pointer Setup and Bounds Checking
For XDP (xdp_md):
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
// Bounds check before accessing
if (data + sizeof(struct ethhdr) > data_end)
return XDP_DROP;
For TC/tcx (__sk_buff):
// Ensure data is linear (pulled into contiguous memory)
if (bpf_skb_pull_data(skb, 0) < 0)
return TC_ACT_OK;
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
// Skip broadcast/multicast if needed
if (skb->pkt_type == PACKET_BROADCAST || skb->pkt_type == PACKET_MULTICAST)
return TC_ACT_OK;
if (data + sizeof(struct ethhdr) > data_end)
return TC_ACT_OK;
2. Ethernet Header Parsing
#include <linux/if_ether.h>
struct ethhdr *eth = data;
// Bounds check already done above
// Access EtherType to determine L3 protocol
__u16 eth_proto = bpf_ntohs(eth->h_proto);
switch (eth_proto) {
case ETH_P_IP:
// IPv4 processing
break;
case ETH_P_IPV6:
// IPv6 processing
break;
case ETH_P_ARP:
// ARP processing
break;
default:
return XDP_PASS; // Unknown protocol
}
3. IPv4 Header Parsing
#include <linux/ip.h>
struct iphdr *ip;
__u32 ip_offset = sizeof(struct ethhdr);
// Bounds check
if (data + ip_offset + sizeof(struct iphdr) > data_end)
return XDP_DROP;
ip = data + ip_offset;
// Extract IPv4 fields
__u32 src_ip = ip->saddr;
__u32 dst_ip = ip->daddr;
__u8 protocol = ip->protocol;
__u8 ttl = ip->ttl;
__u16 tot_len = bpf_ntohs(ip->tot_len);
// Calculate IP header length (IHL field is in 32-bit words)
__u8 ihl = ip->ihl;
if (ihl < 5)
return XDP_DROP; // Invalid header length
__u32 l4_offset = ip_offset + (ihl * 4);
// Process L4 protocols
switch (protocol) {
case IPPROTO_TCP:
// TCP processing
break;
case IPPROTO_UDP:
// UDP processing
break;
case IPPROTO_ICMP:
// ICMP processing
break;
default:
return XDP_PASS;
}
4. IPv6 Header Parsing
#include <linux/ipv6.h>
#include <linux/in6.h>
struct ipv6hdr *ipv6;
__u32 ip_offset = sizeof(struct ethhdr);
// Bounds check
if (data + ip_offset + sizeof(struct ipv6hdr) > data_end)
return XDP_DROP;
ipv6 = data + ip_offset;
// Extract IPv6 fields
struct in6_addr src_ip = ipv6->saddr;
struct in6_addr dst_ip = ipv6->daddr;
__u8 next_hdr = ipv6->nexthdr;
__u8 hop_limit = ipv6->hop_limit;
// L4 offset (IPv6 header is fixed 40 bytes)
__u32 l4_offset = ip_offset + sizeof(struct ipv6hdr);
// Note: This simplified version doesn't handle extension headers
// Production code should iterate through extension headers
switch (next_hdr) {
case IPPROTO_TCP:
// TCP processing
break;
case IPPROTO_UDP:
// UDP processing
break;
case IPPROTO_ICMPV6:
// ICMPv6 processing
break;
default:
return XDP_PASS;
}
5. TCP Header Parsing
#include <linux/tcp.h>
struct tcphdr *tcp;
// Bounds check
if (data + l4_offset + sizeof(struct tcphdr) > data_end)
return XDP_DROP;
tcp = data + l4_offset;
// Extract TCP fields
__be16 src_port = tcp->source; // Big endian
__be16 dst_port = tcp->dest; // Big endian
__u32 seq = bpf_ntohl(tcp->seq);
__u32 ack_seq = bpf_ntohl(tcp->ack_seq);
// Extract TCP flags
__u8 syn = tcp->syn;
__u8 ack = tcp->ack;
__u8 fin = tcp->fin;
__u8 rst = tcp->rst;
__u8 psh = tcp->psh;
// Calculate TCP header length (doff is in 32-bit words)
__u8 doff = tcp->doff;
if (doff < 5)
return XDP_DROP; // Invalid header length
__u32 payload_offset = l4_offset + (doff * 4);
// Access payload if needed
if (data + payload_offset > data_end)
return XDP_DROP;
// Payload starts at: data + payload_offset
6. UDP Header Parsing
#include <linux/udp.h>
struct udphdr *udp;
// Bounds check
if (data + l4_offset + sizeof(struct udphdr) > data_end)
return XDP_DROP;
udp = data + l4_offset;
// Extract UDP fields
__be16 src_port = udp->source; // Big endian
__be16 dst_port = udp->dest; // Big endian
__u16 len = bpf_ntohs(udp->len);
// UDP payload starts immediately after header
__u32 payload_offset = l4_offset + sizeof(struct udphdr);
if (data + payload_offset > data_end)
return XDP_DROP;
// Payload starts at: data + payload_offset
7. ICMP Parsing
#include <linux/icmp.h>
#include <linux/icmpv6.h>
// For ICMPv4
struct icmphdr *icmp;
if (data + l4_offset + sizeof(struct icmphdr) > data_end)
return XDP_DROP;
icmp = data + l4_offset;
__u8 type = icmp->type;
__u8 code = icmp->code;
// For ICMPv6
struct icmp6hdr *icmpv6;
if (data + l4_offset + sizeof(struct icmp6hdr) > data_end)
return XDP_DROP;
icmpv6 = data + l4_offset;
__u8 type = icmpv6->icmp6_type;
__u8 code = icmpv6->icmp6_code;
Complete Parsing Template
Unified IPv4/IPv6 Parser with 5-Tuple Extraction
This pattern uses IPv4-Mapped IPv6 addresses (RFC4291) to unify handling:
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/in.h>
#include <linux/in6.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
// Flow tuple structure
struct flow_tuple {
struct in6_addr src_ip; // Unified IPv4/IPv6
struct in6_addr dst_ip;
__be16 src_port;
__be16 dst_port;
__u8 protocol;
};
static __always_inline int parse_packet(void *data, void *data_end, struct flow_tuple *flow) {
// Initialize flow
__builtin_memset(flow, 0, sizeof(*flow));
// Parse Ethernet
if (data + sizeof(struct ethhdr) > data_end)
return -1;
struct ethhdr *eth = data;
__u16 eth_proto = bpf_ntohs(eth->h_proto);
__u32 l3_offset = sizeof(struct ethhdr);
__u32 l4_offset;
// Parse L3
switch (eth_proto) {
case ETH_P_IP: {
if (data + l3_offset + sizeof(struct iphdr) > data_end)
return -1;
struct iphdr *ip = data + l3_offset;
// Store IPv4 as IPv4-mapped IPv6 (::ffff:x.x.x.x)
flow->src_ip.in6_u.u6_addr32[2] = bpf_htonl(0xffff);
flow->src_ip.in6_u.u6_addr32[3] = ip->saddr;
flow->dst_ip.in6_u.u6_addr32[2] = bpf_htonl(0xffff);
flow->dst_ip.in6_u.u6_addr32[3] = ip->daddr;
flow->protocol = ip->protocol;
l4_offset = l3_offset + (ip->ihl * 4);
break;
}
case ETH_P_IPV6: {
if (data + l3_offset + sizeof(struct ipv6hdr) > data_end)
return -1;
struct ipv6hdr *ipv6 = data + l3_offset;
flow->src_ip = ipv6->saddr;
flow->dst_ip = ipv6->daddr;
flow->protocol = ipv6->nexthdr;
l4_offset = l3_offset + sizeof(struct ipv6hdr);
break;
}
default:
return -1; // Unsupported protocol
}
// Parse L4
switch (flow->protocol) {
case IPPROTO_TCP: {
if (data + l4_offset + sizeof(struct tcphdr) > data_end)
return -1;
struct tcphdr *tcp = data + l4_offset;
flow->src_port = tcp->source;
flow->dst_port = tcp->dest;
break;
}
case IPPROTO_UDP: {
if (data + l4_offset + sizeof(struct udphdr) > data_end)
return -1;
struct udphdr *udp = data + l4_offset;
flow->src_port = udp->source;
flow->dst_port = udp->dest;
break;
}
default:
// For ICMP, ports are 0
break;
}
return 0; // Success
}
Usage in eBPF Program
SEC("xdp")
int xdp_parser(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct flow_tuple flow;
if (parse_packet(data, data_end, &flow) < 0)
return XDP_PASS; // Parsing failed
// Now use the flow data
bpf_printk("Proto: %d, Src Port: %d, Dst Port: %d",
flow.protocol,
bpf_ntohs(flow.src_port),
bpf_ntohs(flow.dst_port));
return XDP_PASS;
}
Important Verifier Requirements
- Always bounds check before dereferencing pointers
- Use
static __always_inlinefor helper functions (prevents function calls) - Avoid loops or use bounded loops with
#pragma unroll - Use
__builtin_memsetfor structure initialization - Be careful with pointer arithmetic - always check bounds after calculating offsets
Common Pitfalls
- Forgetting bounds checks (verifier will reject)
- Not handling variable-length headers (IPv4 IHL, TCP options)
- Incorrect offset calculations
- Not using
bpf_ntohs/bpf_ntohlfor network byte order - Accessing context fields that don't exist for the program type
Best Practices
- Create reusable parsing functions with
static __always_inline - Use helper functions to keep code modular
- Add debug prints with
bpf_printk()during development - Test with various packet types (IPv4, IPv6, fragmented, etc.)
- Handle edge cases (malformed packets, invalid headers)
- Document expected packet formats