networking · difficulty ◆◆
nc — test ports, grab banners, move bytes
No protocol, no config, no daemon — nc just wires stdin and stdout to a socket, which is exactly why it answers questions curl cannot.
A tool that has no idea what HTTP is will still tell you more about a broken service than curl does, because it prints the bytes the daemon actually sent.
$ ncWhat nc is, in one sentence
netcat is a socket glued to your terminal. Whatever you write to stdin goes out of the TCP connection, whatever the peer sends comes back on stdout, and the manual page on this box puts it as plainly as anyone could: 'The nc (or netcat) utility is used for just about anything under the sun involving TCP, UDP, or UNIX-domain sockets.' There is no protocol layer, no TLS, no request model, no config file. That sounds like a limitation until you are staring at a service that returns 502 through your proxy and you need to know what it says when asked directly. nc has three modes: connect (default, `nc host port`), listen (`-l`) and scan (`-z`, zero I/O — knock without sending a byte). It also does the boring thing right: error messages go to stderr, data to stdout, so it pipelines like a well-behaved Unix tool instead of smearing both onto one stream the way telnet used to.
The three jobs it does better than anything else
First, port reachability. telnet is gone from Ubuntu (and from most hardened images), ssl-client is not installed, and curl will happily report a port is open after hanging for two minutes. `nc -zvw3 host port` answers in one word with no packages. Second, talking to servers by hand: banner grabs, SMTP handshakes, and raw HTTP when you need the response before any client rewrites it. This is the mode that finds the real culprit when nginx is fine but a backend answers 404 to /api/health — you see the headers, including which cache layer served you. Third, moving bytes: a file transfer between two machines with no ssh, no keys, no daemon, plus a one-shot listener for catching a webhook or a health check on a box where you cannot install anything. Port ranges work too (`nc -zvw1 host 6332-6336`), and that is often the whole map you needed.
Which nc do you actually have
This is the part that breaks copy-pasted recipes. Debian and Ubuntu ship netcat-openbsd (here: 1.226-1ubuntu2, pulled from OpenBSD 7.4 — `readlink -f $(which nc)` says /usr/bin/nc.openbsd), while netcat-traditional is hobbit's 1996 codebase and busybox carries a third, smaller one. They disagree on the details that matter. In the OpenBSD build the port is a plain argument (`nc -l 8080`), `-p` means source port; in the traditional build `nc -l -p 8080` is the idiom. Connection-refused lines from a scan only appear when `-v` is given. `-e` and `-c` do not exist at all — the usage line here reads `nc [-46CDdFhklNnrStUuvZz]`, no `e` in sight. And the good stuff is OpenBSD-only: `-k` (keep listening), `-q` (quit after EOF), `-N` (shutdown on stdin EOF), `-z` (zero-I/O scan), `-U` (Unix sockets). Learn which one is installed before you trust a one-liner from a blog.
Example
$ nc -zvw3 192.168.1.13 8123
nc -zvw3 127.0.0.1 45999Connection to 192.168.1.13 8123 port [tcp/*] succeeded!
nc: connect to 127.0.0.1 port 45999 (tcp) failed: Connection refusedReal output, both lines, from this box: Home Assistant answers on 8123, nothing answers on 45999. The three flags do one job each — `-z` probes without sending data, `-v` prints the result line at all, `-w3` gives up after three seconds. Exit status is worth knowing: the successful probe returned 0, the refused one returned 1, and a multi-port sweep returns 0 as long as at least one port connected. That is the check to put in front of a deploy script or a monitoring probe: one line, installed everywhere, and no dependency on whether someone left telnet in the image.
$ nc -zvw1 127.0.0.1 6332-6336nc: connect to 127.0.0.1 port 6332 (tcp) failed: Connection refused
Connection to 127.0.0.1 6333 port [tcp/*] succeeded!
Connection to 127.0.0.1 6334 port [tcp/*] succeeded!
nc: connect to 127.0.0.1 port 6335 (tcp) failed: Connection refused
nc: connect to 127.0.0.1 port 6336 (tcp) failed: Connection refusedPort ranges are a documented but little-known feature — the man page says the port may be given as `nn-mm` — and they map an application's port block in one command: here the qdrant container's two published ports (6333 REST, 6334 gRPC) stand out of five, and the refused lines interleave with the successes as each probe finishes. `-w1` matters because without `-w` the default is no timeout, and one filtered port will make your scan sit there until you kill it. This is not nmap: no SYN scan, no service detection, no concurrency. It is the five-second question 'what is listening in this range', and it needs no install.
$ timeout 4 nc -w3 127.0.0.1 22 < /dev/nullSSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.19A banner grab, verbatim from this machine: connect and wait, and sshd introduces itself before any client has said a word. This is how you find out what is really behind a port when a proxy, a port-forward or a container mapping is in the way — the banner names the software and the exact build. The `< /dev/null` keeps nc from sitting on your keyboard, `-w3` bounds the connect, and `timeout 4` catches the case where the peer accepts the connection and then says nothing forever. Point the same command at 25, 587 or 3306 and you get the SMTP or MySQL greeting instead; a service that greets you in the wrong protocol is the fastest diagnosis you can get.
$ printf 'HEAD / HTTP/1.0\r\nHost: kmail.at\r\n\r\n' | nc -w3 127.0.0.1 3401HTTP/1.1 200 OK
Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
x-nextjs-cache: HIT
x-nextjs-prerender: 1
x-nextjs-prerender: 1
x-nextjs-stale-time: 300
X-Powered-By: Next.js
Cache-Control: s-maxage=31536000
ETag: "12532oag3rp1sox"
Content-Type: text/html; charset=utf-8
Content-Length: 84045
Date: Sun, 11 Oct 2026 07:01:15 GMT
Connection: closeReal headers from the kmail-next container behind this site, fetched with a printf pipe and no client library. Note what only this view gives you: `x-nextjs-cache: HIT` says the page came from the cache, `x-nextjs-stale-time: 300` says how stale it was allowed to be, and `x-nextjs-prerender: 1` is printed twice — that doubling is in the actual response, which is exactly the kind of detail a client hides from you. HEAD gets headers without the body; swap it for GET and pipe into `head -40` when the body is what you want. The blank line before the end is the end of the response, and because this is HTTP/1.0 the server closes the socket rather than keeping it open.
$ # terminal 1 — the receiver
nc -l 127.0.0.1 46001 > received.bin
# terminal 2 — the sender, then the verification
nc -v -w3 -N 127.0.0.1 46001 < payload.bin
ls -l payload.bin received.bin && cmp payload.bin received.bin && echo IDENTICAL-rw-rw-r-- 1 kmail kmail 300000 Oct 11 09:01 payload.bin
Connection to 127.0.0.1 46001 port [tcp/*] succeeded!
-rw-rw-r-- 1 kmail kmail 300000 Oct 11 09:01 received.bin
IDENTICALRan it end to end: 300000 random bytes, both files exactly 300000 bytes, `cmp` silent and IDENTICAL. No ssh, no keys, no sshd on the far end, no rsync binary, no config — a listener on one machine and a redirection on the other. `-N` is the flag that makes this work in scripts: it shuts down the network socket when stdin hits EOF, so the receiver knows the file is finished. Without it the client keeps the connection open after the last byte and your transfer sits there until something times out. Plaintext on the wire, so treat it as a LAN or tunnel-only tool, and remember the listening socket is open to whoever reaches that port first.
$ # terminal 1 — one listener, many connections
nc -k -l 127.0.0.1 46010
# terminal 2 — two separate sends
printf 'ping-1\n' | nc -w2 -N 127.0.0.1 46010
printf 'ping-2\n' | nc -w2 -N 127.0.0.1 46010ping-1
ping-2The listener's terminal shows both arrivals; both client commands exited 0. `-k` requires `-l` and is the difference between a single-shot listener and a small server. I measured the other case on this box: without `-k` the first send still succeeded, the listener process was gone afterwards, and the second send exited 1 with nothing printed — refusals only reach the screen when `-v` is on. So the habit for scripts is `timeout 30 nc -k -l -v 8080` while you are debugging, and never expect nc to restart itself. The same flag pair is how people catch webhooks, and `-F` is how you hand the connected socket off to another program.
Common flags
- -z
- Zero-I/O mode: scan for listening daemons without sending them a single byte. The line you want appears together with -v. Cannot be combined with -l, and it is the only way to test a port when you do not know — or do not want to speak — the protocol.
- -v
- Verbose. On this build it is not optional decoration: refusals and failures are printed to stderr only when -v is given (README.Debian lists that as a deliberate difference from netcat-traditional). Run scans and transfers with -v and read stderr.
- -w seconds
- Timeout for connections that cannot be established and for idle ones. The default is no timeout, and it is ignored in listen mode — `nc -l` waits forever with or without -w, which is why listeners belong inside `timeout 30`.
- -l
- Listen instead of connecting; the port is a plain argument (`nc -l 8080`) or comes from -p as the source port. No -x, no -z, and -w is ignored. One connection and it exits, unless -k is added.
- -k
- Keep the listener open for further connections after one completes (requires -l). Measured above: two clients in a row, both exit 0. The listener still dies with the process — -k is not a respawn.
- -N
- shutdown(2) the socket after EOF on stdin. This is what makes file transfers terminate cleanly, and the reason a naive `nc host port < file` can hang after the last byte. -q n implies -N, and -q 0 means 'EOF now, quit immediately'.
- -u
- Use UDP instead of TCP. There is no handshake to observe, so -z results are unreliable over UDP — a 'succeeded' line mostly means nothing replied with an ICMP port-unreachable, not that a daemon is listening. Use it for real datagram tests (syslog, DNS-ish probes), not for scans.
History
1996 — code signed by a pseudonym, a manual copyrighted by someone else
The manual page installed on this box carries one line of provenance in its comment header: 'Copyright (c) 1996 David Sacerdote'. Its AUTHORS section credits the tool to *Hobbit* <hobbit@avian.org> alone. Debian's own one-line description of the package is four words — 'TCP/IP swiss army knife' — which is the shortest summary anyone has managed since. The example scripts from that original release are still installed, and they are a time capsule: `alta` drives Altavista by requesting result pages ten at a time from www.altavista.digital.com (a host that stopped existing in 2013), `bsh` launches a password-protected shell listener, and their README opens with 'I'll be the first to admit that some of these are seriously *sick*, but they do work and are quite useful to me on a daily basis.'
2001 and 2008 — the rewrite Debian ships, and the two options it refuses to implement
The code you ran above is not hobbit's. netcat.c carries 'Copyright: 2001 Eric Jackson <ericj@monkey.org>' — the rewrite that added IPv6 — and socks.c a 1999 copyright from Niklas Hallqvist, which is where `-x`/`-X` proxy support came from. Debian split the modern implementation into netcat-openbsd in 2008 (Soren Hansen, later Decklin Foster) rather than replacing the old package under users' feet, and then deliberately left features out. README.Debian states it without hedging: '-e and -c ... enable anyone on the system to open port and execute arbitrary command on local host from remote very easily, which is not desired for ordinary multi-user systems.' The manual page even prints the workaround — a `mkfifo /tmp/f` plus `/bin/sh -i` pipeline — and labels it DANGEROUS in capitals. The version here, 1.226, was synced from OpenBSD 7.4 in October 2023.
Fun facts
Pros & cons
pros
- + Installed by default nearly everywhere — Debian, Ubuntu, RHEL, the BSDs, macOS, busybox and most container images — and needs no privileges to test a port or read a banner
- + Protocol-agnostic by design: you see the bytes the service actually returns, headers and all, which is the view that diagnoses proxies, cache layers and wrong-protocol bindings that friendly clients hide
- + One binary for reachability checks, banner grabs, hand-written HTTP/SMTP, ad-hoc transfers and a one-shot listener, with exit codes clean enough to use directly in scripts
cons
- − Every implementation behaves differently — netcat-openbsd, netcat-traditional, busybox nc and ncat disagree about -e, -l -p, port ranges and even whether refusals are printed — so a recipe that works on one host can silently do the wrong thing on the next
- − No TLS, no authentication, no checksums, no resume: anything you send is plaintext, and a dropped connection means starting the transfer over, which rules it out for anything sensitive or large
- − As a listener it is a footgun — single-shot, no access control, holds a port until killed — and it is the tool attackers reach for first, which is why -e and -c were written out of the Debian build for good
Takeaways
- 1Make `nc -zvw3 host port` your first move whenever a service looks unreachable — one line, no packages, and it distinguishes listening from refused instead of hanging the way a client library does
- 2Always pass -w in scan mode and wrap listeners in timeout: the default is no timeout, and listen mode ignores -w entirely, so a forgotten `nc -l` keeps the port until the process is killed
- 3Use -N (or -q 0) on the sending side of any transfer or scripted request — it closes the socket at EOF, which is what stops the classic 'the file transferred but the command never returned' hang
- 4Check which nc you have before copying a one-liner: `readlink -f $(which nc)` and `nc -h` — OpenBSD takes `-l 8080`, netcat-traditional wants `-l -p 8080`, and only some builds scan a range or keep a listener alive with -k
- 5Do not look for -e: this build deliberately has no remote-command flag, and that absence is why `nc -zvw3` is safe to run on a shared box while the mkfifo tricks printed in the manual are labelled DANGEROUS