There is a particular class of problem in network analysis that experienced practitioners know well: the machine generating the interesting traffic is not the machine running Wireshark. The server is headless. The embedded device has no GUI. The cloud instance is 2,000 miles away behind a bastion host. You need full packet-level visibility, you need it live, and you need it rendering in Wireshark's dissector engine on your local workstation.

The solution is deceptively elegant -- a raw byte stream piped across an encrypted SSH channel directly into Wireshark's stdin. But to use it well, to trust it, and to troubleshoot it when things go wrong, you need to understand what is happening beneath the surface at each step. This article takes you through that entire stack.

Why This Technique Exists: The Headless Capture Problem

Wireshark is built around libpcap, the portable C library for low-level network traffic capture that underlies tcpdump, Wireshark's dumpcap, and dozens of other tools. libpcap opens a raw socket or BPF (Berkeley Packet Filter) device, applies kernel-level filtering, and hands packets to user space. On Linux, this requires either root privileges or a process with the CAP_NET_RAW and CAP_NET_ADMIN capabilities. On a remote server, you already have those (or can get them via sudo). What you don't have is Wireshark's GUI, which is why you need a way to bring the packet stream to your local machine rather than taking Wireshark to the packets.

The core insight: tcpdump can write raw libpcap-format binary to stdout. SSH can carry that binary stream over an encrypted TCP connection to your local machine. Your local shell can pipe it into Wireshark. No files are written to disk on either end. No specialized daemon is needed on the remote server. All you need is an SSH server and tcpdump.

What Is Actually Moving Through the Pipe

Before writing a single command, you should understand what the data format looks like. This will explain why certain flags are mandatory.

The libpcap savefile format

When tcpdump writes to a file -- or to stdout with -w - -- it produces a binary stream in the libpcap savefile format. The format is defined in the IETF draft draft-ietf-opsawg-pcap and documented in pcap-savefile(5) from the tcpdump project.

The stream begins with a 24-byte global header:

libpcap global header layout
Bytes 0-3:   Magic number (0xa1b2c3d4 for microsecond timestamps,
             0xa1b23c4d for nanosecond timestamps)
Bytes 4-5:   Major version number (currently 2)
Bytes 6-7:   Minor version number (currently 4)
Bytes 8-11:  Reserved (should be 0, was historically GMT offset)
Bytes 12-15: Reserved (should be 0, was historically sigfigs)
Bytes 16-19: Snaplen -- maximum bytes captured per packet
Bytes 20-23: Link-layer header type (DLT_EN10MB = 1 for Ethernet)

The magic number serves two purposes: it identifies the file as a libpcap capture, and it encodes endianness. A reader encountering 0xd4c3b2a1 instead of 0xa1b2c3d4 knows the file was written by a little-endian host and byte-swaps accordingly. Wireshark handles this transparently.

After the global header, each captured packet is represented by a 16-byte per-packet header followed by the raw packet bytes:

per-packet header layout
Bytes 0-3:   Timestamp seconds (Unix epoch)
Bytes 4-7:   Timestamp microseconds (or nanoseconds if magic 0xa1b23c4d)
Bytes 8-11:  Captured length (bytes actually saved)
Bytes 12-15: Original length (bytes on the wire before truncation)

This is a simple, flat, seekable binary format. When you stream it over a pipe, Wireshark reads it incrementally -- each time a new per-packet header and its associated data arrive, Wireshark can decode and display the packet without needing the rest of the stream. This is why the -w - flag in tcpdump works: stdout is a perfectly valid write target, and the binary format is self-describing enough that Wireshark can parse it as a stream.

pcap vs pcapng: What Wireshark Natively Produces

The format described above is the legacy pcap format. Since version 1.8, Wireshark's native capture format is pcapng (PCAP Next Generation), defined in the IETF draft draft-ietf-opsawg-pcapng. pcapng uses a block-based structure that supports multiple interfaces per file, per-packet comments, name resolution blocks, and richer metadata. When you capture directly from within Wireshark, the saved file is pcapng.

