The tcpdump One-Liner I Actually Remember
A small Python wrapper around the only tcpdump invocation I can recall under pressure, plus a parser that turns its line-buffered output into JSON so I can pipe it to jq.
By @elisehuang
March 17, 2026
·
Updated May 18, 2026
382 views
3
4.4 (9)
"""tcpdump_cmd.py: the wrapper I keep so I never retype the flags."""
import shutil
def tcpdump_cmd(port, iface='any', count=100):
cmd = [
'tcpdump', '-i', iface,
'-A', # ASCII payload, so HTTP is readable
'-s', '0', # full snaplen, do not truncate at 96 bytes
'-l', # line-buffered, so piping to grep flushes
'-nn', # skip DNS and port-name lookups
]
if count is not None:
cmd += ['-c', str(count)]
cmd += [f'tcp port {port}']
return cmd
if __name__ == '__main__':
cmd = tcpdump_cmd(port=8080, count=50)
print(f'# tcpdump on PATH: {shutil.which("tcpdump") is not None}')
head, filt = cmd[:-1], cmd[-1]
print(' '.join(head) + f" '{filt}'")Every time my service stops talking to a downstream and metrics are unhelpful, I reach for tcpdump. The flags I always need are -A (ASCII payload so HTTP headers are readable), -s 0 (full snaplen, otherwise headers cut at 96 bytes), -l (line-buffered so piping to grep actually flushes), and -nn (skip DNS, which can take seconds on a slow resolver). The function exists so I stop typing -s 96 and wondering why my Authorization header is truncated. On macOS BSD tcpdump, replace -i any with an explicit interface like -i en0.
"""tcpdump_parse.py: pull one conversation out of the noise as JSON."""
import json
import re
HEADER = re.compile(
r'(?P<ts>\d\d:\d\d:\d\d\.\d+)\s+IP\s+'
r'(?P<src>[\d.]+)\.(?P<sport>\d+)\s*>\s*'
r'(?P<dst>[\d.]+)\.(?P<dport>\d+):'
)
def parse(lines, remote):
cur = None
for line in lines:
m = HEADER.match(line)
if m:
if cur and remote in (cur['src'], cur['dst']):
yield cur
cur = {**m.groupdict(), 'payload': []}
elif cur is not None and line.startswith('\t'):
cur['payload'].append(line.rstrip('\n').strip())
if cur and remote in (cur['src'], cur['dst']):
yield cur
if __name__ == '__main__':
sample = [
'12:00:01.001 IP 10.0.0.1.40000 > 1.2.3.4.443: Flags [P.], length 12\n',
'\tHello server\n',
'12:00:01.005 IP 1.2.3.4.443 > 10.0.0.1.40000: Flags [P.], length 4\n',
'\thi\n',
]
for rec in parse(iter(sample), remote='1.2.3.4'):
print(json.dumps(rec))Port 443 carries every TLS conversation on the host, so tcpdump | grep is too coarse. The regex matches the 5-tuple header line that tcpdump prints, and I accumulate the indented payload lines into a payload list. Emitting JSON means I can pipe to jq 'select(.dst == "1.2.3.4") | .payload' and slice further. The script runs against a canned sample so you can see the JSON shape without needing root or live traffic.
"""tcpdump_live.py: glue cmd builder + parser to a live subprocess."""
import json
import re
import shutil
import subprocess
HEADER = re.compile(
r'(?P<ts>\d\d:\d\d:\d\d\.\d+)\s+IP\s+'
r'(?P<src>[\d.]+)\.(?P<sport>\d+)\s*>\s*'
r'(?P<dst>[\d.]+)\.(?P<dport>\d+):'
)
def parse(lines, remote):
cur = None
for line in lines:
m = HEADER.match(line)
if m:
if cur and remote in (cur['src'], cur['dst']):
yield cur
cur = {**m.groupdict(), 'payload': []}
continue
if cur is not None and line.startswith('\t'):
cur['payload'].append(line.rstrip('\n').strip())
if cur and remote in (cur['src'], cur['dst']):
yield cur
def follow(port, remote, count=20):
if not shutil.which('tcpdump'):
print('# tcpdump missing; live capture skipped')
return 0
cmd = ['sudo', 'tcpdump', '-i', 'any', '-A', '-s', '0', '-l', '-nn',
'-c', str(count), f'tcp port {port}']
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True)
try:
for rec in parse(iter(proc.stdout.readline, ''), remote):
print(json.dumps(rec))
finally:
proc.wait()
return proc.returncode or 0
if __name__ == '__main__':
import sys
print('# would run: sudo tcpdump -i any -A -s0 -lnn -c 20 tcp port 443')
if '--live' in sys.argv:
follow(port=443, remote='1.2.3.4', count=20)This is the glue I actually run on staging. The -l flag from accordion 1 makes tcpdump line-buffered, and iter(proc.stdout.readline, '') lets the parser from accordion 2 consume packets as they arrive instead of waiting for EOF. I usually pipe the JSON to jq for further filtering. The default run prints what would execute and exits clean; pass --live (with sudo) to actually attach to traffic, which keeps the snippet safe to paste into CI.
