socat connects two things and copies bytes between them. That sounds trivial until you see the list of things it will accept as an endpoint: TCP sockets, UDP sockets, UNIX sockets, TLS sessions, serial ports, files, programs, pseudo-terminals, SOCKS proxies. Any endpoint can be joined to any other, which turns a single command into a port forwarder, a TLS terminator, a protocol bridge or a console server.
This guide is the practical subset a network engineer needs. Every capture is real output from a Debian 13 host inside a Cisco Modeling Labs topology, relaying to an nginx server two hops away and a Linux container three hops away over OSPF-routed IOS XE. It is part of the Linux networking commands cluster.
The grammar
Every socat command has exactly the same shape: the program name, options, then two addresses.
socat [options] <address1> <address2>An address is a type in capitals, then colon-separated parameters, then comma-separated options. TCP:10.77.2.10:80 is "connect to that host and port". TCP-LISTEN:8080,fork,reuseaddr is "listen on 8080, fork a child per connection, allow rebinding". A bare - means stdin and stdout. Once that clicks, the rest is vocabulary.
j@llmbits:~$ socat -V | head -4
socat by Gerhard Rieger and contributors - see www.dest-unreach.org
socat version 1.8.0.3 on 31 Mar 2025 20:50:04
running on Linux version #1 SMP PREEMPT_DYNAMIC Debian 6.12.101-1 (2026-08-05), release 6.12.101+deb13-amd64, machine x86_64
features:As a warm-up, here is socat doing exactly what netcat does, joining your terminal to a remote port:
j@llmbits:~$ printf 'HEAD / HTTP/1.0\r\n\r\n' | socat -T3 - TCP:10.77.2.10:80
HTTP/1.1 200 OK
Server: nginx/1.29.8
Date: Wed, 19 Aug 2026 15:11:02 GMT
Content-Type: text/html
Content-Length: 896
Last-Modified: Tue, 07 Apr 2026 11:37:12 GMT
Connection: close
ETag: "69d4ec68-380"
Accept-Ranges: bytes-T3 sets an inactivity timeout, which you want on anything scripted so a half-open connection does not sit there forever.
The port forward
This is the command most people install socat for. Listen locally, connect onward, one line:
j@llmbits:~$ socat TCP-LISTEN:8080,fork,reuseaddr TCP:10.77.2.10:80 &
j@llmbits:~$ ss -lntp | grep 8080
LISTEN 0 5 0.0.0.0:8080 0.0.0.0:* users:(("socat",pid=13287,fd=5))
j@llmbits:~$ curl -sS -I http://127.0.0.1:8080/
HTTP/1.1 200 OK
Server: nginx/1.29.8
Date: Wed, 19 Aug 2026 15:11:48 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: bytesThe cost of the extra hop, measured both ways:
j@llmbits:~$ curl -sS -o /dev/null -w 'via socat: http=%{http_code} time=%{time_total}s\n' http://127.0.0.1:8080/
via socat: http=200 time=0.012207s
j@llmbits:~$ curl -sS -o /dev/null -w 'direct: http=%{http_code} time=%{time_total}s\n' http://10.77.2.10/
direct: http=200 time=0.009982sAbout two milliseconds of userspace relay overhead on this path. That is the honest number for a virtual lab; on real hardware it is smaller, and it matters if you are relaying something latency sensitive.
Two options are non-negotiable on any listener:
forkHandle more than one connection. Without it, socat serves exactly one client and exits, exactly like a bare nc -l.
reuseaddrLets you rebind immediately after killing the previous instance instead of waiting out TIME_WAIT. Saves you the "address already in use" cycle.
bind=IPPins the listener to one interface. Without it you are listening on 0.0.0.0 and anything that can route to the box can use your relay.
That last one deserves emphasis. Bound to a specific lab-facing address, the relay becomes reachable from inside the topology rather than only from localhost:
j@llmbits:~$ socat TCP-LISTEN:8090,bind=10.77.0.100,fork,reuseaddr TCP:10.77.2.10:80 &
j@llmbits:~$ ss -lntp | grep 8090
LISTEN 0 5 10.77.0.100:8090 0.0.0.0:* users:(("socat",pid=13300,fd=5))And from SRV1, three router hops away on the far side of the topology, reaching WEB1 by going through the Debian host:
root@ea5bda805e02:/tmp# curl -sS -I http://10.77.0.100:8090/
HTTP/1.1 200 OK
Server: nginx/1.29.8
Date: Wed, 19 Aug 2026 15:12:06 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: bytesThat is a working application-layer relay between two hosts that had no reason to talk directly. It is enormously useful during a migration, and it is exactly the kind of thing you want to notice on a server you did not deploy yourself.
Adding TLS to something that has none
Plenty of network gear exposes a management interface in plaintext and cannot be persuaded otherwise. OPENSSL-LISTEN puts a TLS front door on it:
j@llmbits:~$ socat OPENSSL-LISTEN:9443,cert=/home/j/lab.pem,verify=0,fork,reuseaddr TCP:10.77.2.10:80 &
j@llmbits:~$ curl -k -sS -I https://127.0.0.1:9443/
HTTP/1.1 200 OK
Server: nginx/1.29.8
Date: Wed, 19 Aug 2026 15:11:03 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: bytesAn HTTPS request went in, a plaintext HTTP request came out the other side, and the client is none the wiser. The certificate file is the concatenation of certificate and private key in one PEM. Checking what the wrapper presents:
j@llmbits:~$ openssl s_client -connect 127.0.0.1:9443 -brief </dev/null
Connecting to 127.0.0.1
depth=0 CN=srv1.pinglabz.lab
verify error:num=18:self-signed certificate
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=srv1.pinglabz.lab
Hash used: SHA256
Signature type: rsa_pss_rsae_sha256
Verification error: self-signed certificate
Peer Temp Key: ECDH, prime256v1, 256 bitsThe reverse direction is just as useful. When a monitoring tool or an old script cannot do TLS, terminate it in socat and hand the tool plain HTTP:
j@llmbits:~$ socat TCP-LISTEN:8081,fork,reuseaddr OPENSSL:10.77.3.10:8443,verify=0 &
j@llmbits:~$ curl -sS http://127.0.0.1:8081/ | head -6
<HTML><BODY BGCOLOR="#ffffff">
<pre>
s_server -accept 8443 -cert s.pem -www
This TLS version forbids renegotiation.
Ciphers supported in s_server binaryverify=0 disables certificate validation, which is fine in a lab and is the same compromise as curl -k. In production use cafile= and leave verification on, or you have built an encrypted tunnel that any machine-in-the-middle can impersonate.
Bridging address families
The reason socat exists rather than another netcat is that the two addresses do not have to be the same kind of thing. A UNIX domain socket on one side, a TCP service on the other:
j@llmbits:~$ socat UNIX-LISTEN:/tmp/web.sock,fork,unlink-early TCP:10.77.2.10:80 &
j@llmbits:~$ ls -l /tmp/web.sock
srwxrwxr-x 1 j j 0 Aug 19 08:10 /tmp/web.sock
j@llmbits:~$ curl -sS --unix-socket /tmp/web.sock -I http://web1/
HTTP/1.1 200 OK
Server: nginx/1.29.8
Date: Wed, 19 Aug 2026 15:11:03 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: bytesThat pattern shows up constantly with Docker, HAProxy admin sockets and anything else that exposes control over a UNIX socket instead of a port. Run it the other way (TCP-LISTEN to UNIX-CONNECT) and you have exposed a local-only control socket to the network, which is a useful trick and a serious risk depending on who asked for it.
UDP works the same way, and relaying UDP into a routed lab is a good test of both directions of a path:
j@llmbits:~$ socat UDP-LISTEN:5300,fork UDP:10.77.3.10:9998 &
j@llmbits:~$ echo 'relayed udp from the VM' | socat -T2 - UDP:127.0.0.1:5300With a listener waiting on SRV1, the proof is on the receiving end, because UDP will never tell you anything useful from the sender:
root@ea5bda805e02:/tmp# cat /tmp/udp.txt
relayed udp from the VMRunning a program per connection
EXEC: and SYSTEM: wire a command's stdin and stdout to the socket. This is the feature OpenBSD netcat removed on purpose, and it is genuinely useful for turning a script into a network service:
j@llmbits:~$ socat TCP-LISTEN:9200,reuseaddr,fork EXEC:'/bin/date -u' &
j@llmbits:~$ nc -w 3 127.0.0.1 9200
Wed Aug 19 03:11:03 PM UTC 2026Understand what you have built. Anything that can reach that port runs that command. Point EXEC: at a shell and you have created a backdoor, which is precisely why this pattern appears in incident reports. Always pair it with bind=127.0.0.1 or an explicit source restriction, and never leave it running after you are done.
Watching the bytes
-v prints everything crossing the relay to stderr, with direction markers and timestamps. It is a protocol analyzer for exactly one connection, with no capture file and no filter syntax:
j@llmbits:~$ printf 'HEAD / HTTP/1.0\r\n\r\n' | socat -T3 -v - TCP:10.77.2.10:80
> 2026/08/19 08:11:06.000951140 length=19 from=0 to=18
HEAD / HTTP/1.0\r
\r
< 2026/08/19 08:11:06.000956911 length=233 from=0 to=232
HTTP/1.1 200 OK\r
Server: nginx/1.29.8\r
Date: Wed, 19 Aug 2026 15:11:06 GMT\r
Content-Type: text/html\r
Content-Length: 896\r
Last-Modified: Tue, 07 Apr 2026 11:37:12 GMT\r
Connection: close\r
ETag: "69d4ec68-380"\r
Accept-Ranges: bytes\r
\r> is client to server, < is server to client, and the \r markers make the CRLF line endings visible, which is worth the whole flag on its own when you are debugging a protocol that cares. Add -x for hex output on binary protocols, and -d -d for socat's own connection diagnostics.
The addresses worth memorizing
TCP:host:portConnect out. TCP4 and TCP6 force a family on a dual-stack host, which settles a lot of "it works for some clients" arguments.
TCP-LISTEN:portAccept in. Always with fork,reuseaddr, usually with bind=.
OPENSSL / OPENSSL-LISTENTLS client and server. cert=, key=, cafile=, verify=. Terminating or originating TLS is the same command with the addresses swapped.
UDP / UDP-LISTENDatagram relay. Useful for syslog, NetFlow, SNMP traps and anything else that needs to be redirected during a collector migration.
UNIX-LISTEN / UNIX-CONNECTFilesystem sockets. The bridge between "this only listens on a socket file" and "I need to reach it over the network".
EXEC / SYSTEMRun a program per connection. Powerful, and the single easiest way to accidentally publish a shell.
/dev/ttyUSB0,raw,b9600A serial port as an address. Joined to TCP-LISTEN this turns any Linux box with a USB console cable into a terminal server.
FAQ
socat or netcat?
Netcat for a quick port check or a one-off pipe, because the command is shorter and it is installed everywhere. socat the moment you need it to survive more than one connection, cross address families, speak TLS, or run unattended.
Why does my socat exit after the first connection?
You left off fork. A TCP-LISTEN without it accepts once, relays, and exits when that connection closes. It is the same default as netcat and it catches everybody once.
Should I use socat or an SSH tunnel for port forwarding?
An SSH tunnel whenever the traffic crosses a network you do not fully control, because it is authenticated and encrypted. socat when both ends are inside a trusted segment and you want the relay to be a service rather than tied to a login session, or when you need an address type SSH cannot express.
How do I keep a socat relay running?
A systemd unit. Type=simple, the socat command as ExecStart, Restart=always. Backgrounding it from a shell means it dies with your session, and a relay that vanishes at the wrong moment is worse than no relay.
Does the far end see the original client IP?
No. Every relayed connection appears to come from the socat host, which is why the ACL on your server suddenly stops matching. If the backend needs the real client address, you need a proxy that speaks the PROXY protocol or a layer 3 approach, not a userspace byte relay.
What does it cost in throughput?
Every byte crosses userspace twice, so expect a measurable latency addition (about 2 ms on the lab path above) and a throughput ceiling well below what the kernel would forward. Fine for management traffic and API calls, wrong for bulk data. Measure it with iperf3 before you rely on it.
Key takeaways
- Every command is
socat [options] address1 address2. Learn the address grammar once and the rest is vocabulary. forkandreuseaddrbelong on every listener. Withoutforkyou get exactly one connection.bind=decides who can use your relay. A listener on 0.0.0.0 is reachable by anything that can route to the host.OPENSSL-LISTENadds TLS to a plaintext service andOPENSSLstrips it for tools that cannot. Usecafile=and dropverify=0outside a lab.- UNIX socket to TCP is the bridge that makes Docker, HAProxy and similar control sockets reachable, in both the useful and the dangerous sense.
EXEC:runs a program per connection. Treat every use as a potential backdoor and bind it to localhost.-vis a per-connection protocol trace with visible CRLF markers, and-xadds hex for binary protocols.- The backend sees the relay's address, not the client's. Plan your ACLs accordingly.
Next in this cluster: SSH beyond login, which does much of this with authentication and encryption built in. Everything above builds on the port checks in netcat and the HTTP testing in curl. The full toolset is indexed on the Linux networking commands pillar.