Code lab 04 · 16 min

The packet capture field kit

How to capture, filter, and read Tailscale traffic on the wire, from STUN probes and WireGuard handshakes to DERP fallback and path migration.

Position in the code lab track 01 Reading Go for i. 02 A guided read of. 03 The pprof field . 04 The packet captu.

The case for packet evidence

Logs tell you what tailscaled believes. A packet capture tells you what actually happened. When a Customer says “it works from the office but not from the hotel,” or a node insists it has a direct connection while latency says otherwise, the wire is the only witness that does not have an opinion. This module builds your field kit: what Tailscale traffic looks like on the wire, the tcpdump filters that isolate each traffic family, how to read a connection being born, and how to interpret what you see when the payload is encrypted and always will be.

The kit assumes you can run tcpdump with root privileges on at least one end of a problem connection, and Wireshark somewhere comfortable for offline analysis. Everything here works with stock tools. No plugins, no decryption keys, no magic.

What Tailscale traffic looks like on the wire

A Tailscale node emits four distinct traffic families, and telling them apart is the first skill. Per the firewall ports documentation, direct WireGuard tunnels use UDP with a source port that defaults to 41641, STUN runs over UDP to port 3478, and connections to the coordination server and DERP relays use HTTPS on port 443 (the coordination server may also prefer port 80 with an encrypted transport, falling back to 443).

So on the physical interface of a healthy node you should expect:

  1. WireGuard UDP. Peer-to-peer tunnel traffic. Your node’s source port defaults to 41641 but is configurable (the daemon takes a -port flag, and 0 means pick one automatically), and the port you see for the remote peer is whatever its NAT assigned. This is the traffic you want to see, though as the peer relay note below explains, UDP alone does not prove the path is direct.
  2. STUN over UDP to port 3478. Short request and response pairs to Tailscale’s relay fleet. As the NAT traversal write-up puts it, your machine asks “what’s my endpoint from your point of view?” and the server replies with the ip:port it saw your UDP packet come from. These recur periodically; they are how the node keeps its public endpoint discovery fresh.
  3. DERP as TLS over TCP 443. When a direct path is not available, tunnel traffic rides an HTTPS stream to a relay. The relay blindly forwards already encrypted WireGuard traffic; it cannot decrypt anything because private keys never leave the device. On the wire this is indistinguishable from any other long-lived TLS session except by destination.
  4. Control plane HTTPS. Bursty, small, and boring: key exchanges with the coordination server, netmap updates. Also TCP to 443 (or 80). Easily confused with DERP in a capture, which is exactly why you learn to identify your DERP relay’s address first.

One more family exists as of 2025: peer relays. The DERP documentation is explicit about the order: “Tailscale first attempts to use any available peer relays in the tailnet. If there aren’t any peer relays available, it then falls back to DERP servers.” On the wire a peer relay path is UDP addressed to the relay node’s ip:port rather than the peer’s, but it is not bare WireGuard framing: the client source prefixes each packet with an 8 byte Geneve header (RFC 8926) carrying a virtual network identifier, so the WireGuard type byte sits 8 bytes further into the payload than it does on a direct path. That single fact defeats the byte-shape filter below, and it is the reason not to conclude “direct connection” from the presence of UDP alone. Confirm the far address is actually your peer by comparing against tailscale status, which prints each peer as direct <addr>, relay "<region>", or peer-relay <addr>.

The four message types, by first byte

WireGuard’s wire format is austere. Every message begins with a one-byte type followed by three reserved zero bytes, and there are exactly four types. From the WireGuard protocol specification:

First byteTypeSize on the wireWhat it means
0x01Handshake initiation148 bytesOne side starts a new session: ephemeral key, encrypted static key, encrypted timestamp, two MACs
0x02Handshake response92 bytesThe other side completes the handshake
0x03Cookie reply64 bytesLoad or DoS mitigation: prove your IP before I spend crypto cycles on you
0x04Transport data32 bytes minimumThe tunnel itself: 16 byte header plus encrypted payload plus auth tag

