A lightweight TCP proxy that routes connections based on the TLS SNI (Server Name Indication) header.
Lets you share a single port 443 between multiple services — MTProto proxies, Xray/VLESS/REALITY, Nginx, and anything else — distinguished only by their SNI hostname.
Client ──TLS──► :443 (sni-proxy)
│
├─ SNI = "proxy1.example.com" ──► 127.0.0.1:8441 (MTProto mtg)
├─ SNI = "proxy2.example.com" ──► 127.0.0.1:9443 (MTProto mtg #2)
├─ SNI = "vpn.example.com" ──► 127.0.0.1:7443 (Xray / 3x-ui)
├─ SNI = "*.example.com" ──► 127.0.0.1:8444 (wildcard)
└─ * (default) ──► 127.0.0.1:1443 (Nginx)
The proxy peeks at the first bytes of each TLS connection (ClientHello), extracts the SNI hostname, finds the matching backend, and replays the peeked bytes verbatim — traffic is forwarded transparently, with no TLS termination or decryption.
| Feature | Description |
|---|---|
| Exact match | proxy1.example.com → exact SNI match |
| Wildcard | *.example.com → any subdomain at any depth |
| Catch-all | * → all unmatched SNI values |
| Default backend | fallback when no catch-all route is defined |
| Hot-reload | kill -HUP <pid> reloads config without dropping connections |
| Graceful shutdown | drains active connections for up to 30 seconds |
| Debug logging | per-connection verbose logs |
| Docker | ready-to-use Dockerfile and docker-compose.yml |
| No TLS termination | traffic is never decrypted |
go build -o sni-proxy .Requirements: Go 1.21+
./sni-proxy -config config.yamlconfig.yaml in the current directory is used by default.
# Address and port to listen on
listen: ":443"
# Timeout for connecting to a backend
dial_timeout: 10s
# Timeout for reading the initial TLS ClientHello from the client
read_timeout: 5s
# Verbose per-connection logging
debug: false
# Fallback backend when no route matches
default: "127.0.0.1:1443"
routes:
# MTProto proxy #1 (mtg)
- sni: "proxy1.example.com"
backend: "127.0.0.1:8441"
comment: "MTProto proxy #1"
# MTProto proxy #2 (mtg)
- sni: "proxy2.example.com"
backend: "127.0.0.1:9443"
comment: "MTProto proxy #2"
# Xray / 3x-ui
- sni: "vpn.example.com"
backend: "127.0.0.1:7443"
comment: "Xray VLESS (3x-ui)"
# Wildcard — all subdomains of example.com
- sni: "*.example.com"
backend: "127.0.0.1:8444"
comment: "wildcard subdomain"
# Catch-all — everything else goes to Nginx
- sni: "*"
backend: "127.0.0.1:1443"
comment: "Nginx default"- Exact match —
sni: "proxy1.example.com" - Wildcard —
sni: "*.example.com" - Catch-all —
sni: "*" defaultfield at the root of the config
# /etc/systemd/system/sni-proxy.service
[Unit]
Description=SNI Proxy
After=network.target
[Service]
ExecStart=/usr/local/bin/sni-proxy -config /etc/sni-proxy/config.yaml
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload
systemctl enable --now sni-proxyReload config without restarting:
systemctl kill -s HUP sni-proxy
# or
kill -HUP $(pidof sni-proxy)docker compose up -dReload config:
docker kill --signal=HUP sni-proxyInternet
│
│ :443
▼
sni-proxy
├─ SNI=proxy1.example.com ──► mtg (MTProto) :8441
├─ SNI=proxy2.example.com ──► mtg (MTProto) :9443
├─ SNI=vpn.example.com ──► xray (3x-ui) :7443
└─ * ──► nginx :1443
mtg #1 listens on :8441 (internal port, blocked externally)
mtg #2 listens on :9443 (internal port, blocked externally)
xray listens on :7443 (internal port, blocked externally)
nginx listens on :1443 (internal port, blocked externally)
MTProto links are generated with server=<your-domain>&port=443 while the backend services listen on non-standard ports that are not reachable from the internet.
Set debug: true in the config to get verbose per-connection logging:
[proxy] listening on 0.0.0.0:443 (debug=true)
[proxy] proxy1.example.com -> 127.0.0.1:8441 # MTProto proxy #1
[proxy] * -> 127.0.0.1:1443 # Nginx default
[1.2.3.4:54321] accepted
[1.2.3.4:54321] peeked=517 bytes sni="proxy1.example.com"
[1.2.3.4:54321] SNI="proxy1.example.com" -> 127.0.0.1:8441
[1.2.3.4:54321] [SNI=proxy1.example.com] dial start backend=127.0.0.1:8441
[1.2.3.4:54321] [SNI=proxy1.example.com] dial success backend=127.0.0.1:8441
[1.2.3.4:54321] [SNI=proxy1.example.com] done tx=1.2KB rx=4.5KB dur=3.241s
.
├── main.go # entry point, OS signals, hot-reload
├── proxy.go # TCP listener, connection dispatcher, bidirectional copy
├── sni.go # TLS ClientHello parser, SNI extraction
├── config.go # YAML config loader, route lookup
├── config.yaml # example configuration
├── Dockerfile # multi-stage build
└── docker-compose.yml
MIT