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.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.
Architecture
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 inobfs.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):
Packet Format
Each outbound WireGuard UDP packet is processed as follows (obfs.go):
-
RTP header (12 bytes) is prepended with:
V=2, P=1flags — RTP version 2, padding present (RFC 3550 compliant)- Payload type
111for audio mode (dynamic OPUS) or96for video mode (H.264) - Sequence number and timestamp that increment realistically across packets
- A random per-session SSRC (Synchronization Source identifier)
-
Nonce derivation — the 12-byte AEAD nonce is built from the RTP fields themselves, eliminating the need for a separate nonce prefix:
- 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.
- 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.
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_id | Path | Notes |
|---|---|---|
8093730 | VK Calls (primary default) | Used by api.vk.me endpoints |
6287487 | Legacy fallback | Primary credential set |
8202606 | Legacy fallback | Secondary credential set, used on rate-limit or failure |
creds_vkcalls.go, default):
api.vk.me/method/auth.getAnonymToken— obtain an anonymous token (app_id8093730)api.vk.me/method/messages.getCallPreview— validate the call join linkapi.vk.me/method/messages.getAnonymCallToken— obtain a per-call anonymous tokencalls.okcdn.ru/fb.do(auth.anonymLogin) — obtain an OK.ru session keycalls.okcdn.ru/fb.do(vchat.joinConversationByLink) — join the call and receive theturn_serverobject containing TURNusername,credential, and relay URLs
creds.go, fallback when VK Calls path fails):
login.vk.ru?act=get_anonym_token— obtain an anonymous access tokenapi.vk.ru/method/calls.getCallPreview— validate the call join linkapi.vk.ru/method/calls.getAnonymousToken— obtain a per-call anonymous token (with captcha handling if triggered)calls.okcdn.ru/fb.do(auth.anonymLogin) — obtain an OK.ru session keycalls.okcdn.ru/fb.do(vchat.joinConversationByLink) — join the call and receive TURN credentials
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 theturn_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: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.| Mode | Default workers | Maximum workers |
|---|---|---|
| Anonymous | 18 (qwdtt:// import) / 16 (wdtt:// import) | 108 |
| VK account | 4 (hard limit) | 4 |
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/udpusingpion/dtlswith 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) on10.66.66.1, assigning client IPs from the10.66.0.0/16subnet - 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 viakill -HUP $(pidof wdtt-server) - Telegram bot — responds to
/list(active passwords with traffic stats),/new(generate a new user password and return awdtt://link), and related admin commands
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.