Same Job, Different Shell Part 10: Packet Capture

Every tool in this series so far tells you the state of something, an address, a route, a rule, a connection. Sometimes that’s not enough and you need to see the packets themselves crossing the wire. This is the deepest tool in the stack on both platforms, and it’s where the two diverge the most in how the raw capture gets from “captured” to “something you can actually read.”

Linux: tcpdump

$ tcpdump -i eth0 -n -c 4 'udp port 53'

-i interface, -n don’t resolve hostnames (a full DNS resolution loop while capturing DNS traffic is exactly the kind of thing that makes a capture confusing), -c 4 stop after 4 packets, and a BPF filter expression at the end scoping the capture to just DNS traffic instead of everything crossing the interface. Here’s a real query and response, captured live while writing this post:

$ tcpdump -i eth0 -n udp port 53
11:09:53.161618 IP 192.0.2.2.56782 > 8.8.8.8.53: 829+ [1au] A? mozilla.org. (52)
11:09:53.165197 IP 8.8.8.8.53 > 192.0.2.2.56782: 829 1/0/1 A 35.190.14.201 (56)

Two lines, and they tell the whole story: my box asked 8.8.8.8 for mozilla.org’s A record (query ID 829), and got back one answer, 35.190.14.201, about 3.6ms later. That query ID matching between request and reply is exactly how you’d correlate a slow or missing DNS response back to the specific query that caused it in a longer capture.

TCP tells a longer story. This is a full connection, captured from SYN to teardown, against a local test server:

$ tcpdump -i lo -n tcp port 8080
11:10:35.774806 IP 127.0.0.1.45454 > 127.0.0.1.8080: Flags [S], seq 1291948572, win 65495, ...
11:10:35.774893 IP 127.0.0.1.8080 > 127.0.0.1.45454: Flags [S.], seq 2249852007, ack 1291948573, ...
11:10:35.774906 IP 127.0.0.1.45454 > 127.0.0.1.8080: Flags [.], ack 1, win 64, ...
11:10:35.774971 IP 127.0.0.1.45454 > 127.0.0.1.8080: Flags [P.], seq 1:78, ack 1, ..., length 77: HTTP: GET / HTTP/1.1
11:10:35.774975 IP 127.0.0.1.8080 > 127.0.0.1.45454: Flags [.], ack 78, win 64, ...
11:10:35.776694 IP 127.0.0.1.8080 > 127.0.0.1.45454: Flags [P.], seq 1:157, ack 78, ..., length 156: HTTP: HTTP/1.0 200 OK
11:10:35.776717 IP 127.0.0.1.45454 > 127.0.0.1.8080: Flags [.], ack 157, win 64, ...
11:10:35.776791 IP 127.0.0.1.8080 > 127.0.0.1.45454: Flags [F.], seq 1139, ack 78, ...
11:10:35.776879 IP 127.0.0.1.45454 > 127.0.0.1.8080: Flags [F.], seq 78, ack 1140, ...

That’s a textbook three-way handshake ([S] SYN, [S.] SYN-ACK, [.] ACK), the actual HTTP request and response riding on top of the now-established connection, and a clean four-way teardown ([F.] FIN-ACK from each side). Being able to read the flag letters directly, S for SYN, . for ACK-only, P. for a push carrying data, F. for a FIN, is most of what you need to follow a capture without reaching for Wireshark every time.

For anything beyond a quick live read, write the capture to a file and open it properly:

$ tcpdump -i eth0 -w capture.pcap 'tcp port 443'

.pcap files from tcpdump open directly in Wireshark on any platform, which is where a real investigation usually ends up once the interesting traffic is identified.

Windows: pktmon

Windows’ modern built-in capture tool is pktmon (Packet Monitor), which replaced the older, more fiddly netsh trace approach. It’s a two-step process: capture, then convert.

Unlike most of the Windows-side commands earlier in this series, this section is sourced from Microsoft’s own documentation rather than a live capture run on real hardware, pktmon needs an elevated session and writes a capture file to disk, so it wasn’t put through the same one-command-at-a-time verification the rest of this series got. Treat the syntax below as accurate, not as tested output, the same distinction Part 8 draws around -CommonTCPPort.

# start capturing, all components, logging full packets
PS> pktmon start --capture --comp nics

# ... reproduce the problem, or just wait ...

PS> pktmon stop

That produces a .etl file (Event Trace Log format, Windows’ native tracing format, not something Wireshark reads directly). Convert it before you open it:

PS> pktmon etl2pcap PktMon.etl --out capture.pcapng

.pcapng is the same family of format tcpdump -w produces and opens in Wireshark identically either way. pktmon’s filtering happens at capture time rather than as a display filter afterward, worth setting up before you start if you already know what you’re after:

PS> pktmon filter add -p 53          # DNS only
PS> pktmon filter add -i 10.0.0.10 -t tcp   # a specific host, TCP only
PS> pktmon start --capture --comp nics

--drop-only on either pktmon start or pktmon etl2pcap is worth knowing about specifically because it has no real Linux equivalent: it captures (or extracts) only packets the Windows networking stack actually dropped, tagged with which component dropped them (a firewall filter, a virtual switch extension, a NIC driver). tcpdump shows you everything that crossed the wire; it can’t natively tell you “this packet was silently discarded by the WFP firewall layer before it reached the application”, because that decision happens above the point tcpdump is watching. pktmon --drop-only is specifically built for exactly the “this should have worked and something ate it” case a filtered port test from Part 9 leaves you with.

There’s also a PowerShell-native capture path (New-NetEventSession, Add-NetEventPacketCaptureProvider, Start-NetEventSession, Get-NetEventPacketCapture), the NetEventPacketCapture module underneath pktmon itself. In practice pktmon’s CLI is simpler for the same job and is what Microsoft’s own docs lead with now, so that’s what’s worth learning first; the cmdlets are there if you’re building capture into a larger PowerShell automation script rather than running it by hand.

Quick reference

What you wantLinuxWindows
Start a capturetcpdump -i <if> -w out.pcap '<filter>'pktmon start --capture --comp nics
Stop itCtrl+C, or -c N to auto-stoppktmon stop
Live read while capturingdefault (no -w)not directly; convert and open after
Filter to a port/protocolBPF filter expression at capture timepktmon filter add -p <port> before starting
Capture only dropped packetsnot natively supportedpktmon start --drop-only / pktmon etl2pcap --drop-only
Get a Wireshark-readable filenative (.pcap)pktmon etl2pcap <file>.etl --out <file>.pcapng

What’s next

Part 11 closes the series with a single consolidated cheat sheet, every command pair from Parts 1 through 10 in one reference table.

Cross-references: for a much deeper tcpdump treatment, BPF filter syntax, capture rotation, and converting FortiGate CLI captures into a real pcap, see tcpdump Deep Dive: BPF Filters, Capture Rotation, and Cross-Mapping to FortiGate’s diag sniffer; for advanced capture recipes on established connections, see The Packet Never Lies: Advanced tcpdump Recipes for the Enterprise Engineer.