README
¶
SNI Proxy
A high-performance SNI (Server Name Indication) proxy implemented in Go that intelligently routes TLS connections to different backend servers based on the SNI hostname.
Features
- 🚀 SNI-based Routing - Route TLS connections to different backends based on the SNI hostname
- 🔄 Proxy Protocol Support - Preserve original client IP addresses using PROXY protocol v1/v2
- 🎯 Wildcard Matching - Support for wildcard rules (e.g.,
*.example.com) - ⚙️ Port-based Rules - Define routing rules per listening port
- 📝 YAML Configuration - Easy-to-read configuration format
- 🔒 TLS Transparent - Works with any TLS version (1.0-1.3)
- 🛡️ Graceful Fallback - Handle connections without SNI or with parsing errors
- ⚡ Zero-downtime - Concurrent connection handling with goroutines
How It Works
- Client initiates a TLS connection to the proxy
- Proxy reads the TLS ClientHello message
- Extracts the SNI (Server Name Indication) field
- Matches SNI against configured routing rules
- Forwards the connection to the appropriate backend server
- Optionally adds PROXY protocol header to preserve client IP
Installation
From Source
go install github.com/zamibd/sniproxy@latest
Build Locally
git clone https://github.com/zamibd/sniproxy.git
cd sniproxy
go build -o sniproxy .
Using Docker
Pull the pre-built image from Docker Hub:
# Pull the latest image
docker pull imzami/sniproxy:latest
# Run the container
docker run -d \
--name sniproxy \
-p 443:443 \
-v $(pwd)/config.yaml:/etc/sniproxy/config.yaml:ro \
imzami/sniproxy:latest
Or build locally:
docker build -t imzami/sniproxy .
docker run -v $(pwd)/config.yaml:/etc/sniproxy/config.yaml -p 443:443 imzami/sniproxy
Configuration
Create a config.yaml file:
# Default destination when no SNI match is found
# or TLS parsing fails
default: 127.0.0.1:8443
# Ports to listen on
# Can specify multiple ports
listen:
- 443
- 9999
# Forward rules
# Supports exact match and wildcard patterns
forward_rules:
# Exact match with PROXY protocol v2
www.example.com: 127.0.0.1:8443 proxy-v2
# Exact match with PROXY protocol v1
api.example.com: 10.0.0.10:8443 proxy-v1
# Exact match without PROXY protocol
blog.example.com: 10.0.0.20:443
# Wildcard match - forward based on SNI
# Example: SNI "app.example.com" from port 9999 → app.example.com:443
"*:9999": "*:443"
# Wildcard with specific backend
"*.internal.example.com": 192.168.1.100:443 proxy-v2
Usage
Basic Usage
# Use default config file (config.yaml)
./sniproxy
# Specify config file
./sniproxy -c /path/to/config.yaml
# With logging
./sniproxy -c config.yaml -log_level DEBUG -log_file /var/log/sniproxy.log
Command-line Options
-c string
Path to config file (default "config.yaml")
-log_file string
Path to log file (default: stdout)
-log_level string
Log level: DEBUG, INFO, WARN, ERROR (default "INFO")
Examples
Example 1: Multi-tenant HTTPS Routing
Route multiple domains to different backend servers:
default: 127.0.0.1:8080
listen:
- 443
forward_rules:
app1.example.com: 10.0.1.10:443
app2.example.com: 10.0.1.20:443
app3.example.com: 10.0.1.30:443
Example 2: Load Balancer with Client IP Preservation
Forward to backend with PROXY protocol to preserve client IPs:
default: 127.0.0.1:8443
listen:
- 443
forward_rules:
# Backend servers that support PROXY protocol
website.com: 10.0.0.10:443 proxy-v2
api.website.com: 10.0.0.20:443 proxy-v2
Example 3: Dynamic Wildcard Routing
Route based on subdomain patterns:
default: 127.0.0.1:8443
listen:
- 443
- 9999
forward_rules:
# Forward *.apps.example.com to the same hostname on port 8443
"*.apps.example.com": "*:8443"
# Port-specific wildcard: anything on port 9999 goes to port 443
"*:9999": "*:443"
Example 4: Kubernetes Ingress Alternative
Use as a simple TLS router for Kubernetes services:
default: default-backend.default.svc.cluster.local:443
listen:
- 443
forward_rules:
service1.example.com: service1.namespace1.svc.cluster.local:443 proxy-v2
service2.example.com: service2.namespace2.svc.cluster.local:443 proxy-v2
"*.staging.example.com": "*-staging.default.svc.cluster.local:443"
PROXY Protocol
SNI Proxy supports PROXY protocol v1 and v2 to preserve the original client IP address when forwarding to backend servers.
Backend Configuration
Your backend servers need to support PROXY protocol:
nginx:
server {
listen 443 ssl proxy_protocol;
real_ip_header proxy_protocol;
set_real_ip_from 10.0.0.0/8;
# ...
}
HAProxy:
frontend https
bind :443 ssl crt /path/to/cert.pem accept-proxy
Go (stdlib):
import "github.com/pires/go-proxyproto"
listener, _ := net.Listen("tcp", ":443")
proxyListener := &proxyproto.Listener{Listener: listener}
Testing
Run the test suite:
# Run all tests
go test -v ./...
# Run specific test
go test -v -run TestProxyProto
# Run tests with coverage
go test -v -cover ./...
Performance
- Concurrent Connections: Handles thousands of concurrent connections using goroutines
- Low Latency: Minimal overhead - only reads TLS ClientHello
- Memory Efficient: Streaming proxying with configurable buffer sizes
- Zero Configuration SSL: No SSL certificates needed on the proxy itself
Deployment
Systemd Service
Create /etc/systemd/system/sniproxy.service:
[Unit]
Description=SNI Proxy
After=network.target
[Service]
Type=simple
User=sniproxy
ExecStart=/usr/local/bin/sniproxy -c /etc/sniproxy/config.yaml -log_level INFO
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl enable sniproxy
sudo systemctl start sniproxy
Docker
Pre-built Image
The official Docker image is available on Docker Hub:
docker pull imzami/sniproxy:latest
Available tags:
latest- Latest stable release from master branchv1.0.0,v1.0,v1- Semantic version tagsmain-<sha>- Specific commit from main branch
Building the Image
Build the optimized Docker image (uses multi-stage build with Go 1.24):
# Build with default tag
docker build -t imzami/sniproxy:latest .
# Build with specific version tag
docker build -t imzami/sniproxy:v1.0.0 .
# Build for multiple architectures
docker buildx build --platform linux/amd64,linux/arm64 -t imzami/sniproxy:latest .
Running with Docker
Basic usage:
docker run -d \
--name sniproxy \
-p 443:443 \
-v $(pwd)/config.yaml:/etc/sniproxy/config.yaml:ro \
imzami/sniproxy:latest
With custom config and multiple ports:
docker run -d \
--name sniproxy \
-p 443:443 \
-p 9999:9999 \
-v $(pwd)/config.yaml:/etc/sniproxy/config.yaml:ro \
-v $(pwd)/logs:/var/log/sniproxy \
imzami/sniproxy:latest -log_level INFO -log_file /var/log/sniproxy/sniproxy.log
View logs:
docker logs -f sniproxy
Stop and remove:
docker stop sniproxy
docker rm sniproxy
Docker Compose
Create a docker-compose.yml file for easy orchestration:
Basic setup:
version: '3.8'
services:
sniproxy:
image: imzami/sniproxy:latest
container_name: sniproxy
ports:
- "443:443"
- "9999:9999"
volumes:
- ./config.yaml:/etc/sniproxy/config.yaml:ro
restart: unless-stopped
networks:
- proxy-net
networks:
proxy-net:
driver: bridge
Production setup with health checks and logging:
services:
sniproxy:
image: imzami/sniproxy:latest
container_name: sniproxy
ports:
- "443:443"
- "9999:9999"
volumes:
- ./config.yaml:/etc/sniproxy/config.yaml:ro
- ./logs:/var/log/sniproxy
restart: always
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- proxy-net
# Health check (adjust port as needed)
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "443"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
proxy-net:
driver: bridge
Start services:
# Start in foreground
docker compose up
# Start in background
docker compose up -d
# View logs
docker compose logs -f
# Stop services
docker compose down
# Rebuild and restart
docker compose up -d --build
Docker Networking
Bridge network (default):
# Backend services on same Docker network
forward_rules:
app.example.com: backend-service:443
api.example.com: api-service:8443
Host network (better performance):
docker run -d \
--name sniproxy \
--network host \
-v $(pwd)/config.yaml:/etc/sniproxy/config.yaml:ro \
imzami/sniproxy:latest
Custom network:
# Create network
docker network create --driver bridge sni-network
# Run with custom network
docker run -d \
--name sniproxy \
--network sni-network \
-p 443:443 \
-v $(pwd)/config.yaml:/etc/sniproxy/config.yaml:ro \
imzami/sniproxy:latest
Docker Image Details
The optimized Dockerfile provides:
- Minimal Size: ~2MB final image using
FROM scratch - Security: Runs as non-root user (UID 65534)
- Static Binary: No external dependencies required
- Multi-stage Build: Separates build and runtime environments
- Layer Caching: Optimized for fast rebuilds
- Multi-architecture: Supports both amd64 and arm64
Official Image: imzami/sniproxy:latest
Image structure:
imzami/sniproxy:latest
├── /bin/sniproxy # Static binary (~2MB)
├── /etc/ssl/certs/ # CA certificates
├── /usr/share/zoneinfo/ # Timezone data
└── /etc/sniproxy/config.yaml # Default config (override with volume)
Kubernetes Deployment
Deploy SNI Proxy in Kubernetes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: sniproxy
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: sniproxy
template:
metadata:
labels:
app: sniproxy
spec:
containers:
- name: sniproxy
image: imzami/sniproxy:latest
ports:
- containerPort: 443
protocol: TCP
volumeMounts:
- name: config
mountPath: /etc/sniproxy
readOnly: true
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
volumes:
- name: config
configMap:
name: sniproxy-config
---
apiVersion: v1
kind: Service
metadata:
name: sniproxy
namespace: default
spec:
type: LoadBalancer
ports:
- port: 443
targetPort: 443
protocol: TCP
selector:
app: sniproxy
---
apiVersion: v1
kind: ConfigMap
metadata:
name: sniproxy-config
namespace: default
data:
config.yaml: |
default: 127.0.0.1:8443
listen:
- 443
forward_rules:
service1.example.com: service1.default.svc.cluster.local:443
service2.example.com: service2.default.svc.cluster.local:443
Troubleshooting
Enable Debug Logging
./sniproxy -c config.yaml -log_level DEBUG
Common Issues
Connection refused:
- Check if backend servers are accessible
- Verify firewall rules allow connections
SNI not extracted:
- Ensure clients send SNI in TLS handshake
- Check TLS version compatibility (some old clients don't support SNI)
PROXY protocol errors:
- Verify backend supports PROXY protocol
- Check PROXY protocol version (v1 vs v2)
- Ensure
proxy-v1orproxy-v2is specified in config
Architecture
Client → SNI Proxy → Backend Server
↓
1. Read TLS ClientHello
2. Extract SNI
3. Match routing rules
4. Add PROXY header (optional)
5. Forward connection
Security Considerations
- SNI Proxy does not decrypt TLS traffic - it only reads the unencrypted ClientHello
- Ensure backend servers validate TLS certificates
- Use PROXY protocol only on trusted internal networks
- Consider rate limiting at firewall level
- Monitor for unusual traffic patterns
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the terms specified in the LICENSE file.
Credits
Based on the original sniproxy implementation with improvements for stability and features.
Support
For bugs and feature requests, please create an issue on the GitHub repository.
Documentation
¶
There is no documentation for this package.