4.4 KiB
Stealth Mode: Real TLS Bypass via HAProxy
Stealth mode makes VPN traffic indistinguishable from normal HTTPS by wrapping it in real TLS with a valid certificate. Deep packet inspection sees a standard TLS 1.3 connection to a legitimate domain — not a VPN.
How It Works
- Client opens a TLS 1.3 connection to port 443 using the system's trusted certificate store — the same TLS stack as any browser.
- HAProxy terminates TLS using a real certbot certificate. It then inspects the
first byte of the decrypted payload to route traffic:
- HTTP method bytes (
GET,POST,HEAD,PUT,DELETE,OPTIONS) → web backend - Any other byte → konduit VPN backend
- HTTP method bytes (
- Konduit receives a plain TCP connection and runs its normal handshake. Stealth mode must be disabled on the server side — HAProxy already handled TLS.
The stealth = true and tls = true config flags are equivalent. Both activate TLS
wrapping on the client. Existing configs with stealth = true continue to work without
modification.
Server Requirements
- A domain with a valid TLS certificate (Let's Encrypt / certbot)
- HAProxy 2.4+
- Konduit server on a non-public port (e.g.
8443) - Web server on a local port (e.g.
8080) — for non-VPN HTTP requests
HAProxy Configuration
Combine the certificate and private key into a single PEM file for HAProxy:
cat /etc/letsencrypt/live/your-domain.com/fullchain.pem \
/etc/letsencrypt/live/your-domain.com/privkey.pem \
> /etc/haproxy/ssl/your-domain.pem
chmod 600 /etc/haproxy/ssl/your-domain.pem
frontend https-ingress
bind *:443 ssl crt /etc/haproxy/ssl/your-domain.pem
mode tcp
option tcplog
tcp-request inspect-delay 3s
# Accept as soon as the first byte arrives — VPN connections never send a FIN,
# so WAIT_END would stall every connection for the full inspect-delay.
tcp-request content accept if { req.payload(0,1) -m found }
use_backend web if { req.payload(0,3) -m str GET }
use_backend web if { req.payload(0,4) -m str POST }
use_backend web if { req.payload(0,4) -m str HEAD }
use_backend web if { req.payload(0,3) -m str PUT }
use_backend web if { req.payload(0,6) -m str DELETE }
use_backend web if { req.payload(0,7) -m str OPTIONS }
default_backend konduit-vpn
backend konduit-vpn
mode tcp
server konduit 127.0.0.1:8443
backend web
mode http
server web 127.0.0.1:8080
Reload after changes:
haproxy -c -f /etc/haproxy/haproxy.cfg # validate first
systemctl reload haproxy
Konduit Server (server.toml)
Disable stealth on the server — HAProxy terminated TLS before the connection arrives:
[server]
listen_addr = "0.0.0.0"
listen_port = 8443
public_addr = "your-domain.com"
public_port = 443
[stealth]
enabled = false
Bootstrap peers with --public-port so generated client configs reference port 443:
echo "your-mantra" | ./konduit-ctl bootstrap \
-l your-domain.com:8443 --public-port 443 -p -
Client Configuration (client.toml)
[client]
server_endpoint = "your-domain.com:443"
tls = true
peer_id = "..."
identity_key = "..."
server_public_key = "..."
The legacy flag is identical:
[stealth]
enabled = true
Flutter / Mobile App
Enable Stealth Mode in the app settings. The toggle maps to stealth = true in
the connection config and activates TLS wrapping on all platforms (Linux, Android,
Windows).
For QR-code-based provisioning, the t: true field in the QR payload enables stealth.
Verify
# A browser must see your real website — not a TLS error or empty response
curl -s https://your-domain.com/ | head -5
# Connect the VPN client — should stay connected without 20-second drops
./konduit --config client.toml
# HAProxy serves both roles: check access log
journalctl -u haproxy -f
Common Mistakes
| Mistake | Effect | Fix |
|---|---|---|
WAIT_END in inspect rule |
5-second stall on every connect | Use req.payload(0,1) -m found |
| Stealth enabled on server | Handshake mismatch after HAProxy strips TLS | Set [stealth] enabled = false |
| Port 8443 exposed to internet | Bypass HAProxy, no DPI camouflage | Firewall port 8443 to localhost only |
| Expired/self-signed cert | TLS error on client | Use Let's Encrypt; renew via certbot |