14 complaints about 11 implementations of netcat

2024/12/12

netcat is that program that hooks up its stdin and stdout to a socket. It does other things too, but this article is about just the basics: sending and receiving streams of data. And I came into this thinking the OpenBSD one is pretty good. But no, it’s weird. I found 11 implementations of the darned thing, and they’re all weird.

That explains why my impression of netcat has been 10% “Hehe I can type ‘hi’ here and it shows up there,” and 90% “Is it done? Is it frozen? How come Ctrl+D isn’t doing anything? Do I need Ctrl+C instead? Shoot, do I need to push Ctrl+D on the other side? I’m not at that keyboard! Why is this file only a few KiB now? WHAT DO YOU MEAN BROKEN PIPE?”

Experiment description

Let’s formalize the weirdness with experimental data.

We test whether each implementation can deliver data and an end-of-file (EOF) condition in each combination of three variables:

  1. with netcat acting as a client or server
  2. having one of the streams being quiet with no data flowing, filled with data transfer stopped by backpressure, or done with an EOF sent
  3. delivering in either direction, in from the socket to stdout, or out from stdin to the socket

In each combination, we attempt to send data (e.g. write to nc’s stdin pipe for the “out” direction) (the program name is shortened to nc) and receive it (e.g. recv matching data from peer socket) up to 10 times, and if those succeed, then we attempt to send an end-of-file condition (e.g. close nc’s stdin pipe) and receive it (e.g. recv zero bytes from peer socket).

These experiments were done on Debian’s testing release, recent-ish-ly. I’ll put the package versions.

The results will be in a table like this.

Client
Out quiet
In
Implementation Version Settings Data EOF
netcat-traditional 1.10-48.2 defaults pass pass

This means:

And we found that:

Results

Client Server
Out quiet Out filled Out done In quiet In filled In done Out quiet Out filled Out done In quiet In filled In done
In In In Out Out Out In In In Out Out Out
Implementation Version Settings Data EOF Data EOF Data EOF Data EOF Data EOF Data EOF Data EOF Data EOF Data EOF Data EOF Data EOF Data EOF
netcat-traditional 1.10-48.2 defaults pass pass (A) n/a pass pass pass (B) (A) n/a (C) n/a pass pass (A) n/a pass pass pass (B) (A) n/a (C) n/a
-q 0 pass pass (A) n/a (D) n/a pass pass (A) n/a (C) n/a pass pass (A) n/a (D) n/a pass pass (A) n/a (C) n/a
-q 999999999 pass pass (A) n/a pass pass pass pass (A) n/a (C) n/a pass pass (A) n/a pass pass pass pass (A) n/a (C) n/a
netcat-openbsd 1.226-1.1 defaults pass (E) pass (E) pass pass pass (B) (F) n/a pass pass pass pass pass (E) pass pass pass (B) (F) n/a (G) n/a
-N pass (E) pass (E) pass pass pass pass (F) n/a pass pass pass pass pass (E) pass pass pass pass (F) n/a (G) n/a
netcat6 1.0-8 defaults pass pass pass pass pass pass pass (B) (A) n/a (C) n/a pass pass pass pass pass pass pass (B) (A) n/a (C) n/a
--half-close pass (H) pass (H) pass pass pass pass (A) n/a pass pass pass (H) pass (H) pass pass pass pass (A) n/a pass pass
netcat-giacobbi 0.7.1 defaults pass pass pass pass pass pass pass (B) (I) n/a (C) n/a pass pass (J) n/a pass pass pass (B) (I) n/a (C) n/a
socat 1.8.0.1-2 defaults pass pass pass pass (K) n/a pass pass (L) n/a (K) n/a pass pass pass pass (K) n/a pass pass (L) n/a (K) n/a
-t 999999999 pass (E) pass (E) pass pass pass pass (L) n/a pass pass pass (E) pass (E) pass pass pass pass (L) n/a pass pass
ncat 7.95+dfsg-1 defaults pass pass pass pass pass pass pass pass (I) n/a pass pass pass pass (M) n/a pass pass pass pass (I) n/a (G) n/a
tcputils 0.6.2-10+b1 defaults pass (E) (A) n/a pass pass pass pass (A) n/a pass pass pass (E) (A) n/a pass pass pass pass (A) n/a pass pass
netpipes 4.2-8+b1 --slave pass (E) pass (E) pass pass pass pass pass pass pass pass
cat contraption pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass
busybox 1:1.37.0-4 defaults pass pass (A) n/a pass pass pass pass (A) n/a (C) n/a pass pass (A) n/a pass pass pass pass (A) n/a (C) n/a
toybox 0.8.9+dfsg-1.1 defaults pass pass (A) n/a pass pass pass pass (A) n/a (C) n/a pass pass (A) n/a pass pass pass pass (A) n/a (C) n/a
u-root 0.14.0 defaults pass pass pass pass pass pass pass (B) pass (B) (C) n/a pass pass pass pass pass pass pass (B) pass (B) (C) n/a
bash 5.2.32-1+b2 custom program pass pass pass pass pass pass pass (N) pass (N) pass pass
python3 3.12.6-1 custom program pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass
perl 5.40.0-7 custom program pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass

Pass Pass

N/A N/A

(A) Only selects on reads, does blocking writes. Thus, hangs without delivering data to stdout or peer socket

(B) Doesn’t shut down socket write side. Thus, hangs without delivering end-of-file to peer socket

(C) Exits when the read side of the socket reaches end-of-file. Thus, writing data to stdin fails with broken pipe

(D) Exits when stdin reaches end-of-file. Thus, reading data from stdout gives end-of-file

(E) Doesn’t close stdout. Thus, hangs without delivering end-of-file to stdout

(F) Has logic to poll when stdout is writable but doesn’t set stdout nonblocking, so it still blocks when trying to write to stdout. Thus, hangs without delivering data to peer socket

(G) Exits when the read side of the socket reaches end-of-file only when it is the server. Thus, writing data to stdin fails with broken pipe

(H) Closes destination file descriptor, but that is a dup of stdout, so stdout remains open. Thus, hangs without delivering end-of-file to stdout

(I) Doesn’t select on stdout being writable, does blocking write. Thus, hangs without delivering data to peer socket

(J) Has logic to select on socket being writable if writing fails with EAGAIN, but doesn’t set socket nonblocking when it is the server, so it still blocks when trying to write to the socket. Thus, hangs without delivering data to stdout

(K) Exits a short time after stdin or the read side of the socket reaches end-of-file. Thus, reading data from stdout gives end-of-file or writing data to stdin fails with broken pipe

(L) Polls stdout being writable, but can’t do a partial write, so it does a blocking write. Thus, hangs without delivering data to peer socket

(M) When it is the server, it does blocking writes to the socket, I assume for code sharing with a broadcast mode when it broadcasts to multiple clients. Thus, hangs without delivering data to stdout

(N) Has no way to shut down socket write side. Thus, hangs without delivering end-of-file to peer socket

Misc programming stuff

Let’s say you’re writing a single-threaded select (or poll or whatever) loop style of implementation, which most of the implementations in these experiments are. And suppose you want to pass “out” direction tests (i.e., deliver data from stdin to socket) with the “in” side filled (i.e., receiving data from socket faster than your stdout is being read). So you select on stdout being writable, and you want your writes to stdout not to block.

Folk wisdom would tell you that a pipe or socket selected as writable means you can write at least one byte to it without blocking. Meanwhile, POSIX tells you that writing a length known as PIPE_BUF or shorter into a pipe (or FIFO) causes some special pipe-specific behavior:

  1. If the pipe has room for the entire write, the entire write goes through.
  2. If the pipe doesn’t have enough room, it blocks.
  3. … Unless you have O_NONBLOCK set, in which case it writes nothing and fails with EAGAIN.
  4. And by the way, if you had given it more than PIPE_BUF bytes with O_NONBLOCK set, it can write some of those bytes and return the number of bytes it wrote.

You don’t want this to block (2), so suppose you set O_NONBLOCK on your stdout.

All this on paper threatens to put you into a livelock where write could keep telling you to try again (3) while select keeps telling you that you can write. Successfully writing some bytes (4) would ensure forward progress, but you might not have collected more than PIPE_BUF bytes from the socket.

I looked into this situation on Linux, and the implementation would not cause this livelock. Here’s the pipe implementation if you want to follow along.

In Linux (as of writing), pipes have a sequence of pages. Pages are… let’s say nominally 4 KiB, although I do see effort going towards 16 KiB page support. Anyway, there’s a maximum number of pages, which adds up to the well documented 65,536 byte pipe capacity. Linux treats a pipe as writable when it’s not using the maximum number of pages. So when select says you can write, you can write at least 4 KiB. (It might still take a while if the kernel needs to get that page for you, but that’s different from waiting for the other side of the pipe to read some stuff.) And the PIPE_BUF constant is also equal to the page size in Linux, so it’s okay to do a small write after selecting.

Some netcat implementations get away with not setting O_NONBLOCK on stdout, just by selecting and doing small writes 4 KiB or shorter.

By the way, what about sockets? There’s no special behavior for making small writes atomic when it comes to sockets. If you’re writing a netcat implementation, you can set O_NONBLOCK on the socket and you’ll be able to make some nonzero writing progress as long as select thinks the socket is writable. So the amount of space available that select waits for is not as important for us.

Let’s look anyway. Here’s the socket implementation. Linux treats a stream socket as writable when at least half of its send buffer is free.

Similarly, some netcat implementations get away with not setting O_NONBLOCK on the socket, just by selecting and doing small-ish writes. Actually they don’t have to be that small. On the test machine, the maximum send buffer size was 4 MiB, and all the implementations only wrote at most a few KiB at a time.

Implementation background

Hobbit’s netcat (not tested in these experiments), I find that sources agree, is the original program to give this functionality the name “netcat.” So if only one program could be called “netcat” with no additional specifier, I’d say it ought to be this one. In modern times, netcat 1.10 and nc110 refer to this, with that version being the final one released. Wikipedia’s entry on netcat links to an updated “community-edition” (also not tested in these experiments). Some software distributions point to Bill Stearns’s software archive as a homepage for this program.

Telnet (not tested in these experiments), which Hobbit compared netcat to in netcat’s release announcement, is a protocol for connecting to a terminal on a remote machine. Think of it as a precursor of SSH. The protocol was simple enough that the client program could be used as a “connect to this host and port and let me type something and see the reply” tool. The Telnet client became popular for uses other than actually using Telnet. So today if you search for an example of using Telnet, you’ll instead see variously how to use it with HTTP servers, SMTP servers, etc.

I actually briefly tried including Telnet (GNU Inetutils’s implementation) in these experiments, but it was wildly incompatible with my testing framework, indeed in several ways that Hobbit pointed out in netcat’s release announcement, for example with incorrect data read due to status messages being written to stdout.

netcat-traditional (1.10-48.2) is Debian’s distribution of Hobbit’s netcat. Debian’s distribution adds some features, notably including the -q option to make it shut down the write side of the socket after an EOF on stdin and then exit.

OpenBSD’s nc (not tested in these experiments) is a reimplementation of netcat. The manual lists Hobbit in the Authors section, but I’m under the impression that it’s only an acknowledgment and it has an original codebase.

netcat-openbsd (1.226-1.1) is Debian’s distribution of OpenBSD’s nc. Debian’s distribution has some patches, although I don’t see any that would affect these experiments.

LibreSSL Portable (not tested in these experiments) is a fork of OpenSSL. It bundles an adaptation of OpenBSD’s nc.

(In case you didn’t know off the top of your head: Debian started distributing netcat-openbsd in 2008; OpenBSD forked OpenSSL into LibreSSL in 2014; in 2015, OpenBSD added TLS support to its nc and added nc to the LibreSSL Portable release “as an example of how to use the library;” later, in 2017, the new upstream version of OpenBSD’s nc made it into Debian, but Debian distributed with a patch to remove the TLS support.)

pkgs.netcat in Nixpkgs is LibreSSL Portable’s version.

netcat6 (1.0-8) is a reimplementation of netcat.

Debian distributed netcat6 in several releases, but they’ve since stopped. Conveniently, you can get the final .deb release in Debian “jessie” from their snapshot archive.

Giacobbi’s Netcat (0.7.1) is something… It’s titled “The GNU Netcat,” but I don’t see it recognized in GNU’s list of their own packages. I think it’s only named after its chosen license and coding style, rather than by any affiliation with the GNU Project. I find that potentially confusing, so I’ll refer to it by its author’s name. The AUTHORS file refers to it as a “branch” of Hobbit’s netcat, while the .c and .h files do not list any authors other than Giacobbi. The code involved in these experiments has substantially diverged, if it is related at all.

In GNU’s Guix operating system, the “netcat” package is Giacobbi’s implementation. Debian doesn’t distribute this one, so I built it from source.

socat (1.8.0.1-2) is a program with much larger scope of functionality, with its command line arguments being pretty much a networking focused domain-specific language. You can use that language to program it into working like netcat—there’s an example in the manual, thank goodness. It’s like this:

socat - TCP:$hostname:$port

ncat (7.95+dfsg-1) is a tool from the Nmap Project. The manual is clear that it’s “based on [netcat] in spirit and functionality.”

ncat is built on nsock, which is an event-loop style library with lots of callbacks.

tcputils (0.6.2-10+b1) is a suite of tools including tcpconnect and tcplisten that do the things we’re looking for, with a different naming philosophy.

netpipes (4.2-8+b1) is a suite of tools in a different style. Instead of you running a tool and your program communicating with that tool’s stdin/stdout, the tool runs your program with its stdin, stdout, and/or other file descriptors redirected to the socket.

The client tool hose from the netpipes suite has an option to transfer data over stdin/stdout instead of running a program, so I’m including it in these experiments. The server tool faucet does not have this option though.

Another way to use it with stdin and stdout pipes is to have the tool pass the socket as a file descriptor other than stdin/stdout and then have the program that you run copy between the socket and stdin/stdout. I tested this with a program that uses two cat programs to do this copying, plus the sockdown utility from this same suite to shut down the write side of the socket when we are done writing to it. I refer to this setup as the “cat contraption.” It’s like this:

hose $hostname $port
  --fd 3
  sh -c '
    cat <&3 &
    exec >- &&
    cat >&3 &&
    sockdown 3 &&
    wait
 '

Oh, I almost forgot to mention, the server tool faucet doesn’t let you choose what interface to listen on. So if anyone’s keeping score, give netpipes a slight penalty.

BusyBox (1:1.37.0-4) is a much broader suite of little tools, implementing common command line tools. That includes having its own reimplementation of netcat.

toybox (0.8.9+dfsg-1.1) is a suite of tools like BusyBox. It also has its own reimplementation of netcat.

u-root (v0.14.0) is an embedded software suite including a set of tools spiritually adjacent to BusyBox (with a certain build tool in the project named “gobusybox,” in fact). Deep within that project, a reimplementation of netcat is among those tools.

Debian doesn’t distribute u-root, so I built it from source.

u-root uses two goroutines to handle the two directions of data transfer.

bash (5.2.32-1+b2) is, as you know, not at all netcat. It’s a shell. It’s a scripting language. Yet it has this >/dev/tcp/... feature which makes it able to connect as a client to a given address and port. So for that, it’s in these experiments. There’s no feature to listen for a connection though.

Basically these last few entries are just me writing my own reference implementations in various languages. My implementation in bash uses two cat programs to handle the two directions of data transfer.

python3 (3.12.6-1) is a programming language.

My implementation in python3 uses two threads to handle the two directions of data transfer.

perl (5.40.0-7) is another programming language.

My implementation in perl uses a select loop.

What actually is correct

I’m in favor of what these tests test for. But I’ll bring up some other ideas.

Backpressure stopping both directions. If the pipeline is acyclic, then it shouldn’t matter if netcat can’t deliver from stdin to the socket when stdout is backed up. The stuff downstream from netcat should still be able to make progress.

The other way around is important though. Imagine a decompression service that you’re feeding faster than it can decompress. Then it hits a high-compression-ratio spot and needs to give you a bunch of data back before looking at the next piece of compressed data. If your netcat is stuck trying to write more compressed data to the socket, unable to read the data it’s receiving, then the pipeline would deadlock.

Half-closed sockets. Are half-closed sockets too much of a foot gun for people? If you’re just running netcat on its own in a terminal, you can’t see an end-of-file condition from the peer. It would just be you mashing on the keyboard without knowing what was going on—and with those mashed keystrokes still being sent to the peer.

netcat serves as its own definition of correct behavior. For example, OpenBSD’s nc would shut down the write side of the socket by default unil 2013; then, they changed it not to, introducing a -N flag to have it do so. That’s what makes the downstream netcat-openbsd in default settings fail (B) our tests for delivering end-of-file to the peer socket. But it brings it closer to doing what Hobbit’s netcat does.

Experiment code

netcat test framework

Addendum: Telnet after all

You know what, with a little more workarounds, I got Telnet working with the test framework.

Client
Out quiet Out filled Out done In quiet In filled In done
In In In Out Out Out
Implementation Version Settings Data EOF Data EOF Data EOF Data EOF Data EOF Data EOF
inetutils-telnet 2:2.5-5 defaults (O) pass (O) pass (D) N/A (O) pass (O) (P) (C) N/A

(O) Writes status messages to stdout, so appears to deliver wrong data to stdout. Also not binary safe. However, skipping status messages and testing with ASCII content passes

(P) Tries to exit, but before exiting tries to change stdout cooked mode assuming it is a terminal, but before changing terminal mode tries to write internally buffered data, but gets stuck in EAGAIN livelock. Thus, spins without delivering end-of-file to peer socket

inetutils-telnet (2:2.5-5) is the current Telnet implementation distributed in Debian. It’s maintained as part of the GNU Inetutils suite, and its codebase is derived from BSD.

My last post was about either macOS user IDs or Microplastics analysis of four different ways of tearing a sauce packet. Find out which.