When piping through tcpdump, however, you receive legacy pcap. tcpdump writes pcap by default; its pcapng support is experimental and not enabled in standard distributions. Wireshark reads both formats transparently, so there is no compatibility problem -- but you should be aware of the distinction if you are processing the capture files downstream with tools that may not handle both. A file received from this pipeline will always begin with the pcap magic bytes (0xa1b2c3d4 or 0xa1b23c4d), never a pcapng Section Header Block.

The Berkeley Packet Filter: What Happens Before Bytes Leave the Kernel

When tcpdump opens an interface, it loads a compiled filter program into the operating system kernel. This is the Berkeley Packet Filter (BPF), originally described by Steven McCanne and Van Jacobson at Lawrence Berkeley National Laboratory in their 1993 USENIX paper "The BSD packet filter: a new architecture for user-level packet capture."

BPF provides a raw interface to data link layers and allows a userspace process to supply a filter program that specifies which packets it wants to receive. BPF returns only packets that pass the filter the process supplies -- avoiding copying unwanted packets from kernel to user space, which greatly improves performance.

Performance Note

BPF filtering happens before any bytes are copied to user space, before they enter tcpdump's process, before they enter the SSH channel. When you write not port 22 in your capture filter, that expression is compiled by libpcap into BPF bytecode and loaded into the kernel via the SO_ATTACH_FILTER socket option. On modern Linux kernels, that bytecode is JIT-compiled to native machine instructions.

The original 32-bit classic BPF (cBPF) has been extended to eBPF (extended BPF) since kernel 3.18, when the full eBPF virtual machine and userspace interface landed. (Foundational eBPF instruction set work began in kernel 3.15, but the complete implementation -- including the bpf() syscall and userspace visibility -- arrived in 3.18.) The eBPF virtual machine has 11 64-bit registers total: 10 general-purpose registers (R0 through R9) plus a read-only frame pointer (R10). The authoritative IETF eBPF Instruction Set Specification v1.0 and the Linux kernel documentation describe the set as "10 general purpose registers and a read-only frame pointer register" -- the distinction matters because R10 cannot be used as a general-purpose destination. When you run tcpdump on a production Linux server, your filter expressions are being compiled and executed as JIT-compiled native code inside the kernel, with decisions made per-packet at line rate.

You can inspect the compiled bytecode yourself. Running tcpdump -i eth0 not port 22 -d dumps human-readable BPF assembler. Running -dd dumps it as a C array of struct bpf_insn structures. This is genuinely educational -- see how a filter like host 10.0.0.1 and tcp expands into a short decision tree that short-circuits quickly for non-matching frames.

cBPF and eBPF at Runtime

libpcap submits classic BPF (cBPF) bytecode to the kernel via the SO_ATTACH_FILTER socket option. On modern Linux kernels, loaded cBPF bytecode is transparently translated into eBPF representation in the kernel before execution -- you never interact with this conversion directly. The -d output from tcpdump shows the cBPF bytecode that libpcap generates; the kernel's internal eBPF representation of that same program is what actually executes. This is documented in the Linux kernel's Classic BPF vs eBPF documentation at docs.kernel.org.

The Mandatory Flags: -U and -w -

Two tcpdump flags make streaming capture over SSH work correctly. Getting either wrong produces a silent failure that is confusing to diagnose.

-w - (write raw to stdout)

The -w flag tells tcpdump to write raw libpcap format to a file instead of printing decoded text. When the filename is -, tcpdump writes to stdout. Without this flag, tcpdump writes human-readable ASCII -- decoded protocol output -- which is not a valid libpcap stream and which Wireshark will immediately reject.

-U (unbuffered output)

