Header-based reverse proxy that routes to different local ports based on the X-Cmux-Port-Internal header. Now also supports per-workspace routing via X-Cmux-Workspace-Internal to choose a distinct upstream IP for network isolation. Supports:
- HTTP requests (streaming)
- WebSocket upgrades (transparent tunneling)
- Generic TCP via HTTP CONNECT tunneling
This is useful for multiplexing multiple local services behind a single port while choosing the target by header.
- One-liner (latest release):
curl -fsSL https://raw.githubusercontent.com/lawrencecchen/cmux-proxy/main/scripts/install.sh | bash- Optionally pin a version:
CMUX_PROXY_VERSION=v0.0.1 curl -fsSL https://raw.githubusercontent.com/lawrencecchen/cmux-proxy/main/scripts/install.sh | bash
- Docker (multi-arch):
docker run --rm -p 8080:8080 ghcr.io/lawrencecchen/cmux-proxy:latest
Notes:
- The installer puts
cmux-proxyin/usr/local/binby default. Override withCMUX_PROXY_BIN_DIR. - Architectures supported by the installer:
x86_64andaarch64.
- Build:
cargo build --release - Run:
./target/release/cmux-proxy --listen 0.0.0.0:8080(default)
Env/flags:
--listenorCMUX_LISTEN(accepts multiple or comma-separated). Defaults to0.0.0.0:8080,127.0.0.1:8080.- Note: binding to
0.0.0.0:<port>already covers127.0.0.1:<port>; duplicate binds are deduped to avoid conflicts.
- Note: binding to
--upstream-hostorCMUX_UPSTREAM_HOST(default127.0.0.1)- If
X-Cmux-Workspace-Internalis present on a request, it overrides this host per-request using the mapping below.
- If
- Build and run tests inside Linux:
docker build -t cmux-proxy-test . - Or use helper:
./scripts/run-tests-in-docker.sh
End-to-end (E2E) bash tests that validate workspace isolation and proxy routing from host:
./scripts/e2e.sh- Builds the runtime image, starts a container exposing
:8080. - Inside the container, starts HTTP servers bound in
/root/workspace-aand/root/workspace-bon the same port via LD_PRELOAD isolation. - Verifies isolation inside the container (A works in A, fails in B).
- From the host, curls the proxy using both header-based routing and subdomain Host routing.
- Requires Docker and curl on the host.
- Stress mode: launches many servers across distinct workspaces and verifies isolation in parallel.
- Tunables:
STRESS_N(default 32),STRESS_PORT(default 3200),STRESS_CONC(default 16).
- Tunables:
- Builds the runtime image, starts a container exposing
This runs cargo test in a Debian-based Rust image and pre-adds example loopback IPs in 127.18.0.0/8.
-
HTTP
-
curl -v -H 'X-Cmux-Port-Internal: 3000' http://127.0.0.1:8080/api -
Proxies to
http://127.0.0.1:3000/api. -
With workspace:
curl -v -H 'X-Cmux-Workspace-Internal: workspace-1' -H 'X-Cmux-Port-Internal: 3000' http://127.0.0.1:8080/api -
Proxies to
http://127.18.0.1:3000/api(see mapping below).
-
-
WebSocket (client must send the header)
- Example with websocat:
websocat -H 'X-Cmux-Port-Internal: 3001' ws://127.0.0.1:8080/ws - Proxies to
ws://127.0.0.1:3001/ws(upgrade tunneled). - With workspace:
websocat -H 'X-Cmux-Workspace-Internal: workspace-2' -H 'X-Cmux-Port-Internal: 3001' ws://127.0.0.1:8080/ws - Proxies to
ws://127.18.0.2:3001/ws.
- Example with websocat:
-
TCP via CONNECT (create a raw TCP tunnel)
- The proxy will ignore the CONNECT target host/port and use the header port.
- Example (Redis tunnel):
curl --http1.1 -x http://127.0.0.1:8080 -H 'X-Cmux-Port-Internal: 6379' -v https://example(establishes CONNECT then tunnels). A better test is to script aCONNECTrequest withnc.
- The header
X-Cmux-Port-Internalis required on every request; value must be a valid TCP port (1-65535). - Optional header
X-Cmux-Workspace-Internalselects a per-workspace loopback IP. If omitted,--upstream-hostis used. - Workspace to IP mapping: for a workspace name
workspace-NwhereNis a positive integer, the upstream host is127.18.(N>>8).(N&255).- Examples:
workspace-1 -> 127.18.0.1,workspace-256 -> 127.18.1.0. - If the name does not end in digits, a stable hash may be used in the future; currently non-numeric names return 400.
- Examples:
- This enables running identical services on the same ports in different workspaces, each bound to a unique loopback IP.
- Only HTTP/1.1 is supported on the front-end. HTTP/2 is not supported (WebSocket over H2 is not handled).
- Hop-by-hop headers are stripped where appropriate; upgrade is handled specially to preserve handshake headers.
- Upstream host defaults to
127.0.0.1. If you need another host, pass--upstream-host. The header only specifies the port.
- This proxy does not terminate TLS; inbound must be plain HTTP/WS. If you need TLS, put a TLS terminator in front.
- For CONNECT, the client and upstream protocols are opaque to the proxy. The proxy just tunnels bytes.
- Per-workspace IPs live in
127/8which is loopback on Linux. Binding to127.18.x.ytypically works without adding the address, but you can also add it explicitly:ip addr add 127.18.0.1/8 dev lo.
If you want processes started inside a workspace directory (e.g., /root/workspace-1) to automatically bind/connect to their per-workspace IP without changing the app code, you can use the provided LD_PRELOAD shim in ldpreload/:
- It intercepts
bind(2)andconnect(2)and rewrites0.0.0.0/127.0.0.1to the workspace IP computed from the directory name. - Detection: if CWD is under
/root/workspace-N, the workspace name isworkspace-N. - Usage (Linux):
- Build:
make -C ldpreload - Run a command in a workspace:
cd /root/workspace-1 && LD_PRELOAD=./ldpreload/libworkspace_net.so your-app - Optional overrides: set
CMUX_WORKSPACE_INTERNAL=workspace-2to force a workspace, orCMUX_PRELOAD_DISABLE=1to disable.
- Build:
Note: creating Linux network namespaces requires root/capabilities; this shim focuses on per-IP isolation on loopback.
MIT