courses

Analyzing traffic with Wireshark

When you want to explore, not just capture

tcpdump shines when you know exactly what you want and can filter at capture time. Wireshark is the other half of that workflow: the GUI for the times when the capture is exploratory, when you grabbed more than you needed, or when you want to follow a stream, chart a conversation, or dissect a protocol byte by byte. Because both rest on the same libpcap, Wireshark happily opens tcpdump captures — many of us capture on the command line and analyze here. This page covers the two filter systems (capture vs. display), the multi-valued-field gotcha that trips everyone up, working with sub-traces, and feeding Wireshark live from a remote tcpdump.

Introduction

Up to now, we’ve been doing all our packet captures on the command line, and any filters get applied at the point of capture. If you are confident in your filters and know for sure what you want to capture, this is totally fine – why capture more data than you want or need? However, we often aren’t in that situation. Quite often, the capture is exploratory in nature, or it turns out we want just a single flow but didn’t know that at the time of capture, or any number of other reasons that we end up capturing more than is “necessary”.

Even if, like me, you eventually decide that you prefer capturing at the command line, but post-processing the data in another tool. Usually, that other tool is Wireshark. In many ways, Wireshark is the GUI version of tcpdump. They use the same underlying libpcap to capture packets, after all. Wireshark will even open tcpdump capture files and let you process them to your hearts content!

Filters

When we were working with tcpdump, filters were filters, they were in BPF format, and that was that. In Wireshark, things are a little more complex, in that there are two different types of filters, they are applied in different ways, and they use completely different syntax.

Capture filters

When you first launch wireshark, you get the Welcome screen seen below. This is where you instantiate any necessary capture filters you might want to use.

Wireshark welcome screen showing the list of available network interfaces with a capture filter input field at the bottom of the window

Capture filters here are passed to libpcap, and therefore use the same syntax as tcpdump filters (BPF). These filters limit the traffic captured, and therefore if you want more data, you need to run a new capture with new filters in place (or none).

In order to actually capture data, instantiate whatever filter you want and simply hit the shark fin button in the upper left corner – this will launch a capture. If you want to stop the capture, hit the big red stop square next to the shark fin.

Display filters

Display filters, on the other hand, purely limit the data presented at the time. Entered in the textbox you see below (at the top of the window), it filters data seen in the top pane of the screen (known as the packet list pane – the middle pane is the packet details pane, and the bottom is the packet bytes pane).

Wireshark main window showing DNS traffic with the display filter bar at the top, a packet list pane in the middle, and capture statistics at the bottom showing 24 packets captured and displayed

As you can see at the bottom of the wireshark window, capture statistics are shown (Packets: 24 Displayed: 24 (100.0%)). As you add display filters, the proportion of packets displayed will vary.

Display filter syntax

Display filters are logical expressions that evaluate to a truth value for each packet. They can be combined using boolean operators. See the two tables below (taken from the wireshark documentation) for the operators – you can use either the English or C-like variant, as you desire.

Logical operators:

English C-like Description Example
eq == Equal ip.src == 10.0.0.5
ne != Not equal ip.src != 10.0.0.5
gt > Greater than frame.len > 10
lt < Less than frame.len < 128
ge >= Greater than or equal to frame.len ge 0x100
le <= Less than or equal to frame.len <= 0x20
contains   Protocol, field or slice contains a value sip.To contains "a1762"
matches ~ Protocol or text field matches a Perl-compatible regular expression http.host ~ "acme\.(org|com|net)"
bitwise_and & Bitwise AND is non-zero tcp.flags & 0x02

Source: Wireshark Documentation

Boolean operators:

English C-like Description Example
and && Logical AND ip.src==10.0.0.5 and tcp.flags.fin
or || Logical OR ip.src==10.0.0.5 or ip.src==192.1.1.1
xor ^^ Logical XOR tr.dst[0:3] == 0.6.29 xor tr.src[0:3] == 0.6.29
not ! Logical NOT not llc
[…​]   Subsequence See “Slice Operator” below.
in   Set Membership http.request.method in {"HEAD" "GET"}. See “Membership Operator” below.

Source: Wireshark Documentation

Every field in every protocol that Wireshark understands has a type, of which there are only a small number:

All of the above seems pretty straightforward, and should feel comfortable if you’ve ever written a program before (which I assume you have or you wouldn’t be here). Here’s where things get wonky. For instance, slicing syntax: tr.src[0:3]…it looks like python, right? Well, not so much. The syntax is object[n:m] where n is the starting index, and m is the length. If you would prefer a python style start:end, the syntax is object[n-m]. Simple, right? I can’t even tell you how often I mess that one up, so be aware.

One big issue

And now for the elephant in the room: display filters don’t work how you might expect. For instance, to exclude packets that use port 22 (in either port), you might want to use one of tcp.port != 22 or not tcp.port == 22. However, these don’t return the same packets! Try it, I’ll wait.

Because tcp.port is a multi-valued field – each packet has both a source port and a destination port, and Wireshark evaluates field comparisons against each value independently.

How each filter works

tcp.port != 22 Means: “at least one value of tcp.port is not 22.”

For a packet with src=22, dst=54321, tcp.port yields two values: 22 and 54321. Since 54321 != 22 is true, the whole expression is true – the packet matches. This means SSH packets (where one port is 22) still pass through.

!(tcp.port == 22) Means: “it is not the case that any value of tcp.port equals 22.”

Same packet: tcp.port == 22 is true (src port is 22), so negating it gives false – the packet is excluded. This correctly hides all SSH traffic.

Summary table
Filter SSH src=22, dst=high SSH dst=22, src=high Non-SSH
tcp.port != 22 shown (dst!=22 is true) shown (src!=22 is true) shown
!(tcp.port == 22) hidden hidden shown
The rule

With multi-valued fields, field != value and !(field == value) are not equivalent:

To actually exclude SSH, use !(tcp.port == 22) or the equivalent not tcp.port == 22. The same gotcha applies to any multi-valued field: ip.addr, eth.addr, udp.port, etc.

So which should I use?

The gold standard here is BPF. not port 22 in BPF means: exclude any packet where either endpoint is port 22. It’s a packet-level test – if 22 appears anywhere, the packet is dropped.

That makes it equivalent to !(tcp.port == 22), which also excludes packets where any value of the field matches 22.

tcp.port != 22 has no clean BPF equivalent because it passes packets that have at least one port that isn’t 22 – which is nearly every TCP packet, including SSH. It’s rarely what you want.

Quick reference

Expression Semantics Excludes SSH?
not port 22 (BPF) neither endpoint is 22 yes
!(tcp.port == 22) no value of tcp.port is 22 yes
tcp.port != 22 at least one value of tcp.port != 22 no

When translating BPF capture filters to Wireshark display filters, the safe pattern is always to wrap in !( ) rather than using != directly on multi-valued fields.

No standard tool exists for this, and the multi-valued field semantics we just discussed is exactly why automated translation is tricky. A BPF expression like not port 22 doesn’t map to a single Wireshark display filter expression in a way that’s always unambiguous.

The practical translation rules for common patterns:

BPF Wireshark display filter
port 22 tcp.port == 22 || udp.port == 22
not port 22 !(tcp.port == 22) && !(udp.port == 22)
host 1.2.3.4 ip.addr == 1.2.3.4
not host 1.2.3.4 !(ip.addr == 1.2.3.4)
src host 1.2.3.4 ip.src == 1.2.3.4
tcp tcp
port 80 or port 443 tcp.port == 80 || tcp.port == 443

The !(field == value) vs field != value distinction only matters for multi-valued fields (ip.addr, tcp.port, etc.) — for scalar fields they’re equivalent.

Please see the wireshark documentation on filters for more details.

Working with packet traces

One of the greatest strengths of wireshark is that you can work with a subset of the full trace, while leaving the full trace intact. While you could certainly do this with tcpdump and combining reading a trace with a new filter and then writing that output to a new file, that can be cumbersome. Within wireshark, we have a lot of capabilities to filter down in real time without fussing with temporary files, lots of copies, or otherwise wasting time.

Marking packets

Marking packets is one of the most useful aspect of wireshark. While capturing packets, you may (in fact almost certainly will) grab significantly more packets than just those in which you are interested. By combining display filters, you can mark all packets you’re interested in and then use the export settings to get a “sub-trace” – it’s like a subset, but of a trace rather than a set – of just those packets. Sometimes we mark packets we want to keep, sometimes we mark packets to toss. Depending on which is the least work, I use both at different times.

Following streams

A stream is a collection of packets that form a single coherent flow of information. Containing both directions, streams are a useful concept within wireshark. If you right click on a packet (any packet), and select Follow, a sub-menu will open allowing you to select the specific protocol that you want to follow. Quite often, a TCP stream may contain significantly more information than any single packet. This has become less useful in recent times due to the rise of HTTPS (there are ways around that – you’re looking for SSLKEYLOGFILE if you’re curious about one of the ways), but often things transferred around on “secure” networks rely on policy and physical security, rather than encryption, and so it still becomes really quite useful.

Capture statistics

Wireshark has an entire Statistics menu. A few of the more useful (for me, anyway) options are captured below.

Combining with tcpdump

This is actually how I use wireshark. I prefer using tcpdump to actually capture packets for lots of reasons, but primarily overhead. That said, at times it’s useful to use wireshark to actually visualize the packets, track the streams, etc. A really cool aspect is that you can stream the packets over a network connection – I’ve used ssh, but I tend to use netcat more. By launching wireshark from the command line with the arguments '-k -i -' you tell it to start capturing immediately from the “interface” of standard input. This allows for a remote capture with local analysis. VERY useful on systems with constrained resources or no local storage.

As an example:

┌─(ROOT@kali478-0:pts/4)────────────────────────────────────────────────────────────────────────(~)─┐
└─(20:51:>)── tcpdump -U -s0 -i eth1 -w - | wireshark -k -i -                                 ──(Sun,Feb02)─┘

In the above, you can easily replace the pipeline with some form of network communication. As mentioned, you could use ssh to execute the line above and it will execute the first portion on the remote host and the righthand side of the pipeline locally. You could also set up a listening socket via netcat and pipe the output to that socket from a remote host – this is usually easier than ssh on low-resource or functionality-constrained devices such as routers or IoT devices.

tshark – wireshark on the command line

tshark is the CLI version of wireshark. Often seen as an alternative to tcpdump, in most ways it is simply tcpdump with more useful defaults. Please refer to the tshark manpage for more details. Much like the Lua comment below, this is more of an existence notice.

Extending wireshark via the Lua scripting interface

Other than mentioning its existence, I won’t be saying much about the Lua extension interface. You can find the Wireshark Lua API documentation on the Wireshark developer site.

Using wireshark with non-ethernet sources

Wireshark can be used in some places where tcpdump can’t follow. Specifically, tcpdump will capture any IP protocol from any IP-like network interface. That’s all well and good when all you want is IP traffic. But sometimes we want more than that. Wireshark brings a couple specific extensions to the table that are worth bringing up.

Bluetooth capture

While bluetooth is often spoken of as if it was an IP networking protocol, it is in fact a serial protocol. Essentially, it’s a wireless UART (I’m simplifying, and it depends on profile, but most act this way, though BLE is a totally different beast). Wireshark supports capture as well as packet dissection for bluetooth.

USB capture

USB is another place where wireshark can come to the rescue if you ever need to examine things at the protocol level. I do hope, for your sake, you’re never tasked with USB analysis, but if you are, wireshark may well become your best friend.

Key takeaways

Optional Additional Resources

Some suggested viewing/reading for additional in-depth coverage:

References


Related course pages: Capturing traffic with tcpdump · Introduction to Networking

🛠️ Maintenance note: the screenshots (welcome screen, DNS view, Statistics dialogs) are from an older Wireshark release — the UI shifts between major versions, so re-shoot them if they drift. Display-filter field names occasionally change; the linked display filter reference is the authoritative list. The SSLKEYLOGFILE stream-decryption workflow and tshark defaults also evolve — verify each term.