SNI-based TLS reverse proxy with termination and passthrough modes
Project description
tls-switch
A TLS reverse proxy that routes incoming TLS connections to backend servers based on the requested hostname using SNI (Server Name Indication). Supports both TLS termination and TLS passthrough on a per-host basis.
tls-switch sits in front of your services on port 443 and inspects the TLS ClientHello to determine which hostname the client is requesting. SNI is a TLS extension that allows the client to indicate which hostname it is trying to connect to before the TLS handshake completes — this is how tls-switch knows where to route the connection without needing a separate IP address per service. It then routes the connection to the appropriate backend, either terminating TLS and forwarding plaintext, or passing the encrypted stream through unmodified.
Features
- SNI-based routing — route TLS connections to different backends based on hostname
- TLS termination — terminate TLS with your certificates and forward plaintext to backends
- TLS passthrough — forward the raw TLS stream to a backend that handles its own TLS
- Hot reload — config and certificate changes take effect on new connections without interrupting existing ones
- Zero buffering — data is forwarded immediately with no processing, filtering, or modification
- PROXY protocol — optional v1/v2 header emission per host so backends can see the original client IP
- Zero runtime dependencies — single statically-linked Go binary, no Python or libc required at runtime
Use Cases
- Run multiple HTTPS services on a single IP address, each with its own certificate
- Put a TLS-terminating proxy in front of plain HTTP services
- Route some domains through to their own TLS servers while terminating others locally
- Consolidate port 443 across multiple services without a full reverse proxy
Requirements
- Python 3.12+ (only required to install from PyPI; the binary itself has no runtime dependencies)
- Root/administrator privileges (if binding to port 443)
Installation
pip install tls-switch
Or run directly with uv:
uvx tls-switch
Quick Start
Create a config file (config.json):
{
"listen": ":443",
"hosts": {
"app.example.com": {
"mode": "terminate",
"cert": "/etc/tls-switch/app.example.com.crt",
"key": "/etc/tls-switch/app.example.com.key",
"backend": "127.0.0.1:8080"
},
"legacy.example.com": {
"mode": "passthrough",
"backend": "10.0.0.5:443",
"proxy_protocol": "v2"
}
}
}
Run the server:
tls-switch -c config.json
In this example:
- Connections to
app.example.comhave TLS terminated by tls-switch, and plaintext HTTP is forwarded to127.0.0.1:8080 - Connections to
legacy.example.comare forwarded as raw TLS to10.0.0.5:443, which handles its own certificates
How It Works
- A client connects to port 443 and begins a TLS handshake
- tls-switch reads the TLS ClientHello message and extracts the SNI (Server Name Indication) hostname
- The hostname is looked up in the configuration
- Depending on the mode:
- terminate: tls-switch completes the TLS handshake using the configured certificate and key, then opens a plaintext TCP connection to the backend and copies data bidirectionally with no buffering or processing
- passthrough: tls-switch opens a TCP connection to the backend, replays the original ClientHello, and then copies data bidirectionally — the backend server handles the TLS handshake itself
- If the hostname is not found in the configuration, tls-switch completes a TLS handshake using any available configured certificate and returns an HTTP 421 Misdirected Request error page — browsers display a clear error rather than a cryptic "can't connect" message
Configuration
Config File
The config file is JSON with the following structure:
{
"listen": ":443",
"hosts": {
"hostname": {
"mode": "terminate|passthrough",
"cert": "/path/to/cert.pem",
"key": "/path/to/key.pem",
"backend": "host:port",
"proxy_protocol": "v1|v2"
}
}
}
| Field | Description |
|---|---|
listen |
Address to listen on (e.g. :443, 0.0.0.0:8443) |
hosts |
Map of hostname to route configuration |
mode |
terminate (TLS termination) or passthrough (forward raw TLS) |
cert |
Path to PEM certificate file (terminate mode only) |
key |
Path to PEM private key file (terminate mode only) |
backend |
Backend address as host:port |
proxy_protocol |
Optional. v1 (text) or v2 (binary) to emit a PROXY protocol header to the backend so it sees the original client IP. Works in both modes. The backend must be configured to expect it, and only to trust PROXY headers from the tls-switch listener address — otherwise clients can spoof their source IP. Omit to disable (default). |
Hot Reload
tls-switch watches the config file and certificate files for changes. When a change is detected:
- The new config is validated
- If valid, new connections use the updated config
- Existing connections continue with their original config until they close naturally
- If invalid, the change is rejected and the current config remains active
Development
# Set up development environment
make dev
# Cross-compile Go binaries
make go-build
# Run format check and lint
make check
# Auto-format code
make format
# Build wheel and docs
make build
Architecture
tls-switch is a single statically-linked Go binary with no runtime dependencies. It bundles:
- TCP listener and accept loop
- TLS ClientHello parsing and SNI extraction
- TLS termination via the Go
crypto/tlsstandard library - Raw passthrough using
io.Copyfor zero-copy forwarding where supported by the OS - Optional PROXY protocol v1/v2 header emission to the backend
- Config + certificate file watching for hot reload
- Coloured terminal logging
The binary is built with CGO_ENABLED=0 and works across macOS, Linux, and Windows on amd64 + arm64. It is distributed as platform-specific Python wheels for ease of installation via pip / uvx, but no Python is required to run it.
Licence
Released under the Unlicense — public domain.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tls_switch-1.1.0-py3-none-win_arm64.whl.
File metadata
- Download URL: tls_switch-1.1.0-py3-none-win_arm64.whl
- Upload date:
- Size: 2.0 MB
- Tags: Python 3, Windows ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
314b18fc1fbfcd40f046548ff94f925eefae767f150eac03df018a3e774d5b39
|
|
| MD5 |
ad7dd2f5f92f8c6938df66bdfbc35bfb
|
|
| BLAKE2b-256 |
dba980a6c42128b91b626acbe807af51f379e71a605c4cda7422d31e2e4fb3c6
|
File details
Details for the file tls_switch-1.1.0-py3-none-win_amd64.whl.
File metadata
- Download URL: tls_switch-1.1.0-py3-none-win_amd64.whl
- Upload date:
- Size: 2.3 MB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9903ad2eb42d6d8f45157b639e65c5dcc5f9410ae28bd42242916cb855d22105
|
|
| MD5 |
2c3d5c8652e6bc129f73fdbd0d9479b2
|
|
| BLAKE2b-256 |
fc12613903770ed836626139c928ffb32223edd6535c9576e57fda83d4a77185
|
File details
Details for the file tls_switch-1.1.0-py3-none-manylinux_2_17_x86_64.whl.
File metadata
- Download URL: tls_switch-1.1.0-py3-none-manylinux_2_17_x86_64.whl
- Upload date:
- Size: 2.2 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c1c5c2210be48da16ab8f7d250265dd036a503498f5eee91af453faa47be7a0e
|
|
| MD5 |
1abd6e33af30a2449361770f53cdbf9b
|
|
| BLAKE2b-256 |
77ea9337c223bbfed71df2ba93c29c331de2f16bea9528b3701ded3b8886dd8f
|
File details
Details for the file tls_switch-1.1.0-py3-none-manylinux_2_17_aarch64.whl.
File metadata
- Download URL: tls_switch-1.1.0-py3-none-manylinux_2_17_aarch64.whl
- Upload date:
- Size: 2.0 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1fb4e5b374279efa0ff7d7330ae3806f0257df44a14253c39a319aec1bd75ae8
|
|
| MD5 |
3a153536a5e537183c108f7d8ee0fef8
|
|
| BLAKE2b-256 |
d815021e126cac4d2f3e4fec71ae45c0528fb046e565f9214bc1bb63c77cc7f1
|
File details
Details for the file tls_switch-1.1.0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: tls_switch-1.1.0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.1 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0111fed7df696feeca9ef1b8624577adf4bc938ea757c7bfded9e0be49d7fbd5
|
|
| MD5 |
addddc486f1c26b392811f425077fdd4
|
|
| BLAKE2b-256 |
d16427881b7e46b67b77823b06a3a343fd38001c530783bc0c04bc1b474096e4
|
File details
Details for the file tls_switch-1.1.0-py3-none-macosx_10_9_x86_64.whl.
File metadata
- Download URL: tls_switch-1.1.0-py3-none-macosx_10_9_x86_64.whl
- Upload date:
- Size: 2.3 MB
- Tags: Python 3, macOS 10.9+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57feca7792a562093dbbe836d5933284e96869b298f393532eb30c72d73a9003
|
|
| MD5 |
a6675ae790f49847c409817246b4d12f
|
|
| BLAKE2b-256 |
6fd9bca14a82e94b65ac2870c4a666ed26e0f119a9ab56b59d3a6cee742d6ab6
|