Every other troubleshooting tool on a Linux box is telling you what it believes. ss believes a socket is established, the application log believes it sent the request, the router believes it has a neighbour. tcpdump is the only one that tells you what actually left the NIC. When the device claims one thing and the network is doing another, the capture settles it in about eight seconds.
This is a working reference for tcpdump for network engineers, not a first-day walkthrough: interface selection, BPF filter syntax, reading the output line, writing and re-filtering pcap files, snaplen, and the ring-buffer flags that let a capture run overnight without filling a disk. Everything below was captured on a Debian 13 VM running tcpdump 4.99.5, bridged onto the same segment as three iol-xe routers running IOS-XE 17.18.2 in CML. It is one slice of the packet analysis toolkit a network engineer actually needs, and it is the slice that works everywhere, because tcpdump is on the box already.
Two things in here are not in the usual tutorials: why a rotating capture stops after exactly one file even under sudo, and a telnet login to a Cisco router with the username and password readable in the capture, character by character.
What this was captured on
The capture host is a Linux VM with a second NIC bridged into a virtual topology, so the routers on the wire are real IOS-XE control planes generating real OSPF traffic. Timestamps drift between blocks because each block is a separate run.
Three OSPF speakers on the segment is deliberate: the wire is never quiet, so no command here has the "nothing is happening" problem. Put a routing protocol on your lab segment and you have a free traffic generator.
Permissions come before filters
The number one tcpdump failure is not a filter mistake. It is this:
! Command: tcpdump -i ens224 -c 1 # as user j, no sudo
tcpdump: ens224: You don't have permission to perform this capture on that device
(socket: Operation not permitted)Opening a packet socket is privileged, and on Debian 13 the binary carries neither route around that:
! Command: which tcpdump; ls -l $(which tcpdump)
/usr/bin/tcpdump
-rwxr-xr-x 1 root root 1273880 Feb 9 2025 /usr/bin/tcpdump
! Command: getcap $(which tcpdump) # file capabilities, if any
<-- empty: no cap_net_raw eitherNo setuid bit, no cap_net_raw. So you run under sudo, or you grant the capability yourself with setcap cap_net_raw,cap_net_admin=eip /usr/bin/tcpdump (which has consequences, since anyone who can then execute it reads every frame on the box). Remember this, because tcpdump does something with those privileges later that surprises people.
Pick the interface, and prove you picked the right one
-D lists what is capturable, and it is the fastest way to avoid the second-most-common mistake, which is capturing on the management NIC and wondering where the traffic went:
! Command: tcpdump -D # list capture interfaces
1.ens192 [Up, Running, Connected]
2.ens224 [Up, Running, Connected] <-- the NIC on the lab segment
3.any (Pseudo-device that captures on all interfaces) [Up, Running]
4.lo [Up, Running, Loopback]
5.bluetooth-monitor (Bluetooth Linux Monitor) [Wireless]
6.nflog (Linux netfilter log (NFLOG) interface) [none]
7.nfqueue (Linux netfilter queue (NFQUEUE) interface) [none]-i any looks like the safe answer and mostly is not: it gives you a Linux cooked-capture link type instead of Ethernet, so no Ethernet header and no VLAN tag. Name the physical interface.
Be honest about where the host sits, too: tcpdump only sees frames delivered to that NIC, so its own traffic, broadcast, multicast, and whatever a SPAN session feeds it. If you cannot get a Linux host onto the segment, capture on the router itself with EPC is the Cisco-side equivalent, and on a firewall the ASA has its own capture engine with the same idea and different syntax.
Bound the capture, and use -nn every time
Two flags belong in muscle memory. -c N stops after N packets so the command always terminates, which matters when you are running tcpdump over the same SSH session you are capturing. -nn disables name resolution for addresses and ports. Compare a default run with the same run under -nn:
! Command: tcpdump -i ens224 -c 8
listening on ens224, link-type EN10MB (Ethernet), snapshot length 262144 bytes
14:55:47.957527 IP 192.168.99.1 > ospf-all.mcast.net: OSPFv2, Hello, length 84
14:55:50.603060 IP 192.168.99.3 > ospf-all.mcast.net: OSPFv2, Hello, length 84
14:55:50.883184 IP 192.168.99.2 > ospf-all.mcast.net: OSPFv2, Hello, length 84
8 packets captured
9 packets received by filter
0 packets dropped by kernel
! Command: tcpdump -i ens224 -nn -c 8
14:56:14.914958 IP 0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from 00:0c:29:b1:cc:47, length 301
14:56:16.913287 IP 192.168.99.1 > 224.0.0.5: OSPFv2, Hello, length 84
14:56:18.903784 IP 192.168.99.3 > 224.0.0.5: OSPFv2, Hello, length 84
14:56:19.218986 IP 192.168.99.2 > 224.0.0.5: OSPFv2, Hello, length 84
8 packets captured
8 packets received by filter
0 packets dropped by kernelospf-all.mcast.net is prettier and useless. You wanted 224.0.0.5, because that is the number you will grep for. Worse, every unresolved address triggers a reverse DNS lookup, which slows the capture and injects your own DNS queries into the capture you are trying to read (enough to cause drops on a busy box). One -n kills host names, the second kills port names, and there is essentially never a reason not to use both.
Add -v or -vv when you want the protocol decoded rather than summarised. tcpdump knows more protocols than people expect:
! Command: tcpdump -i ens224 -nn -v -c 3 'proto ospf'
14:56:37.758339 IP (tos 0xc0, ttl 1, id 1866, offset 0, flags [none], proto OSPF (89), length 104)
192.168.99.3 > 224.0.0.5: OSPFv2, Hello, length 84 [len 52]
Router-ID 3.3.3.3, Backbone Area, Authentication Type: none (0)
Options [External, LLS]
Hello Timer 10s, Dead Timer 40s, Mask 255.255.255.0, Priority 1
Designated Router 192.168.99.3, Backup Designated Router 192.168.99.2
Neighbor List:
1.1.1.1
2.2.2.2That single Hello gives you the timers, area, mask, DR and BDR, and this router's neighbour list. Every classic adjacency-mismatch cause, without touching the router.
Reading the output line
The default one-line format is the same shape for every IP packet:
14:56:58.677331 IP 192.168.99.100.48124 > 192.168.99.1.22: Flags [S], seq 1351658335, win 64240, options [mss 1460,sackOK,TS val 3102991609 ecr 0,nop,wscale 7], length 0Left to right: microsecond timestamp (host clock, so NTP matters if you are correlating with anything else), the protocol tcpdump decoded, then source-address.source-port > destination-address.destination-port. That last octet-looking field is the port, which trips people up until they notice it is a fifth number. After the colon comes protocol detail, then length, which is payload bytes and not frame bytes (a pure ACK is length 0).
The counters at the end of a live run are worth reading rather than skipping. packets captured is what tcpdump printed or wrote, packets received by filter is what the BPF matched, and packets dropped by kernel is the one that invalidates the capture: anything above zero means the kernel ring buffer overflowed and you are missing frames.
BPF filter syntax
The filter is the last argument, and it compiles into a kernel-side BPF program, which is why a good filter costs almost nothing. It is built from qualifiers in three families: type (host, net, port, portrange), direction (src, dst), and protocol (ip, ip6, arp, tcp, udp, icmp, ether, or proto ospf for anything with no keyword of its own).
Combine them with and, or and not. Two things catch people out. not binds tighter than and, so parenthesise whenever you negate more than one term, and quote the whole filter so the shell does not eat them. And qualifiers do not carry forward the way they look: src host A or B is not what you meant, so write src host A or src host B.
The most useful filter on a lab segment is often the negative one:
! Command: tcpdump -nn -r res0017-lab-edge.pcap 'not proto ospf and not port 22'
14:57:56.051646 IP 192.168.99.100 > 192.168.99.1: ICMP echo request, id 8, seq 1, length 64
14:57:56.055154 IP 192.168.99.1 > 192.168.99.100: ICMP echo reply, id 8, seq 1, length 64
14:57:58.064454 IP 192.168.99.100 > 192.168.99.3: ICMP echo request, id 9, seq 1, length 64
14:57:58.067156 IP 192.168.99.3 > 192.168.99.100: ICMP echo reply, id 9, seq 1, length 64ARP and ICMP, with the Ethernet header on
-e prints the link-layer header, which turns an ARP exchange from an abstraction into something you can read straight down. Clear the neighbour entry first so the resolution happens in front of you:
! sudo ip neigh del 192.168.99.2 dev ens224
! Command: tcpdump -i ens224 -nn -e -c 6 'arp or icmp'
14:56:49.319348 00:0c:29:b1:cc:47 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 192.168.99.2 tell 192.168.99.100, length 28
14:56:49.323275 aa:bb:cc:00:dc:00 > 00:0c:29:b1:cc:47, ethertype ARP (0x0806), length 60: Reply 192.168.99.2 is-at aa:bb:cc:00:dc:00, length 46
14:56:49.323306 00:0c:29:b1:cc:47 > aa:bb:cc:00:dc:00, ethertype IPv4 (0x0800), length 98: 192.168.99.100 > 192.168.99.2: ICMP echo request, id 6, seq 1, length 64
14:56:49.325668 aa:bb:cc:00:dc:00 > 00:0c:29:b1:cc:47, ethertype IPv4 (0x0800), length 98: 192.168.99.2 > 192.168.99.100: ICMP echo reply, id 6, seq 1, length 64
6 packets captured
6 packets received by filter
0 packets dropped by kernelBroadcast request to ff:ff:ff:ff:ff:ff, unicast reply straight back, then the echo request goes out with the learned MAC as its destination. The host's own view agrees afterwards (192.168.99.2 lladdr aa:bb:cc:00:dc:00 REACHABLE), and that correlation is the trick: a capture that agrees with the control plane means you can stop looking at Layer 2. There is more on what the ICMP itself is doing in what ping is actually putting on the wire.
Note also that 6 packets received by filter equals 6 packets captured: the BPF narrowed this in the kernel, not at the display layer. On a 10 Gb link that is the difference between a usable capture and a lost one.
A TCP handshake, and filtering on flags
Six packets of an SSH connection to R1 covers the entire opening of a TCP session:
! Command: tcpdump -i ens224 -nn -c 6 'tcp and host 192.168.99.1 and port 22'
14:56:58.677331 IP 192.168.99.100.48124 > 192.168.99.1.22: Flags [S], seq 1351658335, win 64240, options [mss 1460,sackOK,TS val 3102991609 ecr 0,nop,wscale 7], length 0
14:56:58.681261 IP 192.168.99.1.22 > 192.168.99.100.48124: Flags [S.], seq 894975465, ack 1351658336, win 65535, options [mss 1460,sackOK,nop,nop,wscale 2,eol], length 0
14:56:58.681343 IP 192.168.99.100.48124 > 192.168.99.1.22: Flags [.], ack 1, win 502, length 0
14:56:58.682650 IP 192.168.99.100.48124 > 192.168.99.1.22: Flags [P.], seq 1:42, ack 1, win 502, length 41: SSH: SSH-2.0-OpenSSH_10.0p2 Debian-7+deb13u4
14:56:58.684111 IP 192.168.99.1.22 > 192.168.99.100.48124: Flags [P.], seq 1:20, ack 1, win 32768, length 19: SSH: SSH-2.0-Cisco-1.25
14:56:58.684164 IP 192.168.99.100.48124 > 192.168.99.1.22: Flags [.], ack 20, win 502, length 0Four milliseconds for the SYN-ACK, and both version banners in the clear before encryption starts (which is how a scanner fingerprints your gear without ever authenticating: SSH-2.0-Cisco-1.25 is a giveaway). The dot in the flag notation is ACK, so [S.] is SYN plus ACK and [.] is a bare ACK. tcpdump switches to relative sequence numbers after the first packet in each direction, which is why the SYN shows an absolute seq and the rest count from 1.
Once you can read the flags you can filter on them, with the byte-offset syntax BPF exposes when keywords run out. tcp[tcpflags] is the flags byte, and the named constants save remembering bit positions:
! Command: tcpdump -i ens224 -nn -vv -c 1 \
! 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack = 0'
14:57:13.608571 IP (tos 0xb8, ttl 64, id 59487, offset 0, flags [DF], proto TCP (6), length 60)
192.168.99.100.45308 > 192.168.99.2.22: Flags [S], cksum 0x47e6 (incorrect -> 0xcdf1), seq 2288597127, win 64240, options [mss 1460,sackOK,TS val 800626090 ecr 0,nop,wscale 7], length 0
1 packet captured
1 packet received by filterSYN set and ACK clear means connection openers only, SYN-ACKs excluded. That filter answers "who is initiating connections here", which catches a service that keeps reconnecting, and it is the defender's view of what a SYN scan looks like from the far end. If you want to build the detection out on the network gear rather than the host, catching scans from the Cisco side takes the same idea to ACLs and NetFlow.
The cksum 0x47e6 (incorrect -> 0xcdf1) is not a bug, it is checksum offload: the NIC fills the checksum in after tcpdump has seen the frame, so locally originated packets nearly always look wrong. Ignore it on transmit, take it seriously on receive.
A refused connection is just as readable. Here is R1's HTTP port answering a SYN with a reset:
! Command: tcpdump -i ens224 -nn -c 4 'tcp port 80'
14:57:16.946378 IP 192.168.99.100.47292 > 192.168.99.1.80: Flags [S], seq 2550366896, win 64240, options [mss 1460,sackOK,TS val 2988725570 ecr 0,nop,wscale 7], length 0
14:57:16.950538 IP 192.168.99.1.80 > 192.168.99.100.47292: Flags [R.], seq 0, ack 2550366897, win 0, length 0
2 packets capturedSYN answered by RST means nothing is listening, which is diagnostically different from a SYN with no answer at all (that is a drop: check ACLs, routing, firewall). R1's own show ip http server status reported the server Enabled on port 80. The device's opinion and the wire disagreed, and the wire wins.
Write a pcap, then filter it on read
-w writes raw frames to a file instead of printing them, so nothing appears on the terminal while it runs, and -s sets the snaplen, the bytes kept per frame:
! Command: tcpdump -i ens224 -nn -s 0 -w res0017-lab-edge.pcap -c 40
listening on ens224, link-type EN10MB (Ethernet), snapshot length 262144 bytes
40 packets captured
44 packets received by filter
0 packets dropped by kernel
! Command: file res0017-lab-edge.pcap
res0017-lab-edge.pcap: pcap capture file, microsecond ts (little-endian) - version 2.4 (Ethernet, capture length 262144)Modern tcpdump defaults to a 262144-byte snaplen, so -s 0 ("whole frame") is redundant on a current box and essential on an old one, where the historical default of 68 or 96 bytes truncates every payload you were trying to read. Keep the habit. Set a small snaplen deliberately only for headers-only capture off a very high-rate link.
That is a standard libpcap v2.4 Ethernet capture, so Wireshark opens it natively and so does tcpdump itself with -r. Which is the workflow that matters: capture wide once, then ask the file different questions afterwards.
! Command: tcpdump -nn -r res0017-lab-edge.pcap 'host 192.168.99.3'
reading from file res0017-lab-edge.pcap, link-type EN10MB (Ethernet), snapshot length 262144
14:57:58.064454 IP 192.168.99.100 > 192.168.99.3: ICMP echo request, id 9, seq 1, length 64
14:57:58.067156 IP 192.168.99.3 > 192.168.99.100: ICMP echo reply, id 9, seq 1, length 64
14:57:59.066514 IP 192.168.99.100 > 192.168.99.3: ICMP echo request, id 9, seq 2, length 64
14:57:59.070107 IP 192.168.99.3 > 192.168.99.100: ICMP echo reply, id 9, seq 2, length 64
14:58:00.068480 IP 192.168.99.100 > 192.168.99.3: ICMP echo request, id 9, seq 3, length 64
14:58:00.072044 IP 192.168.99.3 > 192.168.99.100: ICMP echo reply, id 9, seq 3, length 64Six frames out of forty, one target, no re-capture. The same file answering six questions, with the real counts:
A filter you got wrong at capture time cannot be undone. A filter you got wrong at read time costs one more command. Capture wider than you think you need, then narrow on read. -q is the fastest first look at an unfamiliar file, one short line per packet with the protocol detail stripped.
Long captures: -G, -W, and the privilege drop that breaks them
An intermittent problem needs a capture that runs for hours without filling the disk. -G N starts a new file every N seconds, -W N caps the number of files, and together they make tcpdump self-terminating, which is what you want in a script or a cron job. Except that on Debian it does this:
! Command: sudo tcpdump -i ens224 -nn -w 'rot-%Y%m%d-%H%M%S.pcap' -G 10 -W 2
listening on ens224, link-type EN10MB (Ethernet), snapshot length 262144 bytes
tcpdump: /home/j/plzcap/ev3/rot-20260724-150201.pcap: Permission denied
! Command: ls -l rot-*.pcap
-rw-r--r-- 1 tcpdump tcpdump 426 Jul 24 15:02 rot-20260724-150149.pcapThe whole command ran under sudo and it still failed. Look at the owner of the one file that did get written: tcpdump:tcpdump, not root. tcpdump opens the capture socket as root, then drops privileges to the unprivileged tcpdump user (Debian's packaging default). The first file is opened before the drop; every rotation after it happens as a user that cannot write into your home directory.
Two fixes, both captured:
! Fix 1 - keep the privileges with -Z root
! Command: sudo tcpdump -i ens224 -nn -w 'zrot-%Y%m%d-%H%M%S.pcap' -G 10 -W 2 -Z root
listening on ens224, link-type EN10MB (Ethernet), snapshot length 262144 bytes
Maximum file limit reached: 2
8 packets captured
8 packets received by filter
0 packets dropped by kernel
! Command: ls -l zrot-*.pcap
-rw-r--r-- 1 root root 650 Jul 24 15:02 zrot-20260724-150201.pcap
-rw-r--r-- 1 root root 560 Jul 24 15:02 zrot-20260724-150211.pcapTwo files ten seconds apart, and tcpdump exited on its own after the second. Fix 2 is to write somewhere the tcpdump user can reach, such as /tmp, with no -Z at all. That is the better habit on a shared box, since -Z root leaves the capture files owned by root.
The other half of a safe long capture is size, not time. -C 100 rotates at 100 MB per file, and -C with -W gives you a true ring buffer that overwrites the oldest file and never grows. -G without -W rotates forever and fills the disk, which is a bad outcome on a jump host. If you are correlating a capture with load you generated deliberately, driving the link with iperf3 makes the timing far easier to line up.
Payload: -A, -X, and a router password on the wire
-A renders the TCP payload as ASCII, -X puts hex beside it. Both are useless on encrypted traffic and devastating on anything that is not. Here is a telnet login to R3, read back from a pcap with tcpdump -nn -A -r telnet.pcap 'tcp port 23 and greater 60':
15:04:00.955095 IP 192.168.99.3.23 > 192.168.99.100.56194: Flags [P.], seq 13:55, ack 1, win 32768, length 42
E..R;?....7...c...cd.....$......P.......
User Access Verification
Username:
15:04:02.919204 IP 192.168.99.100.56194 > 192.168.99.3.23: Flags [P.], seq 45:51, ack 88, win 502, length 6
E...U.@.@..r..cd..c........F.$..P...G...admin
15:04:02.938662 IP 192.168.99.3.23 > 192.168.99.100.56194: Flags [P.], seq 93:105, ack 51, win 32755, length 12
E..4;P....7...c...cd.....$."...LP...o!..
Password:
15:04:04.924441 IP 192.168.99.100.56194 > 192.168.99.3.23: Flags [P.], seq 51:60, ack 105, win 502, length 9
E..1U.@.@..k..cd..c........L.$..P...G...Cisco123No decryption, no tooling, no privileged position: just a host on the same segment. The username admin and the password Cisco123 sit in the payload as text, and because telnet echoes every keystroke the characters appear a second time in the server's replies, one per packet. tcpdump decodes the option negotiation inline too, including the part where the client asks for encryption and the router declines:
15:04:00.954681 IP 192.168.99.3.23 > 192.168.99.100.56194: Flags [P.], seq 1:13, ack 1, win 32768, length 12 [telnet WILL ECHO, WILL SUPPRESS GO AHEAD, DO TERMINAL TYPE, DO NAWS]
15:04:00.967629 IP 192.168.99.3.23 > 192.168.99.100.56194: Flags [P.], seq 55:58, ack 31, win 32760, length 3 [telnet WONT ENCRYPT]Compare that with the SSH session earlier, where the only readable thing was a version banner. This is the argument for transport input ssh in one capture, and it generalises: any management protocol without transport encryption, SNMPv2c and unauthenticated syslog included, is readable by anyone who can capture on the path. If you are building the case for cleaning that up, hardening the management plane end to end is where the rest of it lives.
Two notes. greater 60 drops the pure ACKs so payload frames are not buried. And -A on a session carrying no payload prints header bytes as garbage rather than an error, so pointing it at a refused connection gives nonsense: the capture is telling you there was never any payload.
Gotchas
- The rotation trap is a permissions problem, not a bug. First rotated file succeeds, second fails with
Permission deniedeven under sudo. Check the owner of the first file:tcpdump:tcpdumpmeans you hit the privilege drop. Use-Z rootor write where that user can reach. ip[8] < 64is noisier than the tutorials admit. The classic "find traceroute" recipe matches the TTL byte, and on a router segment it fires on OSPF Hellos first, because IOS sends them with TTL 1. The same filter did catch a real traceroute probe on another run (192.168.99.100.50906 > 192.168.99.3.33434: UDP, length 32), but tighten it toudp and ip[8] < 64 and dst portrange 33434-33534, oricmp and ip[8] < 64for the Windows flavour.- Capturing on the wrong NIC. On a multi-homed host
-Dtakes two seconds and saves ten minutes of staring at an empty capture of the management LAN. - Incorrect checksums on your own transmitted packets are normal. Checksum offload means the NIC fills them in after tcpdump has seen the frame.
- Non-zero
packets dropped by kernelinvalidates the capture. Tighten the filter, raise the buffer with-B, or write to faster storage before concluding anything about missing traffic. - Never run tcpdump unbounded on a remote session. Use
-c, or-Gplus-W. If you drive it from an automation harness that cuts commands off after a timeout, detach it withsetsid nohup ./capture.sh > /tmp/cap.log 2>&1 < /dev/null &; plainnohupwith an ampersand is not enough on its own.
Key Takeaways
- Permissions first: a stock Debian binary has no setuid bit and no
cap_net_raw, soYou don't have permission to perform this captureis a sudo problem, not a filter problem. -nn -i <real-interface> -c Nis the shape of every ad hoc capture. Names cost speed and inject your own DNS into the capture.- BPF filters run in the kernel, so
received by filterequallingcapturedproves you narrowed at the right layer. Reach fortcp[tcpflags]andicmp[icmptype]when the keywords run out. - Capture wide with
-wand full snaplen, then ask the file different questions with-r. A wrong capture-time filter is unrecoverable. - Bound long captures on both axes:
-Gplus-Wfor time,-Cplus-Wfor size, and remember the privilege drop that kills the second rotated file. -Aagainst telnet shows the username and password as text, which is the strongest argument for encrypted management you will produce in ten seconds.
tcpdump reads. When you need to write the packet as well, crafting frames field by field with Scapy is the other half of the same skill, and both sit alongside the on-device capture tools in the full guide to capturing and analysing traffic on Cisco and Linux. Build the flat lab segment with a routing protocol for background noise and every command on this page is reproducible in about five minutes.