Part 13 — Professional networking: operations, debugging, and design
Parts 1–12 teach the protocols. This part teaches the work: operating networks safely, collecting evidence during incidents, changing designs without breaking users, and debugging failures that cross hosts, firewalls, cloud platforms, containers, DNS, TLS, and load balancers.
Chapter 52 — Host networking internals: namespaces, veth, bridges, conntrack
In one sentence
Most "cloud/container networking" is Linux building blocks composed together: interfaces, routes, namespaces, virtual Ethernet pairs, bridges, NAT, connection tracking, and the queues between the NIC and your program.
The problem
A packet reaches a server and the application never sees it. Or the application is "up" but clients time out. Or a node starts dropping connections at exactly the same load every day. These failures happen inside the host, after the network has delivered the packet. To debug them you need a map of the host's own packet path.
The mental model
On one Linux host:
- a network namespace is a separate copy of interfaces, routes, ARP/ND tables, firewall rules, and sockets. Every container has one;
- a veth pair is a virtual cable with two ends. Put one end in a namespace and the other on the host;
- a bridge is a software switch (Chapter 7) that connects many veth ends;
- netfilter (driven by
nftablesoriptables) is a set of hooks where rules can accept, drop, reject, or rewrite packets; - conntrack remembers every flow so NAT and stateful firewalling work;
- queues sit between the hardware and your code, and every one of them can overflow.
How it actually works: the receive path
NIC ── DMA into RX ring buffer ── IRQ → NAPI poll (softirq)
│ GRO merges segments; RSS/RPS spread flows across CPUs
▼
┌──── netfilter PREROUTING ────┐ conntrack lookup, DNAT (port-forwards,
│ (raw → conntrack → mangle → nat) │ kube-proxy Service IPs)
└──────────────┬───────────────┘
routing decision: for me, or forward?
┌─────────────────────┴──────────────────────┐
▼ local ▼ forward (router, container host)
INPUT hook (filter) FORWARD hook (filter)
│ │
TCP: SYN queue → handshake → ACCEPT QUEUE POSTROUTING (SNAT/MASQUERADE)
│ │
accept() → socket RECEIVE BUFFER → read() out the other interface
Every box is somewhere a packet can disappear, and each has a counter:
| Where | Symptom when it overflows | Where to look |
|---|---|---|
| NIC RX ring | rx_missed_errors, rx_no_buffer |
ethtool -S eth0 |
| softirq backlog | drops under very high packet rates | /proc/net/softnet_stat (2nd column) |
| conntrack table | nf_conntrack: table full, dropping packet |
dmesg, conntrack -S |
| SYN queue | SYN floods; SYN cookies kick in | nstat TcpExtSyncookiesSent |
| accept queue | slow connects (1 s, 3 s retries) while the app looks idle | ss -ltn (Recv-Q vs Send-Q), nstat TcpExtListenOverflows |
| socket receive buffer | zero window (Chapter 24) | ss -tm, nstat TcpExtTCPRcvQDrop |
Practice (30 min): build a container network by hand
Run this only on a Linux lab machine or VM. You're building what Docker's default bridge network builds:
# 1. A namespace ("the container") and a virtual cable
sudo ip netns add app
sudo ip link add veth-host type veth peer name veth-app
sudo ip link set veth-app netns app
# 2. A bridge ("docker0") on the host, with the host end plugged in
sudo ip link add br0 type bridge
sudo ip addr add 10.200.0.1/24 dev br0
sudo ip link set br0 up
sudo ip link set veth-host master br0
sudo ip link set veth-host up
# 3. Inside the namespace: an address, loopback, and a default route
sudo ip netns exec app ip addr add 10.200.0.2/24 dev veth-app
sudo ip netns exec app ip link set lo up
sudo ip netns exec app ip link set veth-app up
sudo ip netns exec app ip route add default via 10.200.0.1
ping -c 2 10.200.0.2 # host -> "container" works
# 4. Let the namespace reach the internet: forwarding + NAT (MASQUERADE)
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A POSTROUTING -s 10.200.0.0/24 ! -o br0 -j MASQUERADE
sudo ip netns exec app ping -c 2 1.1.1.1 # works only after step 4
# 5. Watch conntrack record the NAT translation
sudo conntrack -L -p icmp 2>/dev/null | head
Clean up:
sudo ip netns del app
sudo ip link del br0
sudo iptables -t nat -D POSTROUTING -s 10.200.0.0/24 ! -o br0 -j MASQUERADE
Step 5 prints a line like this (from a real run):
icmp 1 29 src=10.200.0.2 dst=1.1.1.1 type=8 ... src=1.1.1.1 dst=172.17.0.2 type=0 ...
Read it as two halves. The original direction (src=10.200.0.2 dst=1.1.1.1) is what the namespace sent. The reply direction
(src=1.1.1.1 dst=172.17.0.2) is what conntrack expects back: replies come
to the host's address, and conntrack rewrites them back to 10.200.0.2.
That one table entry is the whole of NAT.
Step 4 is the one people forget. Without IP forwarding the host won't route
the namespace's packets, and without MASQUERADE the replies have nowhere to
go: 10.200.0.2 is a private address the internet can't route back to
(Chapter 12).
What to inspect during real incidents
ip -br addr; ip route; ip neigh # addresses, routes, ARP
ss -s # socket totals by state
ss -ltn # listeners: Recv-Q vs Send-Q = accept queue
sudo nft list ruleset # firewall + NAT rules (or: iptables-save)
sudo conntrack -S # per-CPU conntrack stats: look at "drop", "insert_failed"
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
nstat -az | grep -Ei 'overflow|drop|prune|collapse'
Fingerprint: if ss shows the service listening on 127.0.0.1 only,
no firewall or route change will make it reachable from another host. The
program is bound to loopback (Chapter 19's bindlab).
Build it in Go (20 min) — overflow an accept queue on purpose
The accept queue is the least understood queue in this list. The kernel
completes handshakes without your program's involvement and parks
finished connections until the program calls Accept(). A program that stops
accepting (deadlocked, all workers busy, a long GC pause in older runtimes)
keeps "accepting" connections until the queue is full. After that, new SYNs
are silently dropped and clients see 1-second, 3-second connect delays while
your dashboards show an idle process.
// acceptq: fill a listening socket's accept queue on purpose.
//
// The kernel completes TCP handshakes on its own and parks finished
// connections in the accept queue until the program calls Accept(). A program
// that stops calling Accept() (deadlocked, GC-paused, all workers busy) still
// "accepts" connections -- until the queue is full. Then new SYNs are dropped
// and clients see slow connects (SYN retries at 1 s, 3 s, ...).
//
// go run ./acceptq
// # Linux, in another terminal: ss -ltn 'sport = :7400' (Recv-Q = queued, Send-Q = queue size)
// # nstat -az TcpExtListenOverflows TcpExtListenDrops
package main
import (
"fmt"
"net"
"time"
)
func main() {
ln, err := net.Listen("tcp", "127.0.0.1:7400") // Go asks for backlog = net.core.somaxconn
if err != nil {
panic(err)
}
defer ln.Close()
fmt.Println("listening, but NEVER calling Accept()")
var conns []net.Conn
for i := 1; i <= 5000; i++ {
start := time.Now()
c, err := net.DialTimeout("tcp", ln.Addr().String(), 2*time.Second)
took := time.Since(start)
if err != nil {
fmt.Printf("connection %d: %v after %v <- queue full, SYN dropped\n", i, err, took.Round(time.Millisecond))
break
}
conns = append(conns, c)
if took > 500*time.Millisecond {
fmt.Printf("connection %d took %v <- SYN was dropped and retried\n", i, took.Round(time.Millisecond))
}
if i%1000 == 0 {
fmt.Printf("%d connections completed the handshake without Accept()\n", i)
}
}
fmt.Printf("\n%d connections are waiting in the accept queue.\n", len(conns))
fmt.Println("Look now with ss/netstat; exiting in 30s.")
time.Sleep(30 * time.Second)
}
Real output on Linux:
$ sysctl net.core.somaxconn
net.core.somaxconn = 4096
$ go run ./acceptq
listening, but NEVER calling Accept()
1000 connections completed the handshake without Accept()
...
4000 connections completed the handshake without Accept()
connection 4098: dial tcp 127.0.0.1:7400: i/o timeout after 2.001s <- queue full, SYN dropped
$ ss -ltn 'sport = :7400'
State Recv-Q Send-Q Local Address:Port
LISTEN 4097 4096 127.0.0.1:7400 <- 4097 waiting, queue size 4096
$ nstat -az TcpExtListenOverflows TcpExtListenDrops
TcpExtListenOverflows 2
TcpExtListenDrops 2
What to notice: for a LISTEN socket, ss reuses its columns:
Recv-Q is the current accept-queue length and Send-Q is its maximum. Go
asks for a backlog of net.core.somaxconn, so the queue held 4,096 (plus
one, a long-standing Linux off-by-one) before dropping. ListenOverflows
is the counter to alert on. It means "my process wasn't calling accept() fast
enough".
Exercises:
- Add a goroutine that calls
Accept()once every 10 ms. What's the steady-state queue length now? (Little's law: arrival rate × time in queue.) - Lower
somaxconnto 128 (sudo sysctl -w net.core.somaxconn=128), rerun, then restore it. Older kernels defaulted to 128, which is why "raise somaxconn" appears in every old tuning guide. - Read how containers get their own copies of these sysctls in the
Linux guide's container chapter.
somaxconnis per network namespace.
Common confusions
- "Conntrack is only for firewalls." Conntrack is also what makes NAT, Docker port publishing, and Kubernetes Services work. A full conntrack table breaks all of them at once, even with no firewall rules at all.
- "iptables and nftables are different firewalls." Both are front ends to
the same netfilter hooks in the kernel. Modern distributions translate
iptablescommands into nftables rules (iptables -Vshowsnf_tables). - "A namespace is a container." A namespace is one isolation feature. A container is several namespaces plus cgroups plus a filesystem (Linux guide, Chapter 44).
Check yourself
- Draw the path of a packet from the NIC to
read()and name two places it can be dropped. - Why does a namespace need both IP forwarding and MASQUERADE on the host to reach the internet?
ss -ltnshowsRecv-Q 4097 Send-Q 4096for a listener. What's wrong, and is it the network's fault?- What breaks when the conntrack table is full?
Further reading
- Book: Linux Kernel Networking (Rami Rosen): the receive path, netfilter, and conntrack, chapter by chapter.
- Article: "Monitoring and Tuning the Linux Networking Stack: Receiving Data" (Packagecloud blog): the definitive walk through the NIC → socket path with every counter.
- Across the series: Linux guide, Chapter 69 (namespaces, nftables, packet paths) and Chapter 50 (Docker networking) show these pieces from the operator's side.