Dissecting a Capture with tshark

tshark is Wireshark’s dissection engine with the graphical shell removed — “a terminal oriented version of Wireshark designed for capturing and displaying packets when an interactive user interface isn’t necessary or available” (Wireshark User’s Guide). It reads the same capture files, runs the same thousands of protocol dissectors, and understands the same display-filter language as the GUI, but it emits text you can pipe into Unix tooling. Where tcpdump prints a terse per-packet summary and leaves you to read hex, tshark turns raw bytes into fully-named protocol fields — tcp.flags.syn, http.request.method, tls.handshake.type — that you can filter on, extract into columns, and aggregate into statistics. This makes it the tool of choice when you need to turn a .pcap into a table, a CSV, or a number, non-interactively and repeatably, in a script or over SSH. Its options are specified in tshark(1).

This note is the reference-layer companion for the dissection and extraction workflow. It owns the tshark command grammar — the filter split, field extraction, statistics, stream following, and decode-as. For the capture-filter language that -f and tcpdump share, see The pcap Capture Filter Language; for the display-filter language -Y shares with the GUI, see Wireshark Display Filter Syntax; for grabbing the packets in the first place, see Capturing Packets with tcpdump. It deliberately does not re-teach protocol mechanics — for what a TCP flag means, cross-link the conceptual sibling.

Mental Model — Capture, Dissect, Filter, Extract

The right way to think about tshark is as a four-stage pipeline that takes bytes and produces meaning, with a filter available at two different stages that are often confused. Bytes enter (live from an interface, or replayed from a file with -r). A capture filter (-f, pcap syntax) can cull them before dissection — this is the kernel-side, irreversible drop inherited wholesale from BPF. The dissector then decodes each surviving packet into a tree of named fields. A display filter (-Y, Wireshark syntax) culls after dissection, keying on those decoded field names — reversible, and able to reference anything the dissector produced. Finally an output stage decides what to emit: a summary line, a full detail tree (-V), a hex dump (-x), specific extracted columns (-T fields -e), or an aggregate statistic (-z).

flowchart LR
    IN["Bytes in<br/>live iface · or -r file.pcap"] --> CF{"-f capture filter<br/>(pcap syntax, BPF)<br/>pre-dissection, irreversible"}
    CF --> DIS["Dissector<br/>bytes → named field tree<br/>tcp.flags.syn, http.request.method"]
    DIS --> DF{"-Y display filter<br/>(Wireshark syntax)<br/>post-dissection, reversible"}
    DF --> OUT["Output stage"]
    OUT --> O1["summary line (default)"]
    OUT --> O2["-V full detail tree"]
    OUT --> O3["-x hex + ASCII"]
    OUT --> O4["-T fields -e ...<br/>columns for scripting"]
    OUT --> O5["-z ...<br/>statistics / stream follow"]
    O4 -.pipe.-> UNIX["sort · uniq -c · awk · grep"]

    style CF fill:#fde68a,stroke:#b45309
    style DF fill:#e0e7ff,stroke:#4338ca
    style O4 fill:#dcfce7,stroke:#15803d

The tshark pipeline. What it shows: two filters at two stages — -f before the dissector (pcap/BPF, the same language as The pcap Capture Filter Language) and -Y after it (Wireshark display syntax, the same as Wireshark Display Filter Syntax) — feeding one of several output stages. The insight to take: the two filters are not interchangeable. -f is fast and lossy and can only see raw bytes; -Y is reversible and can see every dissected field but only runs on packets that survived -f. Reach for -f to make a firehose survivable; reach for -Y to explore what you kept.

The Two Filters — -f vs -Y

This is the distinction that trips up everyone, so tshark makes it explicit in two separate options. -f “set[s] the capture filter expression. This syntax is defined by the pcap library; this syntax is different from the display filter syntax” (tshark(1)). -Y applies a filter that “uses the syntax of read/display filters, rather than that of capture filters” (tshark(1)). Concretely:

  • Capture side: tshark -f 'tcp port 443' — pcap/BPF, kernel-side, packets that fail are never dissected or written. See The pcap Capture Filter Language.
  • Display side: tshark -r cap.pcap -Y 'tcp.flags.syn == 1 && tcp.flags.ack == 0' — Wireshark syntax, runs on the already-dissected tree, references named fields. See Wireshark Display Filter Syntax.

A useful discipline when analysing a file: you almost always want -r + -Y, because the file is already captured and you are exploring; you cannot un-drop what an over-tight -f discarded at capture time. (There is also a subtle -R/two-pass read filter, but -Y is the single-pass display filter you want by default.)

Extracting Fields — -T fields -e

The feature that makes tshark scriptable is field extraction. Setting -T fields switches output from human summaries to machine columns, and each -e fieldname adds one column — a field “to display if -T ek|fields|json|pdml is selected” (tshark(1)). Any field name the dissector knows (the same names the display filter uses) can be extracted:

