BitBang CLI Part 1: A Terminal, File Browser, and Proxy Over WebRTC, No Port Forwarding

bitbang-cli (richlegrand/bitbang-cli on GitHub, MIT licensed) is a small, new project: a single static Go binary that turns bitbang serve into a terminal, a file browser, and a reverse proxy to the local network, reachable from any browser with no port forwarding, no account, and nothing installed on the connecting side. Everything in this post was built from the real source and run for real, not summarized from the README.

Both quotes below are real. The build used a local Go 1.25.0 toolchain (no root needed, just the official tarball extracted to a working directory), then a plain go build ./cmd/bitbang/ against the repo cloned at HEAD:

$ go version
go version go1.25.0 linux/amd64

$ git clone --depth 1 https://github.com/richlegrand/bitbang-cli.git
$ cd bitbang-cli && go build -o bitbang ./cmd/bitbang/

$ ./bitbang version
bitbang v0.4.7

Pure Go, CGO_ENABLED=0, no runtime dependencies. The resulting binary is a self-contained 17 MB executable with the WebRTC stack (the pion libraries), a websocket client, and a PTY implementation statically linked in.

What serve actually shares

bitbang serve has four forms: everything at once, or shell/files/proxy individually. Running it for real prints a QR code, a URL, and a 6-digit pairing code:

$ bitbang serve -ephemeral
URL: https://bitba.ng/V9K63r1oTEqB3BGgS6MWwA#CAhD3KjlPUA
Sharing:
  • shell  ($SHELL or /bin/sh, mirroring to console)
  • files  (/home/michealg)
  • proxy  (target chosen in browser)

Warning: anyone with this URL gets a shell on this machine.
  Use --pin <PIN> for a second factor, or pick a non-shell mode.
Ready
Pairing code: 830922 (valid 5 minutes)

That URL is live the moment it’s printed. -ephemeral generates a throwaway identity for the run instead of reusing a saved one, which is the right default for testing this kind of thing, since without it the tool persists a long-lived keypair (more on that below).

The real flag list for serve (bitbang serve --help), trimmed to what matters day to day:

-ephemeral            Use a temporary identity
-files string         Files path (default: current working directory)
-files-upload         Allow uploads to the shared directory
-nocode               Disable code-exchange pairing; URL still works
-pin string           PIN to protect access
-server string        Signaling server hostname (default "bitba.ng")
-shell-cmd string     Shell command to spawn (default: $SHELL or /bin/sh)
-shell-max-sessions int   Max concurrent shell sessions (default 1)
-target string         Fixed proxy target host:port (proxy-only mode)

-target is what makes proxy mode either dynamic (pick the LAN address in the browser after connecting) or pinned to one service at listen time. -server is worth noting early: it’s not hardcoded to bitba.ng, the signaling protocol is self-hostable, and the install script’s INSTALL_URL behavior (empty means 404) suggests the project was built with self-hosting in mind from the start.

A real end-to-end connection

The interesting test is a genuine connect, not just reading --help. Started a listener, grabbed its printed URL, and connected to it with the CLI client from a separate invocation:

$ bitbang serve -ephemeral -nocode &
URL: https://bitba.ng/5NR9bGVISjYWYjY7HvJSVQ#IXTf9rGlzD0

$ bitbang connect https://bitba.ng/5NR9bGVISjYWYjY7HvJSVQ#IXTf9rGlzD0 -- echo "hello from remote shell"
Connecting to bitba.ng...
Connected.
Saved as "device1".
hello from remote shell

$ bitbang connect https://bitba.ng/5NR9bGVISjYWYjY7HvJSVQ#IXTf9rGlzD0 -- whoami
Connecting to bitba.ng...
Connected.
michealg

That’s a real command executed on the listening machine, over a connection negotiated through bitba.ng but carrying the actual traffic peer-to-peer. The listener’s own log confirms it didn’t touch a relay:

Connection request from 5NR9bGVISjYWYjY7HvJSVQ_ed70947a (browser_ip=203.0.113.42)
Connection connected for 5NR9bGVISjYWYjY7HvJSVQ_ed70947a via DIRECT in 471ms (local=host remote=host)
Connection data channel opened for 5NR9bGVISjYWYjY7HvJSVQ_ed70947a
Shell started: argv=[echo hello from remote shell] pty=false
Shell exited: argv=[echo hello from remote shell] code=0

Two things worth flagging in that log line. First, via DIRECT versus a TURN relay is the WebRTC ICE negotiation working as intended, since a straight peer-to-peer path was available. Second, and more relevant later: the listener’s log records the connecting party’s public IP (browser_ip=), even though the data itself never transits bitba.ng. The signaling server sees enough metadata to broker the connection, and that metadata includes the browser’s address. That’s a real, useful forensic detail, not a theoretical one.

bitbang cp (scp-equivalent) works the same way against serve files:

$ bitbang serve files /tmp -upload -ephemeral -nocode &
URL: https://bitba.ng/C027OeMNBBkiUCj_5cx6Uw#yGWKu1TCLfc

$ bitbang cp https://bitba.ng/C027OeMNBBkiUCj_5cx6Uw#yGWKu1TCLfc:/testfile.txt ./downloaded.txt
Downloading /testfile.txt -> ./downloaded.txt
Done (38 B in 0.0s)

The pairing model, verified

The 6-digit code exists specifically for the case where you can’t paste a URL or scan a QR code, read it over the phone, or across a room. Under the hood it’s a short authentication string (SAS) computed independently on both ends from the negotiated DTLS fingerprints plus two committed nonces, the same mechanism Magic Wormhole uses: a machine-in-the-middle necessarily has different DTLS fingerprints on each leg of the connection, so the two spoken numbers can’t be made to match.

PIN protection is separate and tested cleanly: connecting without the correct PIN fails outright rather than degrading to a partial connection.

$ bitbang serve -ephemeral -nocode -pin 4242 &

$ bitbang connect <url> -- whoami
Connecting to bitba.ng...
PIN:
connect: PIN prompt: inappropriate ioctl for device
(fails without a TTY to prompt on, and fails outright with no -pin flag supplied)

$ bitbang connect <url> -pin 4242 -- whoami
Connecting to bitba.ng...
Connected.
michealg

What actually gets stored on disk

This is the part worth checking directly rather than trusting the README’s summary. Running serve without -ephemeral generates a persistent identity:

$ bitbang serve -nocode &
$ ls -la ~/.bitbang/bitbang/
-rw------- identity.pem
-rw------- lock

$ head -3 ~/.bitbang/bitbang/identity.pem
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDT...

That’s a real RSA private key (MA0GCSqGSIb3DQEBAQUA is the PKCS8 OID for rsaEncryption), file mode 0600, matching the README’s “self-certifying identity” claim: the device’s UID is derived from the public half, so a device is recognized on reconnect without any central account.

The connect client keeps its own record, ~/.bitbang/devices.json, also mode 0600:

{
  "devices": [
    {
      "name": "testbox",
      "uid": "5Me0gptoIY-XTOuVpekdCQ",
      "access_code": "jI_iBZHQb1s",
      "server": "bitba.ng",
      "paired_at": "2026-08-03T19:16:01Z"
    }
  ]
}

That access_code is the credential that authorizes reconnecting to this specific device by its saved name (bitbang connect testbox) instead of the full URL. Worth noting for anyone thinking about opsec around this file: it’s a bearer credential sitting in plaintext JSON, protected by filesystem permissions and nothing else. 0600 is the right default, but it’s the same class of file as an SSH private key or an AWS credentials file, and should be treated with the same caution about backups, dotfile syncing, and shared machines.

How it actually compares to SSH, ngrok, and Tailscale

The project’s own README has a comparison table. Here’s the same comparison with what actually mattered in testing:

SSHngrokTailscalebitbang-cli
Needs an accountNoYesYesNo
Needs an inbound portYesNoNo (mesh)No
Install on connecting sideYes (client)NoYesNo (browser works)
End-to-end encryptedYesNot by defaultYesYes (DTLS)
Data pathDirectTheir serversP2P (mesh)P2P (confirmed DIRECT in testing)
Self-hostableN/ANoNo (Headscale is third-party)Yes (own -server flag)
Setup before first useKeys on target firstAccount + authtokenAccount + login per deviceOne command, no prior setup

The genuinely different thing here isn’t any single row, it’s that bitbang-cli needs nothing configured on the target ahead of time. SSH needs to already be enabled and a key already delivered. ngrok and Tailscale both need an account created before the first connection. bitbang serve works on a completely fresh machine, cold, in one command, and that same property is exactly what makes it worth treating carefully as a possible post-exploitation tool rather than just a convenience utility, which is where Part 2 picks up.

What’s not there yet

The roadmap is honest about gaps: no macOS or Windows builds (Linux/arm only today, issues open for both), and three features not yet shipped: serial bridging (drive a remote /dev/ttyUSB0), TCP port forwarding (-L-style, the thing that would let it stand in for SSH’s -L 1433:... against a raw TCP service the way Pivoting and Tunneling Part 1 covered), and remote desktop over a WebRTC video track. Today’s proxy mode is HTTP/WebSocket reverse-proxying only, genuinely useful for reaching a web app on the target’s LAN, but not yet a general-purpose tunnel for arbitrary TCP services.