A small, hacky TCP-over-WebSocket tunnel written in Python.
It lets you forward arbitrary TCP connections (games, custom protocols, data streams, etc.) through a WebSocket relay on the internet.
Originally this was built to expose a local Minecraft server to friends without router port forwarding.
But the tunnel is completely protocol-agnostic – it doesn’t know or care what’s inside the TCP stream.
So you can also use it for things like:
- custom TCP backends
- home-grown game servers
- simple chat / telnet-like protocols
- internal services you want to tunnel out of your LAN
[Client App] ─TCP─> [Client Adapter] ─WebSocket─> [Relay Server] ─WebSocket─> [Server Adapter] ─TCP─> [Server App]
- Client App: Any application that talks TCP to
localhost:PORT(e.g. Minecraft, your own program, …) - Client Adapter: Python script running on the client machine
- Relay Server: Runs on the internet (e.g. Render, Koyeb, Fly.io, …) and logically links host & client
- Server Adapter: Python script running on the machine where your actual server app lives
- Server App: Your real TCP server (Minecraft, custom TCP service, whatever)
The tunnel is “dumb”: it just forwards raw bytes in both directions and never interprets the protocol.
The project consists of three main parts:
- WebSocket server with two endpoints:
/host– connection from the server adapter/client– connection from the client adapter
- As soon as host and client are both connected, the relay starts piping data 1:1 between them.
Typically implemented with FastAPI +
asyncio, using apipe(ws_a, ws_b)function that runs both directions in parallel.
Runs on the machine that hosts your server app.
- opens a TCP connection to your local server
(e.g.127.0.0.1:25565for Minecraft) - opens a WebSocket connection to the relay (
wss://…/host) - forwards all bytes between TCP and WebSocket
Important constants in the script:
MC_SERVER_HOST = "127.0.0.1"
MC_SERVER_PORT = 25565 # Port of your local server app
WS_URL = "wss://YOUR-RELAY-URL/host" # WebSocket URL of the relay (host endpoint)Runs on the client’s machine.
- starts a local TCP server, e.g.
127.0.0.1:25565 - opens a WebSocket connection to the relay (
wss://…/client) - forwards all bytes between the local client app and the relay
Important constants in the script:
LOCAL_HOST = "127.0.0.1"
LOCAL_PORT = 25565 # Port the client app connects to
WS_URL = "wss://YOUR-RELAY-URL/client" # WebSocket URL of the relay (client endpoint)For the client app this just looks like a normal TCP server on localhost:LOCAL_PORT.
- Python 3.10+ (recommended)
- Python packages:
pip install websockets fastapi uvicorn
- A hosting provider that supports WebSockets, e.g.:
- Render
- Koyeb
- Fly.io
- Northflank
(or any other PaaS / VM where you can runuvicorn)
-
Clone the repo:
git clone <your-repo-url> cd tcp-ws-tunnel
-
Install dependencies:
pip install -r requirements.txt
or manually:
pip install websockets fastapi uvicorn
-
Adjust config values:
server_adapter.py→MC_SERVER_HOST,MC_SERVER_PORT,WS_URLclient_adapter.py→LOCAL_HOST,LOCAL_PORT,WS_URL- Relay server (
main.pyor similar) → WebSocket endpoints/hostand/client
You can deploy the relay on any service that runs a long-lived Python web process and supports WebSockets.
For example:
-
Push your relay server code to GitHub (including
requirements.txtandmain.py). -
On Render: New → Web Service → From Git Repository.
-
Select your repo.
-
Set:
- Build Command:
pip install -r requirements.txt
- Start Command:
uvicorn main:app --host 0.0.0.0 --port $PORT
- Build Command:
-
Choose a region (ideally in Europe if you’re in the EU).
-
After deploy, your app will have a URL like:
https://your-relay.onrender.comWebSocket URLs:
wss://your-relay.onrender.com/host wss://your-relay.onrender.com/client
You can do basically the same thing on Koyeb:
-
Create a new App → Service from GitHub repo.
-
Koyeb auto-detects Python using
requirements.txt. -
Set:
- Build command:
pip install -r requirements.txt
- Start command:
uvicorn main:app --host 0.0.0.0 --port $PORT
- Build command:
-
Service type: HTTP (not raw TCP). WebSockets work as HTTP upgrade on top of this.
-
After deploy, you’ll get a URL like:
https://your-relay.koyeb.appWebSocket URLs:
wss://your-relay.koyeb.app/host wss://your-relay.koyeb.app/client
The same uvicorn main:app --host 0.0.0.0 --port $PORT pattern also works on other platforms (Fly.io, Northflank, etc.) as long as they pass a $PORT env variable.
- Deploy the relay to Render, Koyeb, etc. as described above.
- Note your final relay URL and update:
WS_URLinserver_adapter.pyto…/hostWS_URLinclient_adapter.pyto…/client
On the machine where your server app runs:
-
Start your server app (e.g. Minecraft server, custom TCP server), listening on:
MC_SERVER_HOST : MC_SERVER_PORT -
Set
WS_URLinserver_adapter.py, e.g.:WS_URL = "wss://your-relay.koyeb.app/host"
-
Run the adapter:
python server_adapter.py
The adapter connects:
- via TCP to your local server app
- via WebSocket to the relay
/host
On the client’s machine:
-
Set
WS_URLinclient_adapter.py, e.g.:WS_URL = "wss://your-relay.koyeb.app/client"
-
Run the adapter:
python client_adapter.py
-
Configure the client app to connect to the local TCP server exposed by the adapter, e.g.:
Address: 127.0.0.1 Port: 25565
From the client app’s perspective, it’s just talking to localhost. Under the hood, all traffic flows through the WebSocket relay.
Just as a concrete example (but remember: this works for any TCP app):
-
Server side:
- Minecraft server listening on
127.0.0.1:25565 server_adapter.pywith:MC_SERVER_HOST = "127.0.0.1" MC_SERVER_PORT = 25565 WS_URL = "wss://mc-tunnel-relay.koyeb.app/host"
- Minecraft server listening on
-
Client side:
client_adapter.pywith:LOCAL_HOST = "127.0.0.1" LOCAL_PORT = 25565 WS_URL = "wss://mc-tunnel-relay.koyeb.app/client"
- In Minecraft, simply add a server:
Address: 127.0.0.1 Port: 25565
Important:
Latency and jitter will depend on:
- the quality of your hosting provider
- the physical distance between client ↔ relay ↔ server
- how busy the free tier is (if you use free plans)
-
Security
- The tunnel itself has no auth, ACLs, or TLS handling (apart from what your hoster offers).
- Don’t use this as-is for sensitive production traffic. Add authentication, tokens, IP filtering, etc.
-
Latency
- Every extra hop (client ↔ relay ↔ server) and the WebSocket framing adds latency.
- For chat, status, casual gaming, and debugging it’s often fine.
- It’s not a replacement for a direct TCP connection if you need ultra-low, stable latency.
-
Reliability
- This is more of a learning / toy project than a battle-tested tunneling solution.
- No per-connection reconnection logic.
- No multi-client multiplexing – it’s designed for a single 1:1 tunnel at a time.
Pick any license that fits your use case (MIT, Apache-2.0, etc.).
Until then: use at your own risk 😄