The Challenge: Proxy with Only Netcat

A developer on DEV Community (source: proxy_pass with netcat) tackled an iximiuz Labs challenge: forward HTTP traffic from port 6000 to a server on port 5000 using nothing but netcat, bash, and Linux primitives. No nginx, no socat.

First Attempt: Simple Pipe Fails

The obvious solution: nc -l -p 6000 | nc 127.0.0.1 5000. This pipes the listening netcat's output into a second netcat that connects to the backend. But the response never returns to the client. The second netcat writes the server's response to stdout (the terminal), not back to the first netcat. Pipes are unidirectional; the return path is missing.

Closing the Loop with a Named Pipe

To send data back, the developer used a FIFO (named pipe):

mkfifo /tmp/nc
nc -l -p 6000 < /tmp/nc | nc 127.0.0.1 5000 > /tmp/nc

The first netcat reads from the FIFO as stdin, and the second netcat writes its stdout to the FIFO. This creates a bidirectional flow: the pipe carries the request forward, the FIFO carries the response back.

The trick works because netcat's core loop reads from stdin and writes to the socket, and reads from the socket and writes to stdout. This is not true for most daemons (nginx, PostgreSQL) which ignore stdin/stdout for client data.

One Successful Transaction, Then Death

With verbose output, the setup completes one request-response cycle:

listening on [any] 6000 ...
localhost [127.0.0.1] 5000 (?) open
connect to [127.0.0.1] from localhost [127.0.0.1] 56200
sent 78, rcvd 1092
sent 1092, rcvd 78

But then the pipeline dies. Even with -k on the first netcat, only one transaction works. Why?

SIGPIPE: The Silent Killer

When the backend server closes the connection (HTTP Connection: close), the second netcat sees EOF, finishes writing to the FIFO, and exits. This closes the read end of the pipe (|). The next time the first netcat tries to write to stdout (the pipe write end), the kernel sends SIGPIPE, killing it. The default SIGPIPE handler terminates the process.

The asymmetry: if the reader of a pipe dies, the writer gets SIGPIPE on next write. If the writer dies, the reader gets EOF (graceful). So -k can't save the first netcat from the broken pipe.

The Infinite Loop Fix

Instead of fighting the single-shot nature of the pipeline, the developer embraced it:

mkfifo /tmp/nc
while true; do
  nc -l -p 6000 < /tmp/nc | nc 127.0.0.1 5000 > /tmp/nc
done

Drop -k. Each iteration of the loop creates a fresh pipeline: new listen, new backend connection. After one transaction, both netcat processes die, the FIFO is reopened, and the loop restarts. There's a tiny window where port 6000 isn't listening, but for a lab exercise it's acceptable.

Key Takeaways

  • Netcat bridges stdin/stdout with a socket bidirectionally, unlike typical daemons.
  • FIFOs solve the topology problem of connecting non-adjacent processes in a pipeline.
  • SIGPIPE kills the writer when the pipe reader exits; ignoring it doesn't help if the reader is gone.
  • An infinite loop is a pragmatic solution for single-shot pipelines.

The full exploration, including wrong turns and debugging, took the developer over two days. It's a deep dive into process plumbing that most developers never think about.

Further Reading

  • man 7 pipe for pipe and FIFO internals
  • man 1 nc for netcat options
  • Viacheslav Biriukov's deep dive into pipe internals with code