Domain-based reverse proxy for VPS with single public IP. Routes TCP/UDP traffic to different local backends based on domain name using SNI (TLS), HTTP Host header, SMTP RCPT TO, and QUIC SNI extraction.
Client → VPS Public IP:<port>
│
├─ TLS ClientHello → Extract SNI → domain.com
├─ HTTP Request → Extract Host → domain.com
├─ SMTP → Extract RCPT TO → domain.com
└─ QUIC Initial → Extract SNI → domain.com
│
▼
Case-insensitive domain lookup
│
▼
Forward to backend at --localipv6 or --localipv4:<port>
| Protocol | Detection Method | Notes |
|---|---|---|
| HTTPS/TLS | TLS SNI | Works on any port |
| HTTP | Host header | Incremental read for large headers |
| SMTP | RCPT TO domain | Eater pattern for handshake |
| SMTPS/IMAPS/POP3S | TLS SNI | TLS-first protocols |
| SSH | Protocol banner | Port-based fallback (no domain) |
| DNS (UDP) | QNAME from query | UDP proxy |
| DNS (TCP) | 2-byte length + QNAME | TCP proxy |
| QUIC/HTTP3 | QUIC SNI | UDP-based |
# Clone and build
git clone https://github.com/kelvinzer0/shared-ip.git
cd shared-ip
go build -o shared-ip .
sudo cp shared-ip /usr/local/bin/
# Install as service
sudo shared-ip service install# 1. Add domain mapping
sudo shared-ip add myapp.com --localport=443 --localipv4=192.168.1.10
# 2. withfallback.com setup (IPv6-only VPS)
# CNAME your domain to <your-ipv6-addr>.withfallback.com
sudo shared-ip add myapp.com --localport=8080 --localipv6=::1
# 3. Dual-stack (IPv4 + IPv6 backends)
sudo shared-ip add myapp.com --localport=443 --localipv4=10.0.0.5 --localipv6=fd00::1
# 4. With auto HTTPS (certbot)
sudo shared-ip add myapp.com --localport=80 --localipv4=192.168.1.10 --certbot --certbot-email=you@email.com
# 5. Start
sudo service shared-ip startshared-ip add <domain> --localport=<port> --localipv4=<ip> [--localipv6=<ip>] [--certbot] [--certbot-email=<email>] [--certbot-staging] [--tls-terminate]
shared-ip list
shared-ip show <domain> --localport=<port>
shared-ip update <domain> --localport=<port> [--localipv4=<ip>] [--localipv6=<ip>] [--clear-ipv4] [--clear-ipv6]
shared-ip delete <domain> --localport=<port>
shared-ip reset
shared-ip daemon
shared-ip service <install|uninstall|start|stop|restart|status>
shared-ip version
| Option | Description |
|---|---|
--localport=<port> |
Port where your service listens. shared-ip also listens on this port (default: 80) |
--localipv4=<ip> |
Backend IPv4 address. Creates per-domain dummy interface (sip-<name>) |
--localipv6=<ip> |
Backend IPv6 address. Creates per-domain dummy interface |
--certbot |
Auto-obtain TLS certificate via Let's Encrypt (webroot mode) |
--certbot-email=<e> |
Email for Let's Encrypt notifications |
--certbot-staging |
Use Let's Encrypt staging environment (for testing) |
--tls-terminate |
Terminate TLS at proxy, forward plain TCP to backend (default: passthrough) |
--clear-ipv4 |
Remove IPv4 from mapping (update only) |
--clear-ipv6 |
Remove IPv6 from mapping (update only) |
# Web server on localhost
sudo shared-ip add app.com --localport=8080 --localipv6=::1
# Web server on specific IP
sudo shared-ip add app.com --localport=443 --localipv4=192.168.1.10
# Multiple domains, same backend
sudo shared-ip add a.com --localport=80 --localipv4=10.0.0.5
sudo shared-ip add b.com --localport=80 --localipv4=10.0.0.5
# Different backends per domain
sudo shared-ip add frontend.com --localport=80 --localipv4=10.0.0.5
sudo shared-ip add api.com --localport=80 --localipv4=10.0.0.6
# Auto HTTPS with certbot
sudo shared-ip add app.com --localport=80 --localipv4=10.0.0.5 --certbot --certbot-email=admin@app.com
# Auto HTTPS with TLS termination (proxy decrypts, backend receives plain HTTP)
sudo shared-ip add app.com --localport=8080 --localipv4=10.0.0.5 --certbot --tls-terminate
# Test with staging certs first
sudo shared-ip add app.com --localport=80 --localipv4=10.0.0.5 --certbot --certbot-staging
# Update mapping
sudo shared-ip update app.com --localport=80 --localipv4=10.0.0.99
# Delete mapping
sudo shared-ip delete app.com --localport=80
# View all mappings
sudo shared-ip listshared-ip can automatically obtain and configure TLS certificates from Let's Encrypt using certbot.
shared-ip add --certbotrequests a certificate via HTTP-01 challenge- The proxy serves ACME challenge tokens at
/.well-known/acme-challenge/on port 80 - Certificates are stored at
/etc/letsencrypt/live/<domain>/ - Certificate paths are saved in the config for automatic use
Passthrough (default): Proxy forwards raw TCP (including TLS) to backend. Backend handles TLS termination.
Client ──TLS──▶ Proxy ──TLS──▶ Backend:443
sudo shared-ip add app.com --localport=443 --localipv4=10.0.0.5 --certbot
# Backend nginx/caddy handles TLS on port 443Termination (--tls-terminate): Proxy terminates TLS, forwards plain TCP to backend.
Client ──TLS──▶ Proxy ──plain TCP──▶ Backend:8080
sudo shared-ip add app.com --localport=8080 --localipv4=10.0.0.5 --certbot --tls-terminate
# Backend receives plain HTTP on port 8080certbotinstalled (apt install certbotorsnap install certbot)- Port 80 accessible from the internet (for HTTP-01 challenge)
- Root privileges (for certificate storage)
Certificates auto-renew via certbot's systemd timer. To manually renew:
sudo certbot renew
sudo service shared-ip restart # reload certsmyapp.com A <VPS IPv4>
myapp.com AAAA <VPS IPv6>
myapp.com CNAME 2001-0db8-0000-0000-0000-0000-0000-0001.withfallback.com
With fallback: IPv4 clients connect through the proxy, IPv6 clients connect directly.
Like uvhost, the proxy reads data incrementally until the host is identified. This handles:
- HTTP requests where Host header spans multiple TCP segments
- TLS ClientHello larger than one MTU
- SMTP conversations where RCPT TO comes after multiple round-trips
DNS names are case-insensitive. Example.com and example.com match the same mapping.
For SMTP proxying (port 25), the proxy uses the "eater pattern" from uvhost:
- Send fake SMTP replies (220+250+250) to fast-forward through handshake
- Read client commands until RCPT TO → extract target domain
- Connect to backend, eat the server's replies (client already got them)
- Forward remaining traffic bidirectionally
Preserves the client's original source IP when connecting to the backend. The backend sees the client's IP, not the proxy's IP.
Requires root or CAP_NET_ADMIN. Falls back to normal dial if not available.
kill -HUP $(pidof shared-ip)- New process inherits listener file descriptors
- New process starts accepting on inherited listeners
- Old process waits for connections to drain, then exits
- No dropped connections during upgrade
Each domain gets its own dummy interface (sip-<name>):
sip-myapp-com → 192.168.1.10
sip-api-com → 10.0.0.5
Web servers can bind directly to the assigned IPs. Cleanup is per-domain.
TLS certificates are cached in memory after first load. No disk I/O per connection.
The proxy serves HTTP-01 challenge tokens inline — no separate web server needed for certbot. Works while the daemon is running.
sudo service shared-ip start
sudo service shared-ip stop
sudo service shared-ip restart
sudo service shared-ip status
# Or with systemctl:
sudo systemctl start shared-ip
sudo journalctl -u shared-ip -fConfig is stored at /etc/shared-ip/config.json:
[
{
"domain": "myapp.com",
"port": 8080,
"local_ipv4": "",
"local_ipv6": "::1",
"dummy_interface": "sip-myapp-com",
"cert_path": "/etc/letsencrypt/live/myapp.com/fullchain.pem",
"key_path": "/etc/letsencrypt/live/myapp.com/privkey.pem",
"tls_terminate": false
}
]sudo shared-ip service uninstall
sudo rm /usr/local/bin/shared-ipMIT