Files
konduit-public/docs/stealth-setup.md

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.

Stealth mode architecture

How It Works

  1. Client opens a TLS 1.3 connection to port 443 using the system's trusted certificate store — the same TLS stack as any browser.
  2. 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
  3. 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