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

6.9 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.

There are two client-side flags:

  • tls = true — activates real TLS wrapping only (for the HAProxy TLS-termination model above)
  • [stealth] enabled = true — activates TLS and WebSocket camouflage (see WebSocket mode below)

Use tls = true when HAProxy terminates TLS for you. Use stealth = true when the server handles TLS directly and you need WebSocket framing to bypass DPI.

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)

For the HAProxy TLS-termination model (HAProxy decrypts TLS, konduit receives plain TCP):

[client]
server_endpoint   = "your-domain.com:443"
tls               = true
peer_id           = "..."
identity_key      = "..."
server_public_key = "..."

For WebSocket camouflage mode (konduit handles TLS and WebSocket end-to-end):

[client]
server_endpoint   = "your-domain.com:443"
peer_id           = "..."
identity_key      = "..."
server_public_key = "..."

[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 + WebSocket camouflage 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

WebSocket Camouflage (TSPU / Russia Bypass)

Some ISPs (notably Russia's TSPU system) allow real TLS but still drop connections whose payload doesn't look like HTTP. WebSocket camouflage solves this: konduit protocol runs inside WebSocket binary frames over HTTPS, which TSPU whitelists (Telegram Web, online games, and countless other services use the same pattern).

Client  ──TLS 1.3──▶  HAProxy :443 (TCP passthrough)  ──▶  Konduit :8443
                       HTTP/1.1 GET /ws
                       101 Switching Protocols
                       WS binary frames (konduit protocol inside)

HAProxy acts as a pure TCP proxy — it does not terminate TLS in this mode.

Server (server.toml)

[server]
listen_addr = "0.0.0.0"
listen_port = 8443
public_addr = "your-domain.com"
public_port = 443
tls         = true
tls_cert    = "/etc/letsencrypt/live/your-domain.com/fullchain.pem"
tls_key     = "/etc/letsencrypt/live/your-domain.com/privkey.pem"
websocket   = true

HAProxy (haproxy.cfg)

frontend https-ingress
    bind *:443
    mode tcp
    default_backend konduit-vpn

backend konduit-vpn
    mode tcp
    timeout tunnel 0
    server konduit 127.0.0.1:8443

timeout tunnel 0 disables HAProxy's idle-connection timeout for established tunnels. Without it, connections are killed after 30 seconds of silence.

Client (client.toml)

[client]
server_endpoint   = "your-domain.com:443"
peer_id           = "..."
identity_key      = "..."
server_public_key = "..."

[stealth]
enabled = true

stealth = true tells the client to use TLS and WebSocket framing. No other flags needed.


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 (HAProxy-TLS model) Handshake mismatch after HAProxy strips TLS Set [stealth] enabled = false on server
tls = true instead of stealth = true on client Connects but drops — no WebSocket framing Use [stealth] enabled = true for TSPU bypass
Missing timeout tunnel 0 in HAProxy Connection drops after 30 s of silence Add timeout tunnel 0 to backend
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