ngrep answers one question faster than any other tool on the box: is this string crossing the wire, and if so, in which packets? It takes a BPF capture filter like tcpdump, then runs a regular expression against the payload of everything that passes, and prints the packets that match with the payload rendered as text. That is the entire product, and it is the right tool perhaps twice a month, at which point it saves you twenty minutes.
This article is part of the Linux networking commands guide. All output below is real, captured on a Debian 13 host at 10.77.0.100 bridged into a CML lab, talking to an nginx server at 10.77.2.10 and a server running DNS and TLS listeners at 10.77.3.10.
The basic form
The argument order is ngrep [options] <pattern> <bpf filter>, which is backwards from what most people guess. The pattern comes first, the packet filter second:
j@llmbits:~$ sudo ngrep -d ens224 -q -W byline 'GET' 'tcp port 80'
interface: ens224 (10.77.0.0/255.255.255.0)
filter: ( tcp port 80 ) and ((ip || ip6) || (vlan && (ip || ip6)))
match (JIT): GET
T 10.77.0.100:36384 -> 10.77.2.10:80 [AP] #4
GET / HTTP/1.1.
Host: 10.77.2.10.
User-Agent: curl/8.14.1.
Accept: */*.
.Three flags do most of the work. -d picks the interface. -q suppresses the hash marks ngrep otherwise prints for every non-matching packet, which turn a busy link into a wall of #. And -W byline renders line breaks in the payload as actual line breaks instead of dots, which is the difference between readable and not for any text protocol.
Read the header line: T for TCP, source and destination, [AP] for the TCP flags set on that packet (ACK and PSH), and #4 for the packet's position in the capture. The trailing dots inside the payload are carriage returns that -W byline did not consume.
Change the pattern and you see the other direction of the same conversation:
j@llmbits:~$ sudo ngrep -d ens224 -q -W byline 'Server:' 'tcp port 80'
match (JIT): Server:
T 10.77.2.10:80 -> 10.77.0.100:36394 [AP] #6
HTTP/1.1 200 OK.
Server: nginx/1.29.8.
Date: Wed, 19 Aug 2026 16:12:55 GMT.
Content-Type: text/html.
Content-Length: 896.
Last-Modified: Tue, 07 Apr 2026 11:37:12 GMT.
Connection: keep-alive.
ETag: "69d4ec68-380".
Accept-Ranges: bytes.
.That is a complete response header, on a remote host, with no tooling beyond one command. This is ngrep's actual value: you did not have to write a pcap, move it, open it, find the stream and follow it.
The pattern is a real regular expression
The first argument is POSIX extended regex, not a literal string. Alternation, character classes and anchors all work. Combine that with -i for case insensitivity and -t for absolute timestamps:
j@llmbits:~$ sudo ngrep -d ens224 -q -t -i -W byline 'nginx|user-agent' 'tcp port 80'
match (JIT): nginx|user-agent
T 2026/08/19 09:12:59.054268 10.77.0.100:32858 -> 10.77.2.10:80 [AP] #4
GET / HTTP/1.1.
Host: 10.77.2.10.
User-Agent: curl/8.14.1.
Accept: */*.
.
T 2026/08/19 09:12:59.058325 10.77.2.10:80 -> 10.77.0.100:32858 [AP] #6
HTTP/1.1 200 OK.
Server: nginx/1.29.8.
Date: Wed, 19 Aug 2026 16:12:59 GMT.
Content-Type: text/html.
Content-Length: 896.One expression caught both halves of the transaction, and the timestamps show the server answered in about four milliseconds. Use -T instead of -t if you want deltas between matching packets rather than wall-clock time, which is better for spotting a stall.
Inverting the match
-v flips the sense, exactly as it does in grep. This is how you find the unexpected thing: show me everything on this port that is not the normal request.
j@llmbits:~$ sudo ngrep -d ens224 -q -v -W byline 'GET' 'tcp port 80 and greater 200'
don't match (JIT): GET
T 10.77.2.10:80 -> 10.77.0.100:43996 [AP] #1
HTTP/1.1 200 OK.
Server: nginx/1.29.8.
Date: Wed, 19 Aug 2026 16:13:19 GMT.
Content-Type: text/html.
Content-Length: 896.
Connection: keep-alive.
.
T 10.77.2.10:80 -> 10.77.0.100:43996 [AP] #2
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>Note the BPF filter doing real work there: greater 200 restricts the capture to packets over 200 bytes, which drops every bare ACK before the regex ever runs. Anything tcpdump's filter language can express, ngrep accepts.
Binary payloads and -x
Text protocols are the easy case. For anything else, -x prints a hex dump alongside the ASCII, which is how you confirm a payload is what you think it is. Here is a ping with a known pattern in it:
j@llmbits:~$ sudo ping -c 2 -p deadbeef -s 24 10.77.3.10
j@llmbits:~$ sudo ngrep -d ens224 -q -x '' 'icmp'
interface: ens224 (10.77.0.0/255.255.255.0)
filter: ( icmp ) and ((ip || ip6) || (vlan && (ip || ip6)))
I 10.77.0.100 -> 10.77.3.10 8:0 #1
00 21 00 01 0e d6 85 6a 00 00 00 00 f2 fe 02 00 .!.....j........
00 00 00 00 de ad be ef de ad be ef ............
I 10.77.3.10 -> 10.77.0.100 0:0 #2
00 21 00 01 0e d6 85 6a 00 00 00 00 f2 fe 02 00 .!.....j........
00 00 00 00 de ad be ef de ad be ef ............An empty pattern matches everything, which is the way to use ngrep as a plain payload viewer. The de ad be ef is the pattern ping was told to send, and it comes back byte for byte in the reply: type 8 code 0 out, type 0 code 0 back. That echoed payload is exactly the mechanism ping uses to detect corruption, and here you can see it working.
DNS, and where ngrep stops being useful
DNS is a binary protocol with text embedded in it, which makes it a good illustration of ngrep's limits:
j@llmbits:~$ sudo ngrep -d ens224 -q -W byline 'pinglabz' 'udp port 53'
match (JIT): pinglabz
U 10.77.0.100:53874 -> 10.77.3.10:53 #1
... .........www.pinglabz.lab.......).........
..2GeAf..q
U 10.77.3.10:53 -> 10.77.0.100:53874 #2
.............www.pinglabz.lab......www.pinglabz.lab...........
M.
..)........You can see the query name in both packets, and you can see that the answer echoes the question. What you cannot see is the record type, the response code, the TTL or the answer itself, because those are binary fields that ngrep has no dissector for. It found the string; it cannot tell you what the packet means.
That is the dividing line. If you want the DNS answer parsed, use tshark -Y dns or read the complete dig guide. ngrep is for "did this name get queried at all, and by whom," which it answers in one line on a host you just SSHed into.
The TLS wall, and the thing that leaks through it
This is the section worth the whole article. Run the same search against a plaintext HTTP session and a TLS session and watch what happens.
Plaintext, on port 80:
j@llmbits:~$ sudo ngrep -d ens224 -q -W byline 'Host:' 'tcp port 80'
T 10.77.0.100:37728 -> 10.77.2.10:80 [AP] #4
GET / HTTP/1.1.
Host: 10.77.2.10.
User-Agent: curl/8.14.1.
Accept: */*.
.The same search against a TLS listener on port 8443, while curl fetched a page from it:
j@llmbits:~$ sudo ngrep -d ens224 -q -W byline 'Host:' 'tcp port 8443'
(nothing. no match, no output)Nothing at all. The HTTP request is still there, it is just inside the encrypted record layer. What is actually on the wire is this:
j@llmbits:~$ sudo ngrep -d ens224 -q -x '' 'tcp port 8443 and greater 100'
T 10.77.0.100:60376 -> 10.77.3.10:8443 [AP] #1
16 03 01 06 0a 01 00 06 06 03 03 11 29 ee 34 1d ............).4.
76 cb 9b 66 2a ee e3 76 06 23 59 56 3f 8d f2 ba v..f*..v.#YV?...
86 ba 97 3a d9 e1 bd 30 c8 fa f5 20 d3 29 51 34 ...:...0... .)Q4
ce 6f 5b ca b6 9a a5 c6 69 50 09 ea 00 3c 13 02 .o[.....iP...<..
13 03 13 01 c0 2c c0 30 00 9f cc a9 cc a8 cc aa .....,.0........Those first bytes are readable if you know the format: 16 is a handshake record, 03 01 is the legacy version field, 01 is ClientHello, and the 13 02 13 03 13 01 further in is the TLS 1.3 cipher suite list. Everything after the handshake is opaque.
But the handshake itself is not encrypted, and that is where the leak is. Search for the server name instead:
j@llmbits:~$ sudo ngrep -d ens224 -q 'srv1' 'tcp port 8443'
match (JIT): srv1
T 10.77.0.100:55862 -> 10.77.3.10:8443 [AP] #4
....$... ..k@..U..'.............P.$......]. &.....b.K.c..Zj..N~...Y-...s..a
..<.......,.0.........+./...$.(.k.#.'.g.....9.....3.....=.<.5./............
......srv1.pinglabz.lab.....................................h2.http/1.1....
.....1.....6.4.....................................................+.......There it is, in cleartext, in the middle of an otherwise encrypted session: srv1.pinglabz.lab. That is the Server Name Indication extension in the ClientHello, sent before any key exchange has happened, because the server needs to know which certificate to present. You can also read h2 and http/1.1 from the ALPN extension, so the observer learns the protocol too.
The operational consequences are worth stating plainly. Anyone on the path can log which hostnames you connect to without breaking any encryption, which is how most "TLS-aware" filtering appliances actually work. Encrypted Client Hello exists to close this gap and is not yet widely deployed. And from a troubleshooting angle, this is genuinely useful: ngrep -q 'somehost' 'tcp port 443' will tell you whether a client is even attempting to reach a host, on a box where you cannot decrypt anything.
Reading and writing files
-O writes matching packets to a pcap and -I reads one back. The output is a standard pcap, so every other tool can open it:
j@llmbits:~$ sudo ngrep -d ens224 -q -O ngrep-match.pcap 'GET' 'tcp port 80'
T 10.77.0.100:51990 -> 10.77.2.10:80 [AP] #4
GET / HTTP/1.1..Host: 10.77.2.10..User-Agent: curl/8.14.1..Accept: */*....
T 10.77.0.100:51994 -> 10.77.2.10:80 [AP] #16
GET / HTTP/1.1..Host: 10.77.2.10..User-Agent: curl/8.14.1..Accept: */*....
j@llmbits:~$ ls -l ngrep-match.pcap
-rw-r--r-- 1 root root 336 Aug 19 09:13 ngrep-match.pcap
j@llmbits:~$ tshark -r ngrep-match.pcap
1 0.000000 10.77.0.100 → 10.77.2.10 HTTP 140 GET / HTTP/1.1
2 1.049373 10.77.0.100 → 10.77.2.10 HTTP 140 GET / HTTP/1.1Two GETs out of a full capture, 336 bytes on disk. This is ngrep's best trick as a filter rather than a viewer: use the regex to reduce a firehose to the handful of packets that matter, write those out, and hand the tiny file to tshark or Wireshark for real analysis.
One gotcha when you script it
If you wrap ngrep in timeout, use timeout -s INT. A plain SIGTERM kills ngrep before it flushes and closes the pcap, and you get a zero-byte file:
-rw-r--r-- 1 root root 0 Aug 19 09:13 ngrep-match.pcap
truncated dump file; tried to read 4 file header bytes, only got 0: SuccessWith SIGINT, ngrep exits cleanly and prints its counters, which are useful in their own right:
######exit
12 received, 2 matchedTwelve packets passed the BPF filter, two matched the regex. That ratio is the fastest way to tell whether your pattern is too tight or your capture filter is too loose.
The limitation nobody mentions
ngrep matches per packet. It does not reassemble TCP streams. If the string you are searching for happens to straddle a segment boundary, ngrep will not find it, and you will conclude the traffic is not there when it is.
For short strings in request headers this almost never bites, because headers land at the front of a segment. For anything you expect deep inside a large response body, it absolutely will. That is the case where you want tshark -Y 'http contains "string"', because tshark reassembles the stream first and then searches the reassembled payload.
Two smaller limits round out the picture. ngrep has no dissectors, so it can find bytes but never explain them. And on a busy interface, running a regex against every payload costs real CPU, so a tight BPF filter is not optional.
ngrep against tshark against tcpdump
In a real incident you will often use all three within a minute of each other: tcpdump to grab the traffic, ngrep to confirm a string is present, tshark to work out why the transaction failed.
A note on using this responsibly
ngrep reads other people's traffic in plaintext. On a network you are paid to operate, capturing a request header to prove a proxy is rewriting it is ordinary troubleshooting. On a network you do not own, or against traffic that belongs to users rather than to the service you are debugging, it is not. Capture the narrowest thing that answers your question, and delete the pcap when you are done.
Key takeaways
- Argument order is pattern first, BPF filter second.
-q -W bylineshould be muscle memory for any text protocol. - The pattern is a real regex, so alternation and
-iwork;-vinverts it to find the unexpected packet. - An empty pattern with
-xturns ngrep into a plain hex payload viewer. - TLS hides the payload but not the ClientHello.
ngrep -q 'hostname' 'tcp port 443'reads the SNI in cleartext, which is both a privacy fact and a genuinely useful troubleshooting trick. -Owrites only matching packets to a standard pcap, which is the fastest way to hand a small file to tshark. Usetimeout -s INTor the file will be empty.- ngrep does not reassemble TCP. A string split across two segments is invisible to it; use
tshark -Y 'http contains "..."'for that.
ngrep is the pattern-matching corner of the packet analysis toolkit. The capture side is tcpdump, the dissection side is tshark, and when the question is volume rather than content you want the live bandwidth monitors. The full toolkit is indexed in the complete guide to Linux networking commands.