The sizes fall straight out of the struct definitions on the protocol page (AEAD_LEN(n) is n + 16, so type 2 works out to 4 + 4 + 4 + 32 + 16 + 16 + 16 = 92) and the wireguard-go fork Tailscale embeds hard-codes the same four numbers as MessageInitiationSize = 148, MessageResponseSize = 92, MessageCookieReplySize = 64, and an empty transport message of 32. They are gloriously constant, which makes them a fingerprint. A 148 byte UDP payload followed shortly by a 92 byte payload in the reverse direction is a WireGuard handshake completing, full stop. A stream of 32 byte type 4 messages with nothing inside is keepalives (empty encrypted payload: header plus tag and nothing else). All multi-byte fields, including the 32 bit receiver index that lets one socket multiplex many peers, are little-endian.

Here is the classification logic as a decision flow:

Classifying a UDP packet from a Tailscale node UDP packet on physical interface port 3478, or magic cookie udp[12:4] = 0x2112a442 ? yes STUN probe no first payload byte udp[8] in 1..4 and bytes 9..11 all zero ? yes: switch on udp[8] 0x01 initiation 148 bytes 0x02 response 92 bytes 0x03 cookie reply 64 bytes 0x04 transport 32+ bytes no not WireGuard framing: look elsewhere

The filter kit: tcpdump one-liners

Substitute your real interface names; eth0 stands in for the physical interface below. Each filter isolates one traffic family.

Direct WireGuard traffic, by port:

sudo tcpdump -ni eth0 'udp port 41641'

WireGuard traffic by shape, regardless of port. This is the one to memorize, because 41641 is only a default source port, and the remote peer’s port after NAT is anything at all:

sudo tcpdump -ni eth0 'udp and udp[8] > 0 and udp[8] < 5 and udp[9:2] = 0 and udp[11] = 0'

That matches a first payload byte of 1 through 4 followed by the three reserved zero bytes. False positives are possible but rare in practice; the reserved zeros do most of the filtering work.

Two blind spots come with it, both worth knowing before you trust a quiet capture. First, this filter is IPv4 only. Compile it with tcpdump -d and you can watch libpcap branch on ethertype 0x0800 and send 0x86dd straight to reject; pairing any udp[...] byte test with ip6 gets you “expression rejects all packets.” For an IPv6 path, fall back to a port or host filter. Second, it will not match peer relay traffic, because the Geneve header shifts the WireGuard type byte from udp[8] to udp[16]. If you suspect a peer relay path, add or (udp[16] > 0 and udp[16] < 5 and udp[17:2] = 0 and udp[19] = 0) and treat matches on the shifted offsets as the relayed variant.

Only handshakes. Perfect for answering “is the tunnel renegotiating constantly?”:

sudo tcpdump -ni eth0 'udp and (udp[8] = 1 or udp[8] = 2) and udp[9:2] = 0'

STUN:

sudo tcpdump -ni eth0 'udp port 3478'

DERP. First identify your relay. Run tailscale netcheck, which reports DERP latency per region and names the nearest relay, the one used for traffic. Resolve that relay’s hostname to addresses, then:

sudo tcpdump -ni eth0 'tcp port 443 and host <derp-relay-ip>'

You will not see WireGuard message types here; you will see a TLS session. What matters is its existence, its duration, and whether tunnel-scale volumes of data are moving through it when you expected a direct path.

Practical capture hygiene: add -w /tmp/case.pcap to write a file for Wireshark, -s 0 is the default snap length on modern tcpdump so full packets are kept, and -c 5000 keeps you from filling a disk on a busy exit node. Capture at both ends of a broken connection when you can, with rough clock sync; matching a handshake initiation leaving node-a against its arrival (or non-arrival) at node-b turns speculation into a verdict about which middlebox ate it.

Two capture points: inside and outside

Every Tailscale node gives you two fundamentally different places to attach tcpdump, and a complete diagnosis usually needs both.

Outside: the physical interface. Here you see ciphertext: WireGuard UDP, STUN, DERP TLS. Addresses are real-world addresses. This capture answers transport questions: is there a direct path, is the handshake completing, which relay is in use, is something dropping UDP.

Inside: the Tailscale interface. Tailscale creates a TUN device (via /dev/net/tun on Linux) that behaves like any other network interface, and traffic on it is plaintext from the node’s point of view. Find it by looking for the interface that holds the node’s Tailscale IP, which comes from the CGNAT range 100.64.0.0/10. The default name comes from defaultTunName in cmd/tailscaled: tailscale0 on Linux, utun on macOS (a magic value that claims any free utun number, so you will see utun3 or similar), Tailscale on Windows, and tun on OpenBSD. It is also settable with the daemon’s -tun flag, so when in doubt, match the 100.x address rather than trusting a name. Then:

sudo tcpdump -ni tailscale0 'host 100.101.102.103'

This capture answers intent and delivery questions: did the application actually send anything, what did it send, did the reply make it back through the tunnel, are there TCP retransmissions or MTU-shaped stalls inside the encrypted path.

Inside and outside capture points on one node node-a application (ssh, http, anything) TUN interface, 100.x addresses capture point 1: INSIDE tcpdump -ni tailscale0 plaintext, tailnet IPs tailscaled: encrypt, pick path physical interface, real addresses capture point 2: OUTSIDE tcpdump -ni eth0 WireGuard UDP, STUN 3478, DERP TLS on tcp 443

Why you need both: each capture point has a blind spot exactly where the other sees clearly. The outside capture cannot tell you what is being sent or whether it was delivered to the application; the inside capture cannot tell you how it traveled or why it is slow. The classic split diagnoses:

Reading a connection being born

Set two terminals: one capturing STUN plus WireGuard by shape on the physical interface, one capturing the TUN interface. Then from node-a, ping node-b’s tailnet address for the first time. The capture tells a story in five acts.

Act 1: STUN. Even before you did anything, periodic STUN request and response pairs to port 3478 were keeping node-a’s endpoint discovery current. Each is a small UDP exchange: request out, response back carrying the public ip:port the server observed, which is the raw material for NAT traversal (Module 03).

Act 2: relay first. Per the NAT traversal design, all connections start out with DERP preselected, so the connection is usable immediately through the fallback path while path discovery runs in parallel. In your capture that means the very first ping replies come back while the only tunnel-bearing flow is the TLS stream to the DERP relay. Do not misread this as a broken direct path; it is the deliberate starting state.

Act 3: the probe burst. Both sides begin firing UDP probes at each other’s candidate endpoints: LAN addresses, STUN-discovered public endpoints, sometimes many ports at once. Against hard NATs Tailscale leans on a birthday-paradox strategy, opening many local ports while the far side sprays random destination ports. Be careful quoting the numbers from the NAT traversal write-up, because they describe different cases. With one hard NAT and 256 ports open, it puts 50% success at 174 probes and 99.9% at 2,048. With a hard NAT on both sides the search space is squared, and even with the birthday trick “we need each side to send 170,000 probes,” about 28 minutes at 100 packets per second, which the post contrasts with “the 1.2 years it would take without the birthday paradox.” So 170,000 is the cost of the clever strategy in the worst case, not the cost of scanning sequentially. On the wire all of this is a burst of small UDP packets to addresses you may not recognize, most of which go unanswered. That is normal. Only one needs to land.

Act 4: the handshake. The moment a probe path works both ways, you see the signature pair: a 148 byte type 1 initiation, answered by a 92 byte type 2 response on the same 5-tuple. If the responder is under load you might glimpse a 64 byte type 3 cookie reply first, forcing the initiator to retry with proof of its address; rare in the field, unmistakable when present.

Act 5: transport. Type 4 messages begin flowing on the direct path, and volume on the DERP TLS stream drops to nothing. The write-up describes this as the connection transparently upgrading after a few seconds, and that is exactly what the capture shows: same conversation, new road. From here you will also see fresh handshake pairs roughly every two minutes while traffic keeps flowing: the wireguard-go fork Tailscale embeds sets RekeyAfterTime to 120 seconds, and the original initiator starts a new handshake once the session key is that old. Periodic type 1 and type 2 exchanges on a healthy tunnel are hygiene, not trouble.

Path migration mid capture

Now for the subtle one. Roam node-a from Wi-Fi to a phone hotspot, or watch a laptop wake in a new location, while the capture runs. Tailscale monitors paths continuously and, in the words of the NAT traversal write-up, upgrades connections on the fly as it discovers better paths and transparently upgrades away from previous ones, downgrading to the relay when an active path dies.

In the capture, migration has a recognizable shape:

The tell that this is migration rather than a new connection is continuity at the edges: the inside capture on the TUN interface shows the same TCP session sailing on uninterrupted (perhaps with a burst of retransmits during the gap), while the outside capture shows the transport moving between addresses. This is the whole point of the design, and being able to demonstrate it in a pcap ends arguments about whether “the VPN dropped.” The tunnel did not drop; the road under it changed.

Wireshark display filters

Bring your pcap files into Wireshark for anything beyond quick triage; it ships a WireGuard dissector. The display filters that earn their keep:

The Statistics tools repay attention too. Conversations, filtered to wg, gives per-path byte counts, which answers “which path carried the data” faster than scrolling. IO Graph with one line for wg.type == 4 and another for the DERP TCP conversation makes the moment of upgrade or migration visible as two curves crossing.

What the ciphertext will and will not tell you

Everything inside a type 4 message is encrypted, and DERP relays a stream that is opaque even to the relay operator, since private keys never leave the device that generated them. Be precise with yourself and with Customers about what a capture can therefore establish.

You can conclude: which endpoints are talking and on which ports; whether the path is direct, peer-relayed, or DERP; when handshakes occur and whether they complete in both directions; packet timing, loss and retransmission patterns at the tunnel layer; packet sizes and volumes; when a path migrated and how long recovery took. For most tailnet issues, that is the entire diagnosis.

You cannot conclude: what the payload contains; which application protocol is inside a given transport message; which inner 100.x flows map to which outer packets, when observing only the outside capture. If you need inner detail, capture on the TUN interface, where the node shows you its own plaintext honestly.

One honest caveat cuts the other way: encrypted does not mean informationless. Sizes and timing leak shape. A steady rhythm of small type 4 packets looks like an interactive session; symmetric bursts look like request and response traffic; a saturated one-way flood looks like a transfer. You may use that shape for diagnosis, and you should assume a sufficiently motivated observer on the path can too. That is not a Tailscale weakness; it is the nature of any encrypted transport, and it is why the claim you make from a pcap should always be about transport behavior, never about content.

Cross references

Sources

  1. What firewall ports should I open to use Tailscale? checked 2026-08-10
  2. WireGuard Protocol and Cryptography checked 2026-08-10
  3. DERP servers checked 2026-08-10
  4. How NAT traversal works checked 2026-08-10
  5. Tailscale CLI checked 2026-08-10
  6. What are these 100.x.y.z addresses? checked 2026-08-10
  7. wireguard-go protocol constants, Tailscale fork (device/constants.go) checked 2026-08-10
  8. wireguard-go message sizes, Tailscale fork (device/noise-protocol.go) checked 2026-08-10
  9. Tailscale Geneve header implementation (net/packet/geneve.go) checked 2026-08-10
  10. tailscaled flags and default TUN names (cmd/tailscaled/tailscaled.go) checked 2026-08-10
  11. tailscale status output source (cmd/tailscale/cli/status.go) checked 2026-08-10
  12. Wireshark display filter reference, WireGuard (wg) checked 2026-08-10

All code lab guides