Forwarding streams over Magic Wormhole

March 25, 2023

(Update: now named fowl)

I’ve been playing around with an experimental Magic Wormhole command provisionally spelled fowl (for “forward over wormhole, locally”). This command is somewhat like ssh -L or -R: a way to forward stream-style connections over a secure connection (here, a Dilation subchannel).

Usually these would be TCP streams, but because it’s Twisted anything that clientFromString() supports can be used (and on the other side, anything serverFromString() supports).

Let’s take a look!

Currently the code lives at: https://github.com/meejah/fowl

Why Would I Do This?

A wormhole connection using Dilation gives us account-less, secure, durable connections between two computers. Connections try to be peer-to-peer but can use a “transit relay” if required by current network conditions (e.g. both ends behind a NAT).

So if you already have ssh keys exchanged between the machines, use that (but you don’t get “durable” then).

The “durable” part means that bytes sent into the API will eventually be delivered, even in the face of unreliable, intermittent or changing networks. That is: start a session between your laptop and home desktop, close your laptop, board a train and the connection resumes there.

My immediate interest is to make connections for pair-programming with tty-share, among other tools – but without needing to rely on their central server.

fowl invite and fowl accept

The only wormhole library that has an implementation of the Dilation protocol is the Python one (currently marked “experimental” so you have to opt-in). However, I’d like to use it from another language.

So, the idea is to run fowl as a subprocess. It takes commands on stdin, one line of JSON at a time. It emits events to stdout, one line of JSON at a time.

Magic Wormhole works between two computers, and so does this. Every command has a "kind": "..." key which specifies what the message is.

You can ask for a local forward like this (sent as a single line on stdin, but I’ve formatted example messages nicely for this blog post):

{
    "kind": "local",
    "endpoint": "tcp:8888:interface=localhost",
    "local-endpoint": "tcp:localhost:8888",
}

This says to open a listener on tcp:8888 on localhost. Upon any connection to that listener, open a subchannel and ask the other end to open a local connection on tcp:localhost:8888. These can be anything that serverFromString() or clientFromString() (respectively) understand.

WARNING there’s no policy or similar code in place, so it can be anything – ensure you control or trust both ends.

I would like a permissions API of some sort so UIs can ask or alert their humans.

There are a bunch of messages emitted on stdout with different "kind" keys, but I’m not sure how useful any of them are yet :)

Cool, just use ssh -L though?

There are a few reasons I’m using Wormhole for this:

How it Works

Dilation uses a mailbox to communicate connection hints and decide on connections. This allows it to keep some form of communication open. On top of that, Dilation builds any number of subchannels (including a special always-open “control” one).

See the Dilation description for more detail.

When we receive an incoming connection on a local listener, we open a new subchannel. Down this subchannel is sent a single length-prefixed msgpack-encoded message describing the sort of connection we want on the other side (that is, the client-type Twisted endpoint string).

When this connection succeeds, we communicate back we’re ready (or if it fails, we can close the subchannel) and from then on forward bytes back and forth.

Can I Use It Now?

Sure! It’s “experimental” and “documentation / README”-based development here, so the words outpace the code. It’s pretty messy and lacks tests.

That said, you can make tty-share work over it. On computer one:

tty-share  # separate terminal opens a local listener on port 8000
fowl invite  # extract the invite-code
# once the other side connects, paste in command:
{"kind": "remote", "remote-endpoint": "tcp:8000:interface=localhost", "local-endpoint": "tcp:localhost:8000"}

On computer two:

fowl accept <invite-code>
# once connected, in a separate terminal run:
tty-share http://localhost:8000/s/local/

What’s happening here is that we’re running a tty-share server on computer one. We open a listener on computer two on the same port, forwarding to 8000 on computer one.

We then run a tty-share connect to computer two’s local 8000 port (which is forwarded, over a wormhole subchannel, to computer one).

A tty-share connection without using a public server, connecting securely to our peer computer.

Future Directions

I want to add a TUI of some sort in front of this (so you’re not copy-pasta-ing JSON commands).

This TUI can co-ordinate more than one thing, so adding audio would be great (gstreamer? icecast?)

This TUI can co-ordinate more than one peer too, so we could invite others to our “pairing” session.

I also think this would make a fun testbed for Seeds and other advanced Wormhole features proposed (or in development).

Ultimately: a grab-bag of pairing tools, experimenting with magic-wormhole.

Get in touch!