tshark -r cap.pcap \
  -Y 'http.request' \
  -T fields \
  -e frame.number \
  -e ip.src \
  -e tcp.srcport \
  -e http.request.method \
  -e http.host \
  -e http.request.uri \
  -E header=y -E separator=, -E quote=d

Line by line: -r cap.pcap replays the file; -Y 'http.request' keeps only packets the dissector tagged as HTTP requests; -T fields selects column output; each -e names one column, in output order; and the -E options control the formatting — header=y prints a header row, separator=, makes it comma-separated, quote=d wraps values in double quotes. The -E field is documented to control “separator, header, quote, escape, occurrence, aggregator, bom” (tshark(1)). The result is a CSV you can open in a spreadsheet or feed to awk. This one command is the workhorse of packet analysis at scale: the GUI shows you one packet’s fields beautifully; -T fields shows you one field across every packet, which is what you need to find the outlier.

Two related output formats matter for tooling. -T json and -T pdml (an XML dialect) emit the entire dissection tree as structured data, for when you want a program rather than a human to consume it; -T ek produces newline-delimited JSON suited to bulk-loading into Elasticsearch. Use -T fields when you know exactly which columns you want; use -T json when a downstream program will decide.

Piping Into Unix Text Tooling

The payoff of column output is that the rest of your shell becomes packet-analysis tooling. Because each packet is a line and each field a column, the standard filters compose:

# Top 10 talkers by source IP among SYN packets
tshark -r cap.pcap -Y 'tcp.flags.syn==1 && tcp.flags.ack==0' \
       -T fields -e ip.src \
  | sort | uniq -c | sort -rn | head
 
# Every distinct HTTP Host requested, deduplicated
tshark -r cap.pcap -Y http.request -T fields -e http.host \
  | sort -u
 
# TLS SNI server names (what a passive observer still learns)
tshark -r cap.pcap -T fields -e tls.handshake.extensions_server_name \
  -Y 'tls.handshake.type == 1' \
  | sort | uniq -c | sort -rn

The pattern tshark … -T fields -e X | sort | uniq -c | sort -rn — extract a field, count occurrences, rank — answers a huge fraction of real network questions (“who is sending the most SYNs?”, “which hostnames are being resolved?”, “what user-agents are hitting me?”) without ever opening the GUI. This is the entire reason tshark exists as a separate binary.

Statistics — -z

For aggregates that don’t reduce to a per-packet column, tshark has a built-in statistics engine behind -z, run once over the whole capture (usually paired with -q to suppress per-packet output). The taps are numerous; the ones you actually reach for:

-z invocationProducesNotes
-z conv,tcpTable of all TCP conversations (socket pairs) with frames/bytes/durationconv,TYPE also supports eth, ip, ipv6, udp, sctp, wlan… (tshark(1))
-z endpoints,ipPer-endpoint traffic totalsSame TYPE set as conv
-z io,stat,INTERVAL[,FILTER]…Packet/byte counts bucketed by time intervalInterval in seconds; 0 = whole capture; extra filters add columns
-z follow,tcp,ascii,NThe reassembled bytes of one TCP streamStream selected by index N or by host:port,host:port
-z expertWireshark’s expert-info notices grouped by severityerror / warn / note / chat
-z http,treeHTTP response-code and request-method distributionQuick health view of an HTTP capture

The I/O statistics form is more powerful than it first looks. -z io,stat,interval[,filter][,filter]… “collect[s] packet/bytes statistics for the capture in intervals” (tshark(1)), and each trailing filter becomes an extra column, so you can graph, say, retransmissions per second against total packets per second:

tshark -r cap.pcap -q -z io,stat,1,\
'COUNT(tcp.analysis.retransmission)tcp.analysis.retransmission',\
'COUNT(frame)frame'

Here the interval is 1 second and two calculated columns are requested using the documented aggregate functions — COUNT|SUM|MIN|MAX|AVG|LOAD(field) and FRAMES|BYTES (tshark(1)) — the first counting retransmission markers, the second counting all frames, each per one-second bucket. That single command turns a capture into a time series of “how bad was the retransmission rate, second by second,” which is exactly the kind of question the GUI’s I/O-graph answers but that you can now compute headless and pipe onward.

Following a Stream — -z follow

TCP delivers a byte stream chopped into segments that may arrive out of order and retransmitted; reading the application-layer conversation means reassembling them. -z follow,tcp,ascii,N “displays the contents of a stream between two nodes” (tshark(1)), with the mode selecting the rendering — ascii, hex, raw, ebcdic, utf-8, or yaml — and the stream identified either by its numeric index or by the two endpoints:

# Stream #3 as ASCII (client bytes plain, server bytes tab-indented)
tshark -r cap.pcap -q -z follow,tcp,ascii,3
 
# The same stream selected by endpoints
tshark -r cap.pcap -q -z follow,tcp,ascii,10.0.0.5:52344,93.184.216.34:80

The ascii mode prefixes the second node’s data with a tab so you can tell request from response (tshark(1)). raw (hex without the ASCII gutter) is what you want when you intend to pipe the bytes into another decoder; yaml is the machine-readable form. Following a stream is how you reconstruct an HTTP exchange or a plaintext protocol from a capture without hand-reassembling segments — the reassembly sequence-number logic is done for you by the dissector.

Decode-As — Teaching the Dissector -d

Dissection is driven by heuristics and well-known ports, so traffic on a non-standard port gets mis- or under-dissected — TLS on port 8443 may show up as raw TCP because 8443 isn’t the registered 443. -d fixes this: “like Wireshark’s Decode As… feature, this lets you specify how a layer type should be dissected” (tshark(1)), with the example form -d tcp.port==8888,http. So to force the TLS dissector onto an alternate port:

tshark -r cap.pcap -d tcp.port==8443,tls -Y tls.handshake

-d tcp.port==8443,tls tells the engine “treat TCP port 8443 as TLS,” after which tls.* fields exist for -Y and -e to reference. Without it, the TLS handshake on 8443 is invisible to any tls.* filter because no TLS dissector ever ran. Decode-as is the small but essential piece of glue that makes tshark useful against real-world services on non-default ports.

Full Detail and Hex — -V and -x

When you want the human view of one packet rather than a machine column, -V “cause[s] TShark to print a view of the packet details” (tshark(1)) — the entire dissection tree, every field expanded, the same content the GUI shows in its middle pane. -x “print[s] a hex and ASCII dump of the packet data after printing the summary and/or details” (tshark(1)) — the raw bytes, offset-labelled, which is where you go to reconcile the dissected fields against the actual wire and cross-check the header diagrams. Combined with a tight -Y selecting a single packet and -c 1, -V -x is the “show me everything about this one packet” command:

tshark -r cap.pcap -Y 'tcp.flags.syn==1 && tcp.flags.ack==0' -c 1 -V -x

Reading -x output against The Transmission Control Protocol Header Field by Field is exactly the exercise Mapping a Capture Back to the Header Diagrams walks end to end.

Failure Modes and Common Misunderstandings

-f 'tcp.port==443' failed.” You put display-filter syntax into the capture-filter option. -f is pcap syntax (tcp port 443); the dotted form goes in -Y. This is the same two-language split that catches people in the GUI. See The pcap Capture Filter Language vs Wireshark Display Filter Syntax.

“My tls.* filter matches nothing though I can see the handshake.” The traffic is on a non-standard port and no TLS dissector ran — add -d tcp.port==PORT,tls.

-T fields printed blank columns.” The field name is wrong or the dissector didn’t populate it for these packets (e.g. http.host on a response, or a field that only exists after reassembly). Verify the exact field name by inspecting one packet with -V, or by pressing on the field in the GUI to read its filter name.

“The reassembled stream is missing bytes.” Segments were dropped at capture (snaplen too small, or a capture filter culled them), or the capture started mid-stream. tshark can only reassemble what was captured; see the snaplen discussion in Capturing Packets with tcpdump.

“Decrypting TLS didn’t work.” tshark can decrypt TLS given a key-log file (the SSLKEYLOGFILE a browser writes), supplied via the tls.keylog_file preference (-o tls.keylog_file:path), but only when the session used a key-exchange whose secrets the log captures — you cannot decrypt someone else’s traffic without those secrets. This is a property of TLS, not a tshark limitation.

When to Use tshark vs the Alternatives

NeedToolWhy
Interactive exploration, click-to-drillWireshark GUIThe visual dissection tree and coloring rules
Terse live look, minimal deps, capture-onlyCapturing Packets with tcpdumpUbiquitous, tiny, kernel-filter first
Scripted extraction, CSV/JSON, stats, headlesstsharkFull dissection + -T fields/-z, pipeable
Kernel-side volume reduction before capturecapture filter (-f / tcpdump)BPF drops before the copy; see The pcap Capture Filter Language
Post-hoc field queries on a saved filedisplay filter (-Y / Wireshark)Reversible, field-named; see Wireshark Display Filter Syntax

The honest positioning: tcpdump captures, the Wireshark GUI explores, and tshark is the scriptable middle — the tool you reach for when the question is “compute this over the whole capture and give me a table,” and when it must run without a display, in a pipeline, or over SSH.

Production Notes

In practice tshark is most valuable at the two ends of an incident. Early, -z io,stat and -z conv,tcp give a fast, headless triage of a capture (“is there a retransmission storm? who are the top talkers?”) without dragging a multi-gigabyte file into a GUI. Late, -T fields -e … | sort | uniq -c extracts the one column that proves the hypothesis. Two operational cautions carry over from capture reality: first, dissecting a very large file with -V or heavy reassembly is CPU- and memory-hungry, so pre-narrow with -Y (or better, a capture-time -f) before asking for full detail; second, the fields you see reflect the captured bytes, so offloads that alter checksums or coalesce segments on the local host (Checksum Offloads, Segmentation Offloads GSO TSO) can make a local capture disagree with what left the wire — a capture on a mirror port or a second host is the tie-breaker. For the capstone that ties tshark’s decoded fields back to every header layout, see Mapping a Capture Back to the Header Diagrams.

See Also