0.8 The Go labs: build the network tools yourself
Reading about a protocol teaches you its shape. Writing a program that speaks it shows you the details: the bytes, the timeouts, the error codes, the states. From Part 3 onwards, many chapters end with a "Build it in Go" section: a small, complete program (usually 50–200 lines) that turns the chapter into something you can run, break, and extend.
Why Go
Go's standard library contains a complete network stack API (net,
net/netip, crypto/tls, net/http) with no extra dependencies. Its
goroutines make "one connection per goroutine" natural. And it's the language
much of today's network infrastructure is written in: Kubernetes,
Docker, Caddy, Traefik, CoreDNS, Consul, Prometheus, and most Envoy control
planes. You don't need to know Go well to start. Each lab explains what's
specific to Go, and the Go guide covers the language itself.
Setup (5 minutes)
# Install Go 1.22 or newer (the labs were tested with Go 1.26):
# macOS: brew install go Linux: https://go.dev/doc/install
go version
# One module holds every lab; each lab is a folder with a main.go
mkdir -p ~/netlabs && cd ~/netlabs
go mod init netlabs
mkdir dnsquery # then save the lab's code as dnsquery/main.go
go run ./dnsquery example.com
Labs marked (Linux) use Linux-only kernel features. On macOS, run them in a container. This works even on Docker Desktop:
docker run --rm -it --cap-add NET_ADMIN -v "$PWD":/labs -w /labs golang:1.26 bash
apt-get update && apt-get install -y iproute2 tcpdump # ss, tc, tcpdump inside the container
What Go's net package does underneath
Every lab uses a few calls. Knowing what each one asks the kernel to do connects the code to the chapters:
| Go call | System calls underneath (Linux) | What happens on the wire / in the kernel |
|---|---|---|
net.Dial("tcp", addr) |
socket(), non-blocking connect(), then wait in epoll |
SYN sent; returns after SYN-ACK + ACK (Chapter 21) |
net.Listen("tcp", addr) |
socket(), setsockopt(SO_REUSEADDR), bind(), listen(somaxconn) |
Socket enters LISTEN; kernel now completes handshakes by itself |
ln.Accept() |
accept4() |
Takes one finished connection off the accept queue (Chapter 52) |
conn.Read(buf) |
read(); if no data, the goroutine parks in the netpoller (epoll/kqueue) |
Drains the socket's receive buffer, which opens the window (Chapter 24) |
conn.Write(buf) |
write(); parks if the send buffer is full |
Copies into the send buffer; TCP sends when cwnd/rwnd allow |
conn.SetDeadline(t) |
none (a runtime timer) | Read/Write fail with a timeout error after t |
conn.Close() |
close() |
FIN, or RST if unread data was still waiting |
net.Dial("udp", addr) |
socket(), connect() |
Nothing is sent. Only a route and a source address are chosen |
Three Go defaults that matter later: TCP connections have TCP_NODELAY
on (Nagle off, Chapter 26), TCP keepalives on (every 15 s), and
listeners set SO_REUSEADDR, so a restarted server can rebind its port
even while old connections sit in TIME_WAIT (Chapter 23).
To see the system calls any lab makes, build it and trace it:
go build -o /tmp/lab ./dialcheck
strace -f -e trace=network /tmp/lab example.com:443 # Linux
Index of the Go labs in this guide
| Lab | Chapter | What you build |
|---|---|---|
subnet |
10, 11, 61 | Subnet calculator, subnet splitter, overlap checker, IPAM allocator |
whoami |
12 | Your private vs public address: NAT made visible |
dnsquery |
18 | A DNS client from raw bytes, with message compression |
bindlab |
19 | Bind conflicts, loopback vs wildcard, SO_REUSEPORT |
udpping |
20 | UDP echo + loss/reorder/jitter meter |
dialcheck |
21 | Classifies connected / refused / timed out / DNS failure |
stream |
22 | Proof that TCP has no message boundaries, and length-prefix framing |
closewait |
23 | A CLOSE_WAIT leak, half-close with CloseWrite |
zerowindow |
24 | A slow reader that drives the window to zero |
nagle |
26 | The 40 ms Nagle + delayed-ACK stall, on demand |
rawhttp |
28 | HTTP/1.1 by hand over TCP/TLS, decoding chunked framing |
tlsinspect |
29, 56 | A scriptable openssl s_client with failure diagnosis |
pcapread |
31–32 | A pcap decoder: Ethernet/SLL → IP → TCP/UDP |
tcpinfo (Linux) |
33 | ss -ti from inside your program: rtt, cwnd, retransmits |
netcheck |
34 | The troubleshooting ladder as one command |
prober |
46–47 | A blackbox exporter with a Prometheus latency histogram |
acceptq |
52 | Overflowing a listen queue on purpose |
policytest |
53 | Firewall policy as executable tests |
dnsdiff |
55 | Compare answers across resolvers |
certreload |
56 | Zero-downtime certificate rotation |
lbdrain |
57 | Health-checked load balancer + gracefully draining backends |
proxyproto |
57 | PROXY protocol parser: the real client IP behind an L4 load balancer |
bdp |
59 | The bandwidth-delay product, measured |
Larger projects that build on these, such as an L4 load balancer, a TLS reverse proxy, a WAF, and a DDoS-resistant edge proxy, are in the Go 120-day plan, Weeks 5–17. The individual chapters link to them.