Skip to content

igorhakk/sni-proxy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sni-proxy

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.


How it works

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.


Features

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

Quick start

Build from source

go build -o sni-proxy .

Requirements: Go 1.21+

Run

./sni-proxy -config config.yaml

config.yaml in the current directory is used by default.


Configuration

# 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"

Route matching priority

  1. Exact matchsni: "proxy1.example.com"
  2. Wildcardsni: "*.example.com"
  3. Catch-allsni: "*"
  4. default field at the root of the config

Deployment

Systemd (recommended for production)

# /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.target
systemctl daemon-reload
systemctl enable --now sni-proxy

Reload config without restarting:

systemctl kill -s HUP sni-proxy
# or
kill -HUP $(pidof sni-proxy)

Docker

docker compose up -d

Reload config:

docker kill --signal=HUP sni-proxy

Typical setup: one server, multiple services

Internet
   │
   │ :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.


Debug mode

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

Project structure

.
├── 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

License

MIT

About

A lightweight TCP proxy that routes connections based on the TLS SNI (Server Name Indication) header.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors