Sockets, Filtering, and XDP
Three concepts in the six-part treatment: the socket layer, netfilter, and XDP and tc-BPF.
A packet passing through Linux can be inspected, modified, redirected, or dropped at half a dozen distinct places, each with a different cost, a different amount of context available, and a different set of things it is allowed to do. Knowing which hook is right for a job is most of what "networking expertise" means in practice.
Concept 1: The Socket Layer
1. What problem it solves
A socket is the boundary between a protocol's asynchronous, interrupt-driven world and a program's
synchronous one. Packets arrive at unpredictable times in softirq context; a program calls recv()
whenever it feels like it. Something must buffer between them, apply flow control, and wake the
reader.
2. Where it exists in the kernel
rg -n "struct sock \{" -A 60 include/net/sock.h | head -70
rg -n "struct proto \{" -A 40 include/net/sock.h | head -45
rg -n "SYSCALL_DEFINE3\(recvmsg|__sys_recvmsg" net/socket.c | head
rg -n "tcp_recvmsg\b|tcp_sendmsg\b" net/ipv4/tcp.c | head
3. The layering
struct socket the VFS-facing object; has a struct file
└── struct sock THE protocol-independent state
├── sk_receive_queue skbs waiting for the reader
├── sk_write_queue skbs waiting to be sent / acked
├── sk_rcvbuf, sk_sndbuf the LIMITS
├── sk_wmem_alloc bytes currently accounted
├── sk_prot ──▶ struct proto tcp_prot / udp_prot / ...
│ recvmsg, sendmsg, connect, close, hash...
└── sk_data_ready(), sk_write_space() ← THE WAKEUPS
└── struct tcp_sock (embeds struct sock; TCP's state)
The pattern is the same container_of inheritance as everywhere else:
struct tcp_sock embeds struct inet_connection_sock embeds struct inet_sock embeds
struct sock.
The receive queue and its limit are where most real socket problems live:
softirq: tcp_v4_rcv()
├── is there room? sk_rcvbuf vs sk_rmem_alloc
│ NO -> DROP. TCP will not ACK it, and the sender retransmits.
│ This is FLOW CONTROL, and it is correct behaviour --
│ but it looks like packet loss in every graph.
└── YES -> queue it, then sk_data_ready() -> wake the reader
ss -tinm | head -20 # per-socket: rcv/snd buffers, cwnd, rtt
cat /proc/sys/net/core/rmem_max /proc/sys/net/core/wmem_max
cat /proc/sys/net/ipv4/tcp_rmem /proc/sys/net/ipv4/tcp_wmem
grep -E 'TCPBacklogDrop|ListenOverflows|ListenDrops|TCPRcvQDrop' /proc/net/netstat /proc/net/snmp
Tip:
ListenOverflowsandListenDropsin/proc/net/netstatare the counters for "the accept queue was full and a connection was dropped" — a server that is not callingaccept()fast enough. Users experience it as intermittent connection failures under load; the counter names it exactly.ss -lntshows the current and maximum accept-queue depth in itsRecv-Q/Send-Qcolumns for listening sockets.
4. Experiment
CLAIM. Socket buffer limits produce drops that look like network loss but are entirely local.
METHOD.
# A server that accepts and then does NOT read:
python3 - <<'EOF' &
import socket, time
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8192)
s.bind(("127.0.0.1", 9999)); s.listen(1)
c, _ = s.accept()
print("accepted; not reading"); time.sleep(60)
EOF
sleep 1
# A client that writes hard:
python3 -c "
import socket
c = socket.create_connection(('127.0.0.1', 9999))
n = 0
try:
while n < 100*1024*1024:
n += c.send(b'x' * 65536)
except Exception as e: print('blocked/err at', n, e)
print('sent', n)
" &
sleep 3
ss -tinm '( dport = :9999 or sport = :9999 )' | head -20
grep -E 'TCPRcvQDrop|TCPBacklogDrop' /proc/net/netstat
kill %1 %2 2>/dev/null
PREDICT FIRST: the receiver never reads. How many bytes can the sender push before it blocks?
Predict it from SO_RCVBUF — and then explain why the real number is larger.
Then the accept queue:
python3 -c "
import socket, time
s = socket.socket(); s.bind(('127.0.0.1', 9998)); s.listen(1) # backlog=1
time.sleep(30)" &
sleep 1
for i in $(seq 50); do (python3 -c "
import socket
try: socket.create_connection(('127.0.0.1', 9998), timeout=1)
except Exception: pass" &) ; done
sleep 3
ss -lnt sport = :9998
grep -E 'ListenOverflows|ListenDrops' /proc/net/netstat
kill %1 2>/dev/null
5. Failure mode
| Mistake | Symptom |
|---|---|
| Diagnosing socket-buffer drops as network loss | Weeks spent on the network; the counter was local all along |
A listen() backlog that is too small | ListenOverflows; intermittent connection failures under load |
| Not reading fast enough | The window closes, the sender stalls, and it looks like the network is slow |
Setting SO_RCVBUF explicitly | It disables auto-tuning, and the fixed value is usually worse |
Sleeping in sk_data_ready | It runs in softirq context |
Assuming send() returning means transmitted | It means copied into the socket's write queue |
Concept 2: Netfilter
1. What problem it solves
Firewalling, NAT, and connection tracking need to see packets at well-defined points in the routing path — and crucially, they need to see them at different points depending on whether the packet is inbound for this host, outbound from it, or being forwarded through it.
Netfilter is five hooks placed at exactly those points, plus the infrastructure for registering handlers on them.
2. Where it exists in the kernel
ls net/netfilter/
rg -n "enum nf_inet_hooks" -A 10 include/uapi/linux/netfilter.h
rg -n "NF_HOOK\(" net/ipv4/ip_input.c net/ipv4/ip_output.c | head
nft list ruleset 2>/dev/null | head -20
iptables-save 2>/dev/null | head -20
3. The five hooks
┌──────────────────┐
│ LOCAL PROCESS │
└───▲──────────┬───┘
LOCAL_IN │ │ LOCAL_OUT
│ ▼
wire ──▶ PRE_ROUTING ──▶ [routing] ──▶ POST_ROUTING ──▶ wire
│ ▲
│ FORWARD │
└──────────┘
PRE_ROUTING before the routing decision -- DNAT happens here
LOCAL_IN destined for this host
FORWARD passing through
LOCAL_OUT generated by this host
POST_ROUTING just before transmit -- SNAT/MASQUERADE happens here
Connection tracking (nf_conntrack) sits on these hooks and gives the stack a notion of a
connection rather than isolated packets — which is what makes stateful firewalling and NAT
possible, and which costs a hash-table entry per connection.
sudo conntrack -L 2>/dev/null | head
cat /proc/sys/net/netfilter/nf_conntrack_max /proc/sys/net/netfilter/nf_conntrack_count
grep -E 'conntrack' /proc/net/stat/nf_conntrack 2>/dev/null | head -3
Warning:
nf_conntrack: table full, dropping packetis one of the most common production networking failures and it is invisible unless you look. The table is finite, entries live for a timeout after the connection ends, and a machine under a connection flood fills it and starts dropping everything. Checknf_conntrack_countagainstnf_conntrack_maxbefore you believe any other diagnosis.
nftables replaced iptables. iptables still works — through a compatibility layer over the
same infrastructure — but new work goes into nft, and the underlying model (one unified table,
a bytecode VM, sets and maps as first-class objects) is genuinely better.
4. Experiment
CLAIM. The hooks fire in a defined order and you can watch a packet traverse them.
METHOD.
# Trace a packet through the hooks with nftables' built-in tracing:
sudo nft add table inet t 2>/dev/null
sudo nft add chain inet t trace_chain '{ type filter hook prerouting priority -300; }'
sudo nft add rule inet t trace_chain ip daddr 1.1.1.1 meta nftrace set 1
sudo nft monitor trace &
ping -c 2 1.1.1.1 >/dev/null 2>&1
sleep 2; kill %1; sudo nft delete table inet t
# Or with bpftrace, at the source:
sudo bpftrace -e '
kprobe:nf_hook_slow { @hooks = count(); }
tracepoint:skb:kfree_skb { @drops[args.reason] = count(); }' &
ping -c 5 1.1.1.1 >/dev/null 2>&1
sudo nft add table inet blk; sudo nft add chain inet blk c '{ type filter hook output priority 0; }'
sudo nft add rule inet blk c ip daddr 1.1.1.1 drop
ping -c 5 -W 1 1.1.1.1 >/dev/null 2>&1
sudo nft delete table inet blk; kill %1
PREDICT FIRST: when a packet is dropped by a netfilter rule, what SKB_DROP_REASON_* appears?
And is it dropped at LOCAL_OUT or POST_ROUTING for an outbound ping?
5. Failure mode
| Mistake | Symptom |
|---|---|
nf_conntrack table full | Everything drops, and nothing in the firewall rules explains it |
Rules at PRE_ROUTING expecting a post-NAT address | The translation has not happened yet |
| A rule at the wrong hook for NAT | DNAT belongs at PRE_ROUTING, SNAT at POST_ROUTING. Not interchangeable. |
| Long linear rule chains | Every packet walks them. Use sets and maps in nft. |
Mixing iptables and nft rules | They share infrastructure but the interaction is confusing |
| Conntrack on a high-throughput forwarding path | An entry and a lookup per connection; sometimes the right answer is to bypass it |
Concept 3: XDP and tc-BPF
1. What problem it solves
Netfilter and the socket layer are downstream of sk_buff allocation, protocol parsing, and routing.
For a DDoS scrubber that wants to drop 20 million packets per second, all of that is wasted work
before the decision to drop.
XDP runs a BPF program in the driver, before an sk_buff exists at all — on the raw DMA
buffer, immediately after the NIC wrote it.
2. Where it exists in the kernel
rg -n "enum xdp_action" -A 10 include/uapi/linux/bpf.h
rg -n "struct xdp_md \{" -A 12 include/uapi/linux/bpf.h
rg -n "bpf_prog_run_xdp|xdp_do_redirect" net/core/filter.c include/linux/filter.h | head
ls samples/bpf/ 2>/dev/null | grep -i xdp | head
ls tools/testing/selftests/bpf/progs/ 2>/dev/null | grep -i xdp | head
$EDITOR Documentation/networking/af_xdp.rst 2>/dev/null || ls Documentation/networking/ | grep -i xdp
3. Where every hook sits
wire
│
▼
┌──────────────────────────────────────────────────────────────┐
│ XDP in the DRIVER, on the raw buffer. │
│ NO sk_buff yet. Fastest possible. Most limited. │
│ Returns: XDP_PASS / DROP / TX / REDIRECT / ABORTED│
└──────────────────────────────────────────────────────────────┘
│ XDP_PASS
▼
build an sk_buff ◀── the allocation XDP avoided
│
┌──────────────────────────────────────────────────────────────┐
│ tc INGRESS (clsact + BPF) the skb exists; full helpers, │
│ metadata, and skb rewriting │
└──────────────────────────────────────────────────────────────┘
│
┌──────────────────────────────────────────────────────────────┐
│ NETFILTER PRE_ROUTING → routing → LOCAL_IN │
└──────────────────────────────────────────────────────────────┘
│
┌──────────────────────────────────────────────────────────────┐
│ SOCKET: cgroup/skb BPF, sockmap, socket filters (BPF/classic) │
└──────────────────────────────────────────────────────────────┘
│
the application
| Hook | Sees | Can | Cost |
|---|---|---|---|
| XDP | The raw buffer | Drop, transmit back out, redirect to another interface or to AF_XDP, modify headers within limits | Lowest. No allocation. |
| tc ingress/egress BPF | A full sk_buff | Everything XDP can, plus arbitrary rewriting and full helper access | Low, after the skb cost |
| netfilter | An skb, post-routing-decision | Stateful filtering, NAT, conntrack | Moderate; conntrack adds more |
| socket BPF / cgroup | Per-socket, per-cgroup | Per-application policy | Late, but with the most context |
The three XDP modes, and knowing which you are in matters enormously for performance:
| Mode | Where | Note |
|---|---|---|
| Native | In the driver, on the raw buffer | The real thing. Requires driver support. |
Generic (xdpgeneric) | After the skb is built | Works on any driver. Slower than not using XDP at all — it is for development only. |
| Offloaded | On the NIC's own processor | Very few NICs |
ip -d link show dev eth0 | grep -i xdp # shows which mode is attached
ethtool -i eth0 | head -3 # the driver, which determines native support
Warning: If you benchmark XDP and it is slower than the normal path, you are almost certainly in generic mode.
virtio_netandvethdo support native XDP; many other drivers do not.ip -d link showtells you: look forxdpversusxdpgenericin the output.
4. Experiment
CLAIM. XDP drops packets before the stack does any work, and the difference is visible in
/proc/net/softnet_stat and in CPU usage.
METHOD. Lab 13 does this properly. The shape:
# Baseline: flood, and measure receive processing.
sudo bpftrace -e 'tracepoint:net:netif_receive_skb { @ = count(); }' &
ping -f -c 20000 <peer> >/dev/null 2>&1
kill %1
# Then attach an XDP program that drops the same traffic, and repeat.
# @ should be ZERO -- netif_receive_skb is never reached.
PREDICT FIRST: with an XDP program returning XDP_DROP, what does netif_receive_skb count?
What do the interface's rx_packets statistics show — and why are those two answers different?
5. Failure mode
| Mistake | Symptom |
|---|---|
| Benchmarking in generic mode | XDP is slower than no XDP, and the conclusion is backwards |
Returning XDP_TX without adjusting headers | Malformed frames on the wire |
Reading past xdp_md->data_end | The verifier rejects it — which is the safety net working |
| Assuming XDP sees all traffic | It is per-interface, and it is ingress only |
| Using XDP for something needing conntrack | XDP has no connection state; that is what tc-BPF and netfilter are for |
| Dropping in XDP and wondering why counters disagree | Interface counters increment before XDP; stack counters do not |
| A large XDP program | The verifier has complexity limits, and they are the constraint you will hit |
Choosing a Hook
| You want to | Use |
|---|---|
| Drop a flood at line rate | XDP (native mode) |
| Load-balance packets across backends | XDP with XDP_TX/XDP_REDIRECT |
| Deliver packets straight to user space, bypassing the stack | AF_XDP |
| Rewrite or encapsulate with full skb context | tc-BPF (clsact) |
| Stateful firewalling or NAT | netfilter / nftables |
| Per-container or per-cgroup policy | cgroup BPF |
| Per-application filtering | socket filters, or sockmap |
| Shape or prioritize outbound traffic | a qdisc (the previous chapter) |
| Understand where packets are going | kfree_skb drop reasons, first |
Validation / Self-check
- What is
struct sockand how doesstruct tcp_sockrelate to it? - What happens when a packet arrives and
sk_rcvbufis exhausted? Why does that look like network loss? - Which counters name accept-queue overflow, and what application bug do they indicate?
- Why is setting
SO_RCVBUFexplicitly usually a mistake? - Name the five netfilter hooks and say where DNAT and SNAT each belong.
- What does conntrack add, what does it cost, and what is its characteristic production failure?
- Where does XDP run relative to
sk_buffallocation? What does that buy and what does it cost? - Name the five XDP return actions.
- Name the three XDP modes and say which one makes benchmarks lie.
- Compare XDP, tc-BPF, netfilter, and socket BPF on what each can see and do.
- You need stateful NAT at 10 Mpps. Which hook, and what is the tension?
- Packets are disappearing between the NIC and the application. Give your first three measurements, in order.
Next: Lab 13 — An XDP Packet Counter — run code before the kernel has built a packet.