llmao-af_moshpit

command module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 22, 2026 License: GPL-3.0 Imports: 18 Imported by: 0

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:port
  • tcp6://[host]:port
  • vsock://cid:port
  • unix:///path/to/socket (may use a relative path as well)

Options:

  • -verbose — log connection events to stderr
  • -tcp-user-timeout <duration> — the kernel's TCP_USER_TIMEOUT for 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 (default 10s; 0 disables). Only applies to TCP; AF_UNIX and AF_VSOCK connects are unbounded.
  • -graceful-drain-timeout <duration> — when shutting down (subprocess exit, or SIGINT/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 (default true)
  • -tcp-keepalive-idle <seconds> — idle time in seconds before the first keepalive probe (default 60)
  • -tcp-keepalive-interval <seconds> — interval in seconds between keepalive probes (default 15)
  • -tcp-keepalive-count <n> — failed probes before the connection is dropped (default 4)
  • -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/SIGTERM starts the drain; the proxy then stops catching them, so a second one is what force-quits.
  • With a subprocess, SIGINT/SIGTERM are propagated to it (via pidfd_send_signal). If the subprocess ignores them, it never exits and the drain never starts; use SIGQUIT to force the proxy down. Once the subprocess has exited and draining is under way, a further SIGINT/SIGTERM force-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.
  • SIGQUIT is 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

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
tests
helper command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL