Skip to content

Latest commit

 

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

muxbridge

codecov

MuxBridge is a self-hosted HTTPS tunnel inspired by Cloudflare Tunnel. It lets you securely expose HTTP services running behind NAT or a firewall to the public internet — no inbound firewall rules or port forwarding required.

How It Works

Browser ──HTTPS──▶ Edge Server (public) ──gRPC tunnel──▶ Client (private network) ──▶ Local HTTP app

You run an edge server on a public host. Your client — sitting behind NAT, a corporate firewall, or any private network — connects outbound to the edge over a persistent gRPC stream. The edge then forwards incoming HTTP requests through that stream to the client, which answers them using its local HTTP handler. Responses travel back the same way.

No VPN. No open ports on the client machine. Just an outbound gRPC connection.

Key features

  • Token-based authentication — each client authenticates with a secret token; the edge maps tokens to usernames
  • Automatic subdomain routing — a client registered as demo is published at demo.<public-domain>
  • WebSocket support — upgraded connections are proxied as bidirectional byte streams
  • Automatic TLS — edge uses CertMagic for automatic ACME certificate provisioning, or you can supply your own cert/key
  • Multiplexed requests — multiple in-flight HTTP requests share a single gRPC stream, correlated by request ID
  • Reconnect on disconnect — clients automatically reconnect with configurable backoff

Compared to Cloudflare Tunnel

Cloudflare Tunnel MuxBridge
Control plane Cloudflare's network Your own edge server
Protocol QUIC / HTTP/2 gRPC over HTTP/2
TLS Cloudflare-managed CertMagic (ACME) or static cert
Auth Cloudflare Access Token → username mapping
WebSocket Yes Yes
Self-hostable No Yes
Client deployment Separate cloudflared daemon Embeddable Go library — add tunnel.NewClient(...) directly to your app

Cloudflare Tunnel requires you to install and run a separate cloudflared process on every network where you want to expose a service. MuxBridge ships a Go library so you can embed the tunnel client directly into your own application — no sidecar, no external process, no extra deployment step. Your app dials the edge and serves traffic through its own http.Handler.

Build

Build the binaries with:

make -f makefile all

This produces:

  • bin/edge
  • bin/demo-client
  • bin/perf-client

You can also build directly with Go:

go build -o bin/edge ./cmd/edge
go build -o bin/demo-client ./cmd/demo-client
go build -o bin/perf-client ./cmd/perf-client

Test

go test ./...
go test -race ./...
go test ./... -coverprofile=/tmp/muxbridge.cover.out
go tool cover -func=/tmp/muxbridge.cover.out

The current suite uses real listeners and gRPC streams and keeps total statement coverage above 90%.

How Routing Works

  • The edge control endpoint is always edge.<public-domain>.
  • Each client authenticates with a token.
  • The edge maps tokens to usernames with token=username.
  • A connected client is published at <username>.<public-domain>.
  • Example: demo-token=demo publishes the client at demo.example.com.
  • Username edge is reserved and cannot be used.
  • Usernames must be a single lowercase DNS label containing a-z, 0-9, and interior -.
  • Request forwarding preserves method, headers, body, query string, and the escaped request path via raw_path.
  • The forwarded request scheme comes from the edge connection itself: HTTPS requests arrive as https, plain HTTP requests arrive as http, and client-supplied X-Forwarded-Proto is ignored.
  • WebSocket upgrade requests are proxied as upgraded byte streams after the HTTP handshake.

Register.tunnel_id is still present in the wire format for compatibility, but routing no longer depends on it.

Edge Server

The edge server listens on:

  • :80 for HTTP redirect and ACME HTTP-01 challenges
  • :443 for HTTPS and gRPC

Requests are routed like this:

  • <public-domain> -> MuxBridgh active with uptime <duration>
  • edge.<public-domain> with gRPC over HTTP/2 -> tunnel control plane
  • edge.<public-domain> without gRPC -> 404
  • <username>.<public-domain> -> proxied through the authenticated client session
  • anything else -> 404

If the same user connects again, the newest connection replaces the old one.

TLS

By default, the edge server uses CertMagic and manages certificates for:

  • <public-domain>
  • edge.<public-domain>
  • every configured <username>.<public-domain>

Managed certificates use ACME HTTP-01 challenges on port 80. DNS for each managed hostname must point at the edge server, and both ports 80 and 443 must be reachable. TLS-ALPN challenges are disabled.

Static Certificate Support

If you provide both a certificate file and key file, the edge server uses them instead of CertMagic:

  • --tls-cert-file
  • --tls-key-file

or:

  • MUXBRIDGE_TLS_CERT_FILE
  • MUXBRIDGE_TLS_KEY_FILE

Both must be provided together.

Edge Configuration

Flags

--public-domain
--client-credential token=username
--tls-cert-file
--tls-key-file
--debug

--client-credential may be repeated.

Environment Variables

