Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/SpaceNeuroX/proxy-turn-vk-android/llms.txt

Use this file to discover all available pages before exploring further.

Many mobile carriers apply deep-packet inspection (DPI) and traffic whitelists that block conventional VPN protocols — WireGuard, OpenVPN, and similar — while allowing media streams from popular services such as VK. qWDTT exploits this asymmetry: it tunnels WireGuard inside VK’s own TURN relay infrastructure, wrapping every UDP datagram in RTP frames that are indistinguishable from a live WebRTC audio or video call. From the carrier’s perspective, the device is participating in a VK group call, not running a VPN.

Architecture

Android Phone              VK TURN Relay               Your VPS
┌─────────────┐  RTP+WRAP   ┌──────────┐  RTP+WRAP+DTLS  ┌─────────────┐
│ WireGuard   │ ──/DTLS───► │  relay   │ ───────────────►│ wdtt-server │
│ Go client   │             │  (VK)    │                 │ WireGuard   │
│ 127.0.0.1   │             └──────────┘                 └─────────────┘
└─────────────┘
The Android app runs two components side by side: the system WireGuard kernel module (via VpnService + GoBackend) listening on 127.0.0.1, and a native Go client compiled as libclient.so. The Go client lifts UDP datagrams off the local WireGuard socket, applies WRAP obfuscation, and sends them through a DTLS connection to a VK TURN relay. The relay forwards the stream to wdtt-server on the VPS, which reverses the process and hands clean WireGuard packets to its own WireGuard interface.

WRAP Obfuscation

WRAP is the packet-level obfuscation layer that makes WireGuard UDP datagrams look like RTP media frames. It is implemented in obfs.go and wrap.go in the Go client.

Key Derivation

The WRAP encryption key is derived deterministically from the connection password using HKDF-SHA256 (wrap.go):
reader := hkdf.New(
    sha256.New,
    []byte(password),       // IKM  — the connection password
    []byte("WDTT-WRAP-v1"), // salt
    []byte("rtp-obfs/chacha20poly1305"), // info
)
This produces a 32-byte ChaCha20-Poly1305 key. Because the key is password-derived, a packet cannot be decrypted without knowing the connection password — even by someone who can observe the TURN relay traffic.

Packet Format

Each outbound WireGuard UDP packet is processed as follows (obfs.go):
  1. RTP header (12 bytes) is prepended with:
    • V=2, P=1 flags — RTP version 2, padding present (RFC 3550 compliant)
    • Payload type 111 for audio mode (dynamic OPUS) or 96 for video mode (H.264)
    • Sequence number and timestamp that increment realistically across packets
    • A random per-session SSRC (Synchronization Source identifier)
  2. Nonce derivation — the 12-byte AEAD nonce is built from the RTP fields themselves, eliminating the need for a separate nonce prefix:
    [SSRC 4B][SeqNum 2B][0x00 0x00][Timestamp 4B]
    
  3. Encryption — the WireGuard payload is encrypted with ChaCha20-Poly1305, using the RTP header as Additional Authenticated Data (AAD). The AEAD tag (16 bytes) is appended immediately after the ciphertext.
  4. Random padding — between 0 and 23 bytes (audio mode) or 0 and 59 bytes (video mode) of random data are appended, followed by a one-byte padding length field (RFC 3550 §5.1). This varies packet sizes naturally, matching the statistical profile of real media streams.
The final wire format is:
[RTP Header 12B][Ciphertext][AEAD Tag 16B][Random Padding 0–N B][PadLen 1B]
On the receive side (obfsUnwrapPacket), the server strips the RTP header, removes padding, reconstructs the nonce, and decrypts the payload — recovering the original WireGuard datagram.
The RTP payload type check (pt == 111 || pt == 96) is used by both the client and server to quickly reject non-WRAP packets before attempting decryption, preventing amplification of malformed input.

DTLS Transport

The WRAP layer does not operate over raw UDP — it runs inside a DTLS (Datagram TLS) connection established between the Go client and the VK TURN relay. DTLS provides a second layer of encryption and authentication for the relay leg of the journey. From the relay’s perspective, it sees a standard DTLS session carrying RTP-shaped datagrams, which is exactly what a WebRTC client producing media would send. The server (server.go) also terminates a DTLS connection on 56000/udp, using the pion/dtls library with a self-signed certificate. The relay forwards the DTLS-wrapped stream; wdtt-server unwraps DTLS, then unwraps WRAP/RTP, and delivers raw WireGuard packets to the local wdtt0 WireGuard interface.

VK TURN Credential Acquisition

To connect to a VK TURN relay, the client must present short-lived credentials issued by VK for the specific group call identified by its hash. The Go client (creds.go and creds_vkcalls.go) supports two modes for obtaining these credentials.

Anonymous Mode (Default)

The client makes a multi-step API call chain mimicking a real VK mobile app. By default it uses the VK Calls path (creds_vkcalls.go), which contacts api.vk.me with app_id 8093730. If that path fails it falls back to the legacy path (creds.go), which rotates between two stable app_id / client_secret pairs:
app_idPathNotes
8093730VK Calls (primary default)Used by api.vk.me endpoints
6287487Legacy fallbackPrimary credential set
8202606Legacy fallbackSecondary credential set, used on rate-limit or failure
VK Calls path (creds_vkcalls.go, default):
  1. api.vk.me/method/auth.getAnonymToken — obtain an anonymous token (app_id 8093730)
  2. api.vk.me/method/messages.getCallPreview — validate the call join link
  3. api.vk.me/method/messages.getAnonymCallToken — obtain a per-call anonymous token
  4. calls.okcdn.ru/fb.do (auth.anonymLogin) — obtain an OK.ru session key
  5. calls.okcdn.ru/fb.do (vchat.joinConversationByLink) — join the call and receive the turn_server object containing TURN username, credential, and relay URLs
Legacy path (creds.go, fallback when VK Calls path fails):
  1. login.vk.ru?act=get_anonym_token — obtain an anonymous access token
  2. api.vk.ru/method/calls.getCallPreview — validate the call join link
  3. api.vk.ru/method/calls.getAnonymousToken — obtain a per-call anonymous token (with captcha handling if triggered)
  4. calls.okcdn.ru/fb.do (auth.anonymLogin) — obtain an OK.ru session key
  5. calls.okcdn.ru/fb.do (vchat.joinConversationByLink) — join the call and receive TURN credentials
All requests use a Chrome 146 TLS fingerprint (tls-client with profiles.Chrome_146) and realistic browser headers to avoid TLS fingerprinting detection. Inter-request delays (100–400 ms, randomised) prevent rate-limiting. If VK returns a captcha challenge on the legacy path, the client runs an automatic solver chain: Go-based reCAPTCHA v2 solver (up to 2 attempts) → automatic WebView solver (up to 2 attempts) → final Go attempt → manual WebView fallback.

Account Mode

When the user has signed in to VK via the in-app WebView, the client intercepts the turn_server response from the live call page directly, bypassing the anonymous API chain entirely.
VK account mode is limited to 4 workers maximum per session. This is a quota imposed by VK’s TURN relay infrastructure (~4 relay allocations per call session). The Go client enforces this limit automatically and logs a warning if a higher worker count is requested.

Credential Caching and Renewal

TURN credentials have a lifetime of approximately 9–10 minutes. The client caches credentials per stream group and renews them transparently before expiry:
credentialLifetime  = 10 minutes
cacheSafetyMargin   =  1 minute  (renew at ~9 min)
Multiple workers sharing the same cache ID reuse a single credential fetch, serialised with a 3–6 second inter-request throttle to avoid VK rate limits. If consecutive authentication errors are detected (3+ within a 10-second window), the cache is invalidated and a fresh credential fetch is triggered.

Worker Model

The Go client spawns a configurable number of concurrent DTLS workers, each maintaining an independent connection through the TURN relay. Workers are organised into groups that start sequentially (each group waits for the previous group’s first worker to become ready before launching), preventing a thundering-herd of simultaneous DTLS handshakes toward VK.
ModeDefault workersMaximum workers
Anonymous18 (qwdtt:// import) / 16 (wdtt:// import)108
VK account4 (hard limit)4
Up to 4 VK call hashes can be specified per profile. Workers are distributed across hashes to spread load. With 4 hashes in anonymous mode and the 108-worker cap, a single profile can sustain dozens of parallel DTLS streams through VK’s relay network simultaneously. Each worker reads WireGuard UDP datagrams from the local socket (127.0.0.1:9000 by default), applies WRAP obfuscation, and forwards them through its DTLS/TURN channel. Inbound DTLS/WRAP packets are unwrapped and written back to the local socket for the WireGuard kernel module to process.

Server Side

wdtt-server (compiled from server.go) is the counterpart to the Go client running on Android. It handles:
  • DTLS termination on 0.0.0.0:56000/udp using pion/dtls with a self-signed certificate
  • WRAP/RTP unwrapping — strips the RTP header, removes padding, and decrypts each payload with ChaCha20-Poly1305 using the connecting client’s password-derived key
  • WireGuard interface (wdtt0) on 10.66.66.1, assigning client IPs from the 10.66.0.0/16 subnet
  • Password database at /etc/wdtt/passwords.json — supports a main password and up to 10 per-user passwords with optional expiry timestamps, device binding (device_id), and traffic accounting. Changes take effect without restart via kill -HUP $(pidof wdtt-server)
  • Telegram bot — responds to /list (active passwords with traffic stats), /new (generate a new user password and return a wdtt:// link), and related admin commands
The internal WireGuard port (56001/udp) is used only for the loopback between wdtt-server and the wdtt0 WireGuard interface on the VPS itself; clients never connect to it directly.
Traffic counters (down_bytes, up_bytes) are persisted per password entry and per device in /etc/wdtt/passwords.json. The Telegram bot surfaces these values in /list output, giving administrators per-user traffic visibility without any additional monitoring infrastructure.

Build docs developers (and LLMs) love