This flag is critical and commonly forgotten. Without -U, tcpdump uses standard C library buffering on its stdout writes. The buffer size is typically 8 KB or 64 KB depending on the platform. On a low-traffic interface, you could capture hundreds of packets and have Wireshark show nothing -- because the bytes are sitting in tcpdump's output buffer waiting for it to fill. The -U flag enables packet-buffered output: tcpdump calls pcap_dump_flush() after writing each packet, ensuring the bytes are flushed to the SSH channel immediately. This is distinct from fully unbuffered I/O; the tcpdump man page uses the term "packet-buffered" deliberately, meaning one flush per packet rather than one write per byte. Each packet immediately transits to the SSH channel and appears in Wireshark in near real-time.

Snaplen

The snaplen flag -s is also worth considering. The default in modern tcpdump is 262,144 bytes -- effectively the entire frame. For most analysis you can use -s 0 (synonymous with the default). For header-only captures to reduce SSH bandwidth, use at least -s 128 to accommodate TCP options, VLAN tags, and tunneled encapsulation headers. The legacy value -s 96 predates many modern protocol headers and will truncate option fields; treat it as an absolute minimum for IPv4-only captures, not a safe default for general use.

Three Methods for the Remote Capture

Method 1: Raw Pipe to stdin (Classic, POSIX-portable)

The simplest and most transparent approach:

bash
ssh user@remotehost 'sudo tcpdump -U -n -s 0 -w - -i eth0 not port 22' | wireshark -k -i -

Breaking this down precisely:

macOS and Windows: Launch from Terminal

On macOS and Windows, Wireshark must be launched from the terminal (not by clicking the application icon) for stdin piping to work. The GUI process started by double-clicking does not inherit the shell's stdin. On macOS, invoke Wireshark as /Applications/Wireshark.app/Contents/MacOS/Wireshark -k -i -. On Windows with WSL2, run the command from a WSL terminal and point it at the Windows Wireshark binary, or use Method 3 (sshdump) which integrates with the GUI directly.

Method 2: Named Pipe (FIFO) for Better Control

A named pipe (FIFO) lets you decouple the SSH process from Wireshark, making it easier to restart one without killing the other:

bash
mkfifo /tmp/remote_capture
ssh user@remotehost 'sudo tcpdump -U -n -s 0 -w - -i eth0 not port 22' > /tmp/remote_capture &
wireshark -k -i /tmp/remote_capture

The FIFO blocks on open until both a writer and a reader are connected, which is why Wireshark must be started before or immediately after the SSH command. The FIFO has a kernel-managed buffer (65,536 bytes by default on Linux since kernel 2.6.11, adjustable via fcntl(F_SETPIPE_SZ) up to the limit set in /proc/sys/fs/pipe-max-size) that acts as a cushion during brief processing spikes -- when Wireshark's dissector is busy with a complex protocol stack, packets queue in the FIFO rather than backing up into tcpdump's output buffer. Clean up with rm /tmp/remote_capture when done; FIFOs persist on the filesystem between accesses.

Method 3: Wireshark's sshdump Extcap (GUI-integrated)

Wireshark ships with a binary called sshdump that implements the extcap interface -- an API that allows external capture programs to appear as Wireshark capture interfaces. When you open Wireshark and scroll through the interface list, you will see "SSH remote capture" listed. That is sshdump presenting itself as a first-class capture source.

Verifying sshdump Is Installed

sshdump is included with most Wireshark distributions but is not always present in minimal installs. On Debian and Ubuntu it ships in the wireshark-common package. To verify: run wireshark --extcap-interfaces 2>&1 | grep sshdump from the terminal. If no output appears, locate your extcap directory with wireshark --extcap-interfaces 2>&1 | head -1 or check the path shown under Wireshark's Help > About Wireshark > Folders > Extcap path, and confirm sshdump (or sshdump.exe on Windows) is present there.

The extcap mechanism works through a defined protocol: when Wireshark enumerates interfaces, it calls sshdump --extcap-list and sshdump --extcap-interfaces; when the user clicks Start, Wireshark calls sshdump with a --fifo argument pointing to a temporary named pipe it has already created, plus all the connection parameters. sshdump then establishes the SSH connection and streams capture data into that fifo.

To use sshdump from the command line directly:

bash
sshdump --extcap-interface=sshdump \
        --fifo=/tmp/ssh.pcap \
        --capture \
        --remote-host 192.168.1.10 \
        --remote-username admin \
        --remote-priv sudo \
        --remote-interface eth0 \
        --remote-capture-command 'tcpdump -U -i eth0 -w - not port 22'

The --remote-priv sudo argument tells sshdump to prepend sudo to the capture command on the remote host. Combined with passwordless sudo for tcpdump, this allows non-root SSH accounts to perform privileged captures without exposing the root account over SSH.

SSH Performance Considerations: Compression, Ciphers, and Multiplexing

The SSH channel is not neutral. It introduces encryption overhead, and by default it may also introduce compression. On high-volume captures, these factors matter.

Compression

SSH compression (-C) uses zlib. Raw network traffic, especially encrypted TLS sessions, is largely incompressible. Enabling compression on such traffic adds CPU overhead on both ends with minimal bandwidth savings and introduces latency through the zlib buffer. For most production captures, disable compression explicitly: add -o Compression=no to your SSH command or configure it in ~/.ssh/config.

The exception: captures of unencrypted text-based protocols (HTTP/1.1, SMTP, FTP command channels) can compress significantly, reducing bandwidth at the cost of slight CPU increase.

Cipher Selection

Modern OpenSSH negotiates [email protected] first when both sides support it, followed by AES-CTR variants (aes128-ctr, aes192-ctr, aes256-ctr). For high-throughput capture on hardware with AES-NI acceleration (all modern x86 and ARM processors), AES-CTR adds negligible overhead -- AES-NI can process multiple gigabits per second in hardware. ChaCha20 is similarly fast in software and is the preferred default precisely because it performs well on hardware without AES acceleration. You can inspect your negotiated cipher with ssh -v user@host 2>&1 | grep "cipher:".

SSH Multiplexing for Repeated Sessions

If you frequently connect to the same remote host for iterative captures, SSH multiplexing eliminates the repeated TCP handshake and key exchange. Configure ~/.ssh/config:

~/.ssh/config
Host capture-server
    HostName 192.168.1.10
    User admin
    ControlMaster auto
    ControlPath ~/.ssh/cm-%r@%h:%p
    ControlPersist 10m

ControlMaster auto enables multiplexing with intelligent behavior -- the first connection becomes the master automatically, and subsequent connections over the same ControlPath reuse the TCP connection. For repeated short capture sessions this can shorten setup time from 1-2 seconds (handshake + auth) to milliseconds. Keep in mind the control socket file is a potential attack vector; SSH mitigates this with strict permission checking (the socket is created with mode 0600, owner-only), but you should ensure your home directory is not world-readable on shared systems.

Host Key Verification

The SSH pipe inherits all standard SSH security properties, including host key verification. In automated or scripted capture contexts, resist the temptation to set StrictHostKeyChecking no -- this disables the TOFU (trust-on-first-use) protection against man-in-the-middle attacks and would allow a network adversary to intercept or tamper with the packet stream itself. If you need scriptable trust, pin the host key explicitly via known_hosts or use certificate-based host authentication. The packet data you receive is only trustworthy if the SSH channel is.

BPF Filter Craft: What You Should Actually Filter

The not port 22 filter is table stakes. Practical scenarios require richer filters.

Capture only HTTP and HTTPS traffic, excluding the SSH channel:

BPF filter
(port 80 or port 443) and not port 22

Capture traffic to and from a specific host:

BPF filter
host 10.0.0.50 and not port 22

Capture only TCP SYN and FIN packets (connection lifecycle only):

BPF filter
tcp[tcpflags] & (tcp-syn|tcp-fin) != 0 and not port 22

Capture by VLAN tag:

BPF filter
vlan 100 and not port 22

Capture UDP DNS traffic:

BPF filter
udp port 53

When composing filters, keep BPF short-circuit evaluation in mind. BPF evaluates conditions left to right with short-circuit logic. Putting the most selective (most rejecting) condition first reduces the number of instructions executed per packet. For a filter like tcp and host 10.0.0.50 and not port 22, if you expect 90% of traffic to be UDP, put tcp first -- it will reject 90% of packets with the minimum evaluation work.

One important caveat: libpcap's compiler applies its own optimizations to the BPF bytecode before loading it into the kernel, and these optimizations may reorder the decision tree independently of how you wrote the filter expression. The short-circuit ordering advice above is still sound practice -- it makes your intent explicit and tends to produce better unoptimized bytecode -- but you should verify the actual compiled output rather than assuming the text order maps directly to execution order.

Run tcpdump -d '<your filter>' -i eth0 to inspect the compiled BPF bytecode as assembler before deploying a filter. This shows you the actual decision tree the kernel will execute.

The Privilege Problem: Running Without Root

Running tcpdump as root over SSH is operationally uncomfortable. A better pattern uses Linux capabilities. Assign CAP_NET_RAW and CAP_NET_ADMIN to the tcpdump binary:

bash
sudo setcap cap_net_raw,cap_net_admin=eip /usr/sbin/tcpdump

With these capabilities set, a non-root user can run tcpdump. Combined with a dedicated capture user account, this allows you to SSH in as captureuser -- a non-root, low-privilege account -- and run tcpdump directly, without sudo, without exposing root via SSH, and without the account needing write access to anything except possibly /tmp.

Verify the capability assignment with getcap /usr/sbin/tcpdump. The =eip means effective, inheritable, and permitted -- all three capability sets include cap_net_raw and cap_net_admin. The binary can now open raw sockets as any user who executes it.

Capabilities Are Lost on Package Upgrades

Filesystem capabilities set with setcap are stored as an extended attribute on the binary itself. When the package manager replaces the binary during a tcpdump upgrade, the new binary has no capabilities set. You must re-run the setcap command after every tcpdump update. To make this automatic, consider a post-upgrade hook (e.g., a dpkg trigger on Debian/Ubuntu, or a %post scriptlet in an RPM spec). Verify the state before relying on it in an operational context: run getcap /usr/sbin/tcpdump and confirm the capabilities are present.

Troubleshooting the Pipeline

Wireshark shows "end of file" or immediately closes the capture: tcpdump likely exited immediately. Common causes: the interface name is wrong (try -D to list available interfaces), the user lacks capture permissions, or the remote host's tcpdump is writing text output instead of binary (missing -w -).

Wireshark shows "format not recognized": The global libpcap header is corrupt or absent. The most common cause is that text was written to stdout before the binary pcap header. Two sources produce this: (1) tcpdump itself printing a warning or deprecation notice to stdout rather than stderr; (2) sudo writing a message to stdout, which can happen when a NOPASSWD rule is absent and the session is non-interactive, or in some sudo configurations where lecture or warning text is directed to stdout. The fix for both is to redirect stderr on the remote command and confirm your sudoers entry includes NOPASSWD for tcpdump: sudo tcpdump ... 2>/dev/null. You can also verify there is no stdout contamination by running the SSH command and piping to xxd | head on the local side -- a valid pcap stream always begins with d4 c3 b2 a1 (little-endian) or a1 b2 c3 d4 (big-endian).

Wireshark shows packets but they are all very delayed, appearing in bursts: The -U flag is missing. tcpdump is buffering its output, and Wireshark is receiving batches when the buffer flushes.

The SSH session is very slow or the terminal is unresponsive during capture: No BPF filter, or insufficient filter. The SSH channel is saturated with packet data. Add or tighten the filter. For immediate relief: kill tcpdump on the remote side (send q or SIGINT via another SSH session), which will cause the ssh command on the local side to receive EOF and exit cleanly.

Capture works but Wireshark dissectors show wrong protocol: The remote interface may not be Ethernet. If you are capturing on a tun/tap interface (VPN), a veth pair, or a loopback, the DLT (data link type) in the pcap header will differ from DLT_EN10MB. Wireshark adapts based on the DLT in the global header, so this usually works automatically. If you are seeing garbage dissection, run tcpdump -D to check the interface type and verify the DLT.

Wireshark shows nothing or an error when piped from stdin on older versions: On some Wireshark builds prior to version 3.x, reading from stdin with -i - is handled correctly by tshark but not by the GUI binary. If you encounter this, use Method 2 (named FIFO) or Method 3 (sshdump), both of which avoid stdin entirely. Verify your Wireshark version with wireshark --version before assuming stdin piping will work.

You need to know if the kernel dropped packets during the capture: tcpdump reports kernel and libpcap drop counts to stderr when it exits or receives SIGINFO. In the SSH pipe scenario, stderr from the remote tcpdump process is separate from stdout -- by default it flows back through SSH to your local terminal, so you will see drop statistics in your terminal window when the capture ends. To surface drops while the capture is running on Linux, query the interface statistics directly: ssh user@host 'cat /proc/net/dev | grep eth0' before and after captures, or use ip -s link show eth0 on the remote host. The tcpdump -v flag also prints periodic interface drop counts to stderr.

The Conceptual Weight of What You Are Doing

It is worth pausing on what this technique represents. You are running a kernel-resident BPF filter on a machine you are not physically touching, producing a live packet stream, encrypting it, transporting it over a TCP connection, decrypting it, piping it through a kernel FIFO, and feeding it into one of the most sophisticated protocol dissection engines in existence -- all in real time, with latency measured in milliseconds, at line rates that can exceed a gigabit per second.

The BPF virtual machine in the remote kernel is executing JIT-compiled instructions written from your filter expression. The libpcap global header written by the remote tcpdump tells your local Wireshark exactly what link-layer encapsulation to expect. The SSH channel provides integrity, confidentiality, and authentication simultaneously. The FIFO provides flow control and decoupling. Wireshark's dissectors parse dozens of protocol layers without any coordination with the remote host.

Each piece of this pipeline has been independently standardized, battle-tested, and documented. The pcap format has an IETF draft. BPF has a 30-year research history and Linux kernel documentation. OpenSSH's ControlMaster is in the man pages. The extcap API is in Wireshark's developer documentation. This is what it means to understand infrastructure deeply: not just memorizing commands, but knowing why each flag exists, what format the bytes are in, where the filtering happens, and where the failure modes live.

Quick Reference Command Matrix

Scenario Command
Basic live capture ssh user@host 'sudo tcpdump -U -n -s 0 -w - -i eth0 not port 22' | wireshark -k -i -
Filter to specific host 'sudo tcpdump -U -n -s 0 -w - -i eth0 host 10.0.0.50 and not port 22'
Named pipe method mkfifo /tmp/cap; ssh user@host '...' > /tmp/cap & wireshark -k -i /tmp/cap
Inspect BPF bytecode tcpdump -d 'not port 22 and tcp' -i eth0
Capabilities instead of sudo sudo setcap cap_net_raw,cap_net_admin=eip /usr/sbin/tcpdump
All interfaces (cooked mode) 'sudo tcpdump -U -n -s 0 -w - -i any not port 22'
No DNS resolution Add -n flag to tcpdump
Save to disk simultaneously ssh ... | tee /tmp/capture.pcap | wireshark -k -i -
Check kernel drop count ssh user@host 'ip -s link show eth0'
Adjust FIFO buffer size ssh ... > /tmp/cap & sleep 0.1; python3 -c "import fcntl,os; fcntl.fcntl(os.open('/tmp/cap',os.O_RDONLY|os.O_NONBLOCK),1031,1048576)"

Sources and Further Reading