MUXBRIDGE_PUBLIC_DOMAIN
MUXBRIDGE_CLIENT_CREDENTIALS
MUXBRIDGH_DATA
MUXBRIDGE_MAX_INFLIGHT_PER_SESSION
MUXBRIDGE_MAX_TOTAL_INFLIGHT
MUXBRIDGE_TLS_CERT_FILE
MUXBRIDGE_TLS_KEY_FILE
MUXBRIDGE_DEBUG

MUXBRIDGE_CLIENT_CREDENTIALS uses comma-separated token=username entries:

demo-token=demo,admin-token=admin

Because entries are comma-separated, tokens supplied via MUXBRIDGE_CLIENT_CREDENTIALS must not contain ,. Tokens that contain commas must be provided via repeated --client-credential flags instead.

MUXBRIDGH_DATA sets the single persistent data directory used by the edge. When the edge manages TLS with CertMagic, certificates, ACME account data, OCSP cache entries, and lock files are all stored here. MUXBRIDGE_DATA is also accepted as a compatibility alias.

MUXBRIDGE_MAX_INFLIGHT_PER_SESSION limits how many active proxied requests a single connected client session may hold at once. The default is 128. When the limit is reached, additional public requests for that tunnel are rejected with 503 Service Unavailable instead of continuing to grow memory usage.

MUXBRIDGE_MAX_TOTAL_INFLIGHT limits the total number of active proxied requests across all connected client sessions. The default is 512. When the global cap is reached, the edge rejects additional public requests with 503 Service Unavailable even if an individual client session is still below its per-session limit.

Flag credential entries are appended after environment entries. Credential values are trimmed, usernames are normalized to lowercase before validation, and startup fails on malformed entries, duplicate tokens, duplicate usernames, invalid usernames, usernames with ports such as demo:443, or reserved username edge.

Running The Edge

CertMagic-managed TLS

bin/edge \
  --public-domain example.com \
  --client-credential demo-token=demo

Static TLS Certificate

bin/edge \
  --public-domain example.com \
  --client-credential demo-token=demo \
  --tls-cert-file /etc/ssl/example/fullchain.pem \
  --tls-key-file /etc/ssl/example/privkey.pem

Profiling

When the edge is started with --debug (or MUXBRIDGE_DEBUG=1), the standard net/http/pprof handlers are exposed on the edge domain under /pprof/:

https://edge.<public-domain>/pprof/          # index of available profiles
https://edge.<public-domain>/pprof/heap
https://edge.<public-domain>/pprof/goroutine
https://edge.<public-domain>/pprof/allocs
https://edge.<public-domain>/pprof/profile   # 30 s CPU profile by default
https://edge.<public-domain>/pprof/trace
https://edge.<public-domain>/pprof/cmdline
https://edge.<public-domain>/pprof/symbol

Example:

go tool pprof https://edge.example.com/pprof/heap
curl "https://edge.example.com/pprof/goroutine?debug=2"

The endpoints are only mounted when debug mode is enabled and return 404 otherwise.

Security: pprof exposes heap contents, goroutine stacks, and lets callers trigger long-running CPU profiles or execution traces. The edge domain has no built-in authentication, so leaving --debug on in production makes this data world-readable. Restrict access at the network layer (firewall, IP allowlist, reverse proxy with auth) before enabling debug mode on a public deployment.

Demo Client

The demo client serves a small HTTP app locally and connects it to the edge.

Routes served by the demo app:

  • / -> plain text greeting plus the browser's remote IP
  • /slow -> slow chunked plain-text response
  • /ws-demo -> browser page that exercises a WebSocket through the tunnel
  • /ws-demo/socket -> WebSocket endpoint used by /ws-demo
  • /sse-demo -> browser page that exercises Server-Sent Events through the tunnel
  • /sse-demo/events -> SSE endpoint used by /sse-demo

Defaults:

  • TLS enabled
  • token: demo-token
  • edge address: edge.<public-domain>:443 when --edge-addr is not provided
  • flag and environment string values are trimmed before use

Flags

--public-domain
--edge-addr
--token
--debug

Environment Variables

MUXBRIDGE_PUBLIC_DOMAIN
MUXBRIDGE_EDGE_ADDR
MUXBRIDGE_CLIENT_TOKEN
MUXBRIDGE_DEBUG

Run The Demo Client

bin/demo-client --public-domain example.com --token demo-token

With the matching edge configuration:

demo-token=demo

the demo client becomes available at:

https://demo.example.com/

Additional demo pages are available at:

https://demo.example.com/ws-demo
https://demo.example.com/sse-demo

Performance Client

The perf client serves a purpose-built benchmark app locally through the tunnel and then drives load against the real public hostname. It is meant for end-to-end edge+tunnel+backend measurements rather than synthetic localhost-only benchmarking.

Routes served by the perf app:

  • /healthz -> readiness probe used before the load phase starts
  • /fast -> small fixed plain-text response
  • /bytes -> fixed-size binary payload response
  • /stream -> chunked streaming response

The load generator keeps a configurable number of workers active for the full test duration. Each worker owns its own HTTP client and keeps traffic on HTTP/1.1 so the test uses real parallel public connections instead of collapsing onto a single HTTP/2 session.

Defaults:

  • token: perf-token
  • connections: 1000
  • duration: 30s
  • scenario: mixed
  • edge address: edge.<public-domain>:443 when --edge-addr is not provided

Scenarios

  • fast -> every request goes to /fast
  • stream -> every request goes to /stream
  • mixed -> weighted mix of /fast, /bytes, and /stream

The default mixed scenario is intentionally uneven: it spends most requests on /fast, adds a smaller amount of fixed-size /bytes traffic, and keeps a small stream workload in the mix. That gives a more realistic blend of short responses, payload-heavy responses, and chunked responses without making the run entirely CPU-bound or entirely bandwidth-bound.

Flags

--public-host
--public-domain
--edge-addr
--token
--connections
--duration
--scenario
--request-timeout
--ready-timeout
--debug

Environment Variables

MUXBRIDGE_PUBLIC_HOST
MUXBRIDGE_PUBLIC_DOMAIN
MUXBRIDGE_EDGE_ADDR
MUXBRIDGE_CLIENT_TOKEN
MUXBRIDGE_DEBUG

Run The Perf Client

With the matching edge credential:

perf-token=perf

and DNS pointing perf.example.com at the edge, run:

bin/perf-client \
  --public-domain example.com \
  --public-host perf.example.com \
  --token perf-token \
  --connections 1000 \
  --duration 30s \
  --scenario mixed

The client waits for https://perf.example.com/healthz to return 200 OK, then keeps roughly 1000 HTTP/1.1 public connections active for the configured duration and prints request throughput, response throughput, status counts, and latency percentiles.

The summary includes:

  • total requests, successful responses, and request errors
  • response status counts
  • requests per second and bytes per second
  • latency min, average, p50, p95, p99, and max

Example:

performance test summary
host: perf.example.com
scenario: mixed
connections: 1000
duration: planned=30s observed=30.017s
requests: total=48211 success=48211 errors=0
throughput: req/s=1606.20 bytes/s=12483011.44
latency: min=3.411ms avg=18.772ms p50=12ms p95=49ms p99=87ms max=214.118ms
statuses: 200=48211

Debug Logging

The edge, demo client, and perf client all support opt-in debug logging via --debug or MUXBRIDGE_DEBUG=1.

Example:

MUXBRIDGE_DEBUG=1 bin/edge \
  --public-domain example.com \
  --client-credential demo-token=demo

MUXBRIDGE_DEBUG=1 bin/demo-client \
  --public-domain example.com \
  --token demo-token

MUXBRIDGE_DEBUG=1 bin/perf-client \
  --public-domain example.com \
  --public-host perf.example.com \
  --token perf-token

Minimal End-To-End Example

  1. Point DNS at the edge server for:
    • edge.example.com
    • demo.example.com
  2. Start the edge:
bin/edge \
  --public-domain example.com \
  --client-credential demo-token=demo
  1. Start the demo client:
bin/demo-client \
  --public-domain example.com \
  --token demo-token
  1. Open:
https://demo.example.com/

Docker

A minimal Alpine-based image is provided. The edge binary is the entrypoint and exposes ports 80 and 443. All persistent edge data, including CertMagic certificates, is stored under MUXBRIDGH_DATA, which defaults to /var/lib/muxbridge in the container image.

Example docker-compose.yml:

services:
  muxbridge:
    image: ghcr.io/define42/muxbridge:latest
    restart: unless-stopped
    read_only: true
    ports:
      - "80:80"
      - "443:443"
    environment:
      MUXBRIDGE_PUBLIC_DOMAIN: example.com
      MUXBRIDGE_CLIENT_CREDENTIALS: demo-token=demo
      MUXBRIDGH_DATA: /var/lib/muxbridge
    volumes:
      - ./muxbridge-data:/var/lib/muxbridge

With read_only: true, the container root filesystem stays read-only and only the mounted data directory remains writable.

Then start the edge with:

docker compose up -d

Using the Tunnel Package

To expose your own http.Handler instead of the demo app, import the tunnel package:

import "github.com/define42/muxbridge/tunnel"

client, err := tunnel.New(tunnel.Config{
    EdgeAddr: "edge.example.com:443",
    Token:    "my-secret-token",
    Handler:  myHandler,
})
if err != nil {
    log.Fatal(err)
}

if err := client.Run(ctx); err != nil {
    log.Fatal(err)
}

The client automatically reconnects on disconnect with a configurable backoff (default 2 s).

Hello World Example

A complete Go program that serves a Hello World page through the tunnel:

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"github.com/define42/muxbridge/tunnel"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, World!")
	})

	client, err := tunnel.New(tunnel.Config{
		EdgeAddr: "edge.example.com:443",
		Token:    "my-secret-token",
		Handler:  mux,
	})
	if err != nil {
		log.Fatal(err)
	}

	if err := client.Run(context.Background()); err != nil {
		log.Fatal(err)
	}
}

With the matching edge credential my-secret-token=alice, visiting https://alice.example.com/ returns Hello, World!. No separate process or sidecar required — the tunnel is part of your binary.

About

MuxBridge

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages