> ## Content Index
> Fetch the complete content index at: https://www.pinglabz.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# socat: Port Forwarding and Bidirectional Relays
- URL: https://www.pinglabz.com/socat-linux/
- Published: 2026-08-19T15:39:02.000Z
- Updated: 2026-08-19T15:39:02.000Z
- Description: One command joins any two endpoints: TCP, UDP, TLS, UNIX sockets, serial ports or a program. Port forwards, TLS wrappers and protocol bridges from real captures.
- Author: Jaime
- Tags: Linux, Tools, Troubleshooting, Labs

`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](https://www.pinglabz.com/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](https://www.pinglabz.com/netcat-nc-linux/) 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: bytes
```

The 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.009982s
```

About 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:

**`fork`**Handle more than one connection. Without it, `socat` serves exactly one client and exits, exactly like a bare `nc -l`. 

**`reuseaddr`**Lets you rebind immediately after killing the previous instance instead of waiting out TIME\_WAIT. Saves you the "address already in use" cycle. 

**`bind=IP`**Pins 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: bytes
```

That 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: bytes
```

An 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 bits
```

The 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 binary
```

`verify=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: bytes
```

That 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:5300
```

With 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 VM
```

## Running 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 2026
```

Understand 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:port`**Connect 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:port`**Accept in. Always with `fork,reuseaddr`, usually with `bind=`. 

**`OPENSSL` / `OPENSSL-LISTEN`**TLS client and server. `cert=`, `key=`, `cafile=`, `verify=`. Terminating or originating TLS is the same command with the addresses swapped. 

**`UDP` / `UDP-LISTEN`**Datagram relay. Useful for syslog, NetFlow, SNMP traps and anything else that needs to be redirected during a collector migration. 

**`UNIX-LISTEN` / `UNIX-CONNECT`**Filesystem sockets. The bridge between "this only listens on a socket file" and "I need to reach it over the network". 

**`EXEC` / `SYSTEM`**Run a program per connection. Powerful, and the single easiest way to accidentally publish a shell. 

**`/dev/ttyUSB0,raw,b9600`**A 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](https://www.pinglabz.com/netcat-nc-linux/) 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](https://www.pinglabz.com/ssh-tunneling-scp/) 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](https://www.pinglabz.com/iperf/) before you rely on it.

## Key takeaways

- Every command is `socat [options] address1 address2`. Learn the address grammar once and the rest is vocabulary.
- `fork` and `reuseaddr` belong on every listener. Without `fork` you 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-LISTEN` adds TLS to a plaintext service and `OPENSSL` strips it for tools that cannot. Use `cafile=` and drop `verify=0` outside 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.
- `-v` is a per-connection protocol trace with visible CRLF markers, and `-x` adds 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](https://www.pinglabz.com/ssh-tunneling-scp/), which does much of this with authentication and encryption built in. Everything above builds on the port checks in [netcat](https://www.pinglabz.com/netcat-nc-linux/) and the HTTP testing in [curl](https://www.pinglabz.com/curl-network-engineers/). The full toolset is indexed on the [Linux networking commands](https://www.pinglabz.com/linux-networking-commands/) pillar.