README
¶
0. llmao-af_moshpit
An inter-address-family stream proxy for Linux.
Impatient? Peek at this invocation example (alternative anchor).
It creates circuits where the client side and the server side may be of different socket address families (not particularly novel) and supports "ephemeral operation" where the proxy only runs for as long as some subcommand runs (a more distinctive feature).
Supported streams are:
| Specifier | Address family | Description |
|---|---|---|
tcp4 |
AF_INET |
TCP over IPv4 |
tcp6 |
AF_INET6 |
TCP over IPv6 |
vsock |
AF_VSOCK |
Linux inter-VM/inter-container sockets (see here) |
unix |
AF_UNIX |
Domain sockets |
The target platform is Linux. It depends on its splice system call to efficiently move data between sockets (without tripping that data through userspace).
0.1 Compatibility
When not using the subprocess functionality, it should work on kernels upwards of version 3.9, or even 3.2 (the minimum version for Golang 1.24) if not using the vsock address family.
For the subprocess functionality, kernel version 5.11+ is required; earlier versions lack the CLOSE_RANGE_CLOEXEC flag for the close_range() syscall.
0.2 Installing
go install -v "nontrivialpursuit.org/llmao-af_moshpit@latest"
0.3 Colophon
© 2026 Wicher Minnaard. Licensed to the public on the terms of the GNU GENERAL PUBLIC LICENSE, Version 3, see LICENSE.txt. Need this code, but under a different license? Just ask.
1. Prior art, motivation
- the venerable and versatile socat, which has a one circuit per process model, which is not the best mechanism when scaling to many connections.
- vsock-proxy, which only allows one address pair per process, and does not support unix sockets.
Plus, both the above programs trip proxied data through userspace, and I wanted to explore how the purported "high performance" approach of using the splice system call, which avoids that, would look like.
More background on the rationale is to be found here.
2. LLM-assisted❗
This project has been created with LLM assistance. For the better, as I probably wouldn't have created it if I would have had to do all the work myself. For the worse, because, well, others have written eloquently on this subject, but one component is that I don't always have the highest opinion of LLM taste/judgment, hence the program's portmanteau name prefix of llmao. In the end quite some human thoughtfulness and intervention has gone into this project to end up with something not utterly irresponsible to use in production.
3. Operation and example use case
llmao-af_moshpit takes one or more listen+connect pairs. For each pair it listens on the first address and, for every incoming connection, opens a connection to the second address. Data is then shuttled between the two sides.
Optionally, it runs a subprocess, and then instead of running indefinitely, it will run until the subprocess exits.
3.1 Usage
llmao-af_moshpit [options] <listen-addr>+<connect-addr> [<listen-addr>+<connect-addr> ...] [-- <command> [args...]]
Address formats:
tcp4://host:porttcp6://[host]:portvsock://cid:portunix:///path/to/socket(may use a relative path as well)
Options:
-verbose— log connection events to stderr-tcp-user-timeout <duration>— the kernel'sTCP_USER_TIMEOUTfor outgoing connections: abort a connect that takes longer than this to establish, and later abort a connection on which data goes unacknowledged for this long (default10s;0disables). Only applies to TCP; AF_UNIX and AF_VSOCK connects are unbounded.-graceful-drain-timeout <duration>— when shutting down (subprocess exit, orSIGINT/SIGTERM), stop accepting new connections and wait at most this long for existing circuits to finish before exiting;0(the default) waits indefinitely-unlink-unix-sockets— remove existing Unix socket files before binding. Independently of this flag, any Unix listening socket created is always unlinked upon exit.-tcp-keepalive— enable TCP keepalives on TCP connections (defaulttrue)-tcp-keepalive-idle <seconds>— idle time in seconds before the first keepalive probe (default60)-tcp-keepalive-interval <seconds>— interval in seconds between keepalive probes (default15)-tcp-keepalive-count <n>— failed probes before the connection is dropped (default4)-inherit-fd <fd>— inherit this file descriptor in the subprocess (may be given multiple times); stdin/stdout/stderr are always inherited
TCP keepalives are enabled on both TCP sides of a circuit: on outgoing TCP connections, and on TCP connections accepted from clients.
3.2 Subprocess mode
If a -- separator is present in the invocation arguments, everything after it is treated as a command to execute as a subprocess once all listeners are bound. Then, the proxy runs for the lifetime of that subprocess and exits with its exit code.
The subprocess always inherits stdin, stdout, and stderr. By default any other inherited file descriptors are not bestowed on the subprocess. Use -inherit-fd <fd> to pass through specific inherited file descriptors as well.
3.3 Signals and shutdown
The proxy runs until it is asked to stop, either because the subprocess exited (subprocess mode) or because it received a signal (both modes). It always shuts down in two phases: first it stops accepting new connections by closing its listeners, and then it drains extant connections.
-graceful-drain-timeout <duration> bounds how long that draining may take. Once it elapses, the remaining circuits are cut and the proxy exits. The default of 0 waits indefinitely; in that case, abort the drain with a signal (see below).
Signals are handled as follows:
| Signal | Effect |
|---|---|
SIGINT, SIGTERM |
Request a graceful shutdown. Without a subprocess, the proxy starts draining immediately. With a subprocess, the signal is forwarded to it instead and the proxy keeps serving clients until the subprocess exits, draining from then on. |
SIGQUIT |
Terminate immediately, without draining. |
Once draining has begun, the proxy restores the default disposition of SIGINT and SIGTERM, so sending either one again (or SIGQUIT) ends the process at once. This is how you abort a drain that is taking too long, which matters most with the default indefinite timeout. All other signals keep their default dispositions throughout.
Corollaries:
- Without a subprocess, the first
SIGINT/SIGTERMstarts the drain; the proxy then stops catching them, so a second one is what force-quits. - With a subprocess,
SIGINT/SIGTERMare propagated to it (viapidfd_send_signal). If the subprocess ignores them, it never exits and the drain never starts; useSIGQUITto force the proxy down. Once the subprocess has exited and draining is under way, a furtherSIGINT/SIGTERMforce-quits the proxy. - When the proxy and the subprocess share a controlling terminal, a terminal-generated
SIGINT(e.g.Ctrl-C) is delivered by the kernel to the whole foreground process group, so the subprocess typically receives it directly as well as through the proxy's forwarding. SIGQUITis never caught, so it terminates the proxy immediately, both before and during a drain.
In subprocess mode the proxy exits with the subprocess's exit code; otherwise it exits 0. A signal that terminates the proxy itself ends it with that signal's usual status.
4. Example invocations
4.1 A most basic example
Let's have ourselves an impromptu TCP server, using socat:
4.1.1 Process 1, TCP server
socat TCP-LISTEN:2222 -
In another shell, start the proxy:
4.1.2 Process 2, af_moshpit proxy
llmao-af_moshpit vsock://-1:1234+tcp4://127.0.0.1:2222
The CID of -1 (VMADDR_CID_ANY) allows to listen for any host/guest; it's basically the AF_VSOCK equivalent of the AF_INET 0.0.0.0 address. Other special addresses are 0, 1 and 2; see man 7 vsock.
This makes the proxy listen on port 1234/vsock, and will proxy onwards to the upstream socat TCP server that we launched as Process 1.
In yet another shell, address the vsock proxy address:
4.1.3 Process 3, vsock client
echo Hello | /usr/bin/socat - VSOCK-CONNECT:2:1234
If all went well, we'll see the socat server of Process 1 print "Hello", and subsequently exit.
4.2 Step up: Subprocess demo
In this case, for the same effect as the previous example, we could replace Process 1 and 2 with just one incantation, using the subprocess feature, like this:
4.2.1 Proxy-wrapped server process
llmao-af_moshpit vsock://2:1234+tcp4://127.0.0.1:2222 -- \
socat TCP-LISTEN:2222 -
Going even further, we can do this kind of wrapping on the client side as well, involving another proxy to let our client speak the common tongue, TCP:
4.2.2 Proxy-wrapped client process
echo Hello | llmao-af_moshpit tcp4://127.0.0.1:4444+vsock://2:1234 -- \
socat - TCP-CONNECT:127.0.0.1:4444
4.3 Step up: Container-Host networking, without network interfaces
Not having to clean up proxies when they're no longer needed can be handy, especially on the clientside — we can provide proxies inside sandboxes which are running a single command, such as bubblewrap:
4.3.1 Impromptu server process, proxy-wrapped:
llmao-af_moshpit vsock://2:1234+tcp4://127.0.0.1:2222 -- \
socat TCP-LISTEN:2222,reuseaddr,fork -
This time we use some socat server options so as to not make it exit when a client disconnects.
4.3.2 Containerized client process, accessing a server socket on the host:
echo Hello | \
bwrap --unshare-all --ro-bind / / -- \
llmao-af_moshpit tcp4://127.0.0.1:4444+vsock://2:1234 -- \
socat - TCP-CONNECT:127.0.0.1:4444
Again, "Hello" appears at the server process' stdout.
What's happening is that bwrap launches llmao-af_moshpit, which launches socat. The "Hello" again travels all the way through to socat, because each process lets their child process inherit the standard input file descriptor. The containerized socat client connects to the (also containerized) proxy, which connects over vsock to the proxy outside the container (remember, vsock is not subject to namespace isolation – but we could have also used a unix socket to the same effect, by sharing the unix socket's filesystem location). That last proxy "turns vsock into tcp", and so the circuit completes to the socat impromptu server.
Replace the impromptu server with a filtering http/https proxy, and you will be able to work with toxic substances such as npm install and its reaction byproducts inside your sandbox while only allowing https access to npmjs.org, etc. The sandbox wouldn't even need DNS resolution (data exfil via DNS requests is a thing). Of course, you could achieve the same end result through elaborate bridging and firewalling setups, but then you'd need to do setup of elaborate bridging and firewalling, which is quite different from the stateless shell one-liner below:
4.3.3 Culmination: everything in one go
Supposing you have a HTTP proxy running on 127.0.0.1:8889, then:
llmao-af_moshpit vsock://2:1234+tcp4://127.0.0.1:8889 -- \
bwrap --unshare-all --ro-bind / / -- \
llmao-af_moshpit tcp4://127.0.0.1:4444+vsock://2:1234 -- \
curl --proxy http://127.0.0.1:4444 --proxytunnel https://api.ipify.org
will make a sandboxed curl use that proxy, while not having any other means to reach out to the internet from inside the container.
It should be clear by now how this works. But note that one of the advantages of using vsock (no namespace isolation) is also its drawback (whoops, no isolation): containers (and VMs) can, for instance, vsock-portscan their host. But that's easily remedied at least for containers, as they can use unix sockets on a shared filesystem location to achieve better isolation.
5 Future work, maybe
- allow blocking incoming connections by origin specification, a bit like tcpwrappers
- as a listening address, accepting a file descriptor of a bound socket (useful for systemd socket activation)
Documentation
¶
There is no documentation for this package.