WebSocket Tunnel for Telegram (ripdpi-ws-tunnel)

SkillCommunication

Use when modifying the MTProto WebSocket tunnel for Telegram in ripdpi-ws-tunnel, Telegram DC IP routing (149.154.* / 91.108.* etc.), MTProto obfuscated2 classification, kws*.web.telegram.org bootstrap and TLS handshake, choosing between rustls and BoringSSL backends, the AvoidsBlocked517ByteClientHello invariant, or the telegram_availability diagnostics probe. Triggers on "Telegram", "MTProto", "ws tunnel", "WsTunnelMode", "ClientHello fingerprint", or Telegram-DC routing questions.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the WebSocket Tunnel for Telegram (ripdpi-ws-tunnel) skill

What this skill tells your AI

The instructions your AI receives, as published by po4yka/ripdpi in .agents/skills/ws-tunnel-telegram/SKILL.md and read by ahel’s review.

1. Purpose

middlebox/DPI blocks Telegram by detecting MTProto signatures on direct TCP to known DC IPs. The WS tunnel wraps MTProto inside WSS to kws*.web.telegram.org, so DPI sees standard TLS + HTTP Upgrade rather than raw MTProto.

WsTunnelMode lives in ripdpi-config/src/model/group.rs; the adaptive runtime setting lives in model/runtime.rs:

  • Off -- disabled (default)
  • Always -- every Telegram connection uses WS tunnel immediately
  • Fallback -- try desync first; escalate to WS tunnel only on failure

2. Architecture

App -> TUN -> ripdpi-proxy -> handshake pipeline
  1. detect_telegram_dc(target_ip)          -- dc.rs: dc_from_ip
  2. classify_target(target_ip)             -- lib.rs: WsTunnelDecision::Tunnel(dc)
  3. read 64-byte MTProto init from client  -- ws_tunnel.rs: read_mtproto_seed
  4. classify_mtproto_seed(init)            -- mtproto.rs: ValidatedMtproto{dc}
  5. resolve_ws_tunnel_addr(dc)             -- ws_bootstrap.rs: encrypted DNS
  6. open_ws_tunnel(dc, resolved_addr)      -- connect.rs: TLS + WS handshake
  7. ws_relay(client, ws, seed_request)     -- relay.rs: bidirectional relay

Runtime entry points (ripdpi-proxy-runtime/src/runtime/handshake/ws_tunnel.rs):

  • should_ws_tunnel_first() -- WsTunnelMode::Always + known Telegram IP
  • should_ws_tunnel_fallback() -- WsTunnelMode::Fallback + known Telegram IP
  • run_ws_tunnel() / run_ws_tunnel_with_seed() -- classify, resolve, relay

WsTunnelResult enum: ValidatedMtproto, NotMtproto, UnmappableDc, ShortInit, BootstrapFailed, WsOpenOrRelayFailed.

3. Telegram DC Database (dc.rs)

Types: TelegramDc (number/raw/class), TelegramDcClass (Production/Test/MediaOrCdn). TelegramDc::from_raw(): 1-5 = Production, 10001-10005 = Test, -1..-5 = MediaOrCdn. is_tunnelable() returns false for MediaOrCdn (no WS gateway).

IP ranges (dc_from_ip): maps IPv4 by first two octets:

Prefix3rd octetDCPrefix3rd octetDC
149.154160-163191.10856-595
149.154164-167291.1088-113
149.154168-171391.10812-154
149.154172-175191.108fallback2
149.154fallback291.105.*--2
185.76.*--2

Unrecognized subnets within Telegram prefixes fall back to DC2 intentionally -- unknown-but-Telegram traffic should be tunneled rather than leaked as raw MTProto. IPv6 always returns None.

WS endpoints: ws_host() -> kws{n}.web.telegram.org (or kws{n}-test.*), ws_url() -> wss://kws{n}.web.telegram.org/apiws. MediaOrCdn returns None.

4. MTProto Handling (mtproto.rs)

The proxy intercepts all TCP to Telegram IPs, but not all is MTProto (HTTPS API, media downloads, etc). The 64-byte obfuscated2 init has a distinctive structure for reliable classification without false positives.

Pipeline: classify_mtproto_seed(seed) ->

  1. has_valid_encrypted_prefix(init) -- rejects 0xef first byte, TLS prefix, HTTP methods (GET/HEAD/POST/OPTI), transport tags (0xdddddddd/eeeeeeee/efefefef)
  2. decrypt_init_packet(init) -- AES-256-CTR: key=[8..40], IV=[40..56]
  3. has_allowed_protocol_tag(decrypted) -- bytes [56..60]: [0xdd;4]/[0xee;4]/[0xef;4]
  4. DC extraction from bytes [60..64] as i32 LE -> TelegramDc::from_raw()

Results: ValidatedMtproto{dc} (tunnelable), NotMtproto (prefix/tag fail), UnmappableDc{raw_dc, dc} (valid MTProto but media/CDN or unknown DC).

5. VPN Socket Protection (protect.rs)

On Android, outgoing sockets must be "protected" to prevent VPN routing loops. protect_socket(socket, path) sends the socket FD via SCM_RIGHTS over a Unix socket to the Java VpnService which calls VpnService.protect(fd). Compiled only on linux/android; returns Unsupported elsewhere. Path flows from RuntimeConfig::process::protect_path -> WsTunnelConfig::protect_path -> connect_tcp_socket().

6. TLS Backend (connect.rs)

Two backends via chrome-fingerprint feature flag:

  • Default (rustls): WebSocket<StreamOwned<ClientConnection, TcpStream>>, webpki-roots, ALPN http/1.1, explicit handshake loop
  • chrome-fingerprint (BoringSSL): WebSocket<SslStream<TcpStream>>, Chrome-native ClientHello defeating JA3/JA4 fingerprinting

Why two backends (2026 perspective)

The ripdpi-tls-profiles crate (wrapping boring) is the fingerprint-sensitive path. The rustls path is the standard-TLS path where fingerprinting isn't a concern (DNS-over-TLS bootstrap, host-verification-only targets). Rationale as of April 2026:

  • rustls narrowed the perf gap: the Q1 2026 Prossimo benchmark showed rustls handshake throughput within ~5% of BoringSSL on x86_64. See https://www.memorysafety.org/blog/26q1-rustls-performance/. The historical perf argument for BoringSSL is weak.
  • TLS fingerprint mimicry still requires BoringSSL: no Rust crate (as of April 2026) offers byte-precise ClientHello construction for JA3/JA4 mimicry. The ripdpi-tls-profiles crate wraps SSL_CTX at the BoringSSL C level to control extension order, GREASE values, and cipher suite ordering. When a uTLS-in-Rust equivalent lands, this backend can migrate.
  • aws-lc-rs not ready on Android: the FIPS-validated provider for rustls (rustls-aws-lc-rs) has open Android cross-compile issues as of Jan 2026 (see https://github.com/aws/aws-lc-rs/issues/1006), so switching the default backend isn't viable yet.

The 517-byte ClientHello invariant

ripdpi-tls-profiles enforces an invariant named AvoidsBlocked517ByteClientHello across all four profiles (chrome_stable, firefox_stable, safari_stable, edge_stable). A ClientHello with exactly 517 bytes total length is known to be flagged by certain DPI implementations (the length sits right at a common buffer boundary that triggers deep-inspection heuristics).

Rule: any new profile added to the set must maintain this invariant. The crate's test suite asserts it; do not bypass the assertion. Profile weights: Chrome 65%, Firefox 20%, Safari 10%, Edge 5% — this distribution matches plausible real traffic and must not drift without a corresponding threat-model review.

See the desync-engine skill for adjacent QUIC anti-fingerprinting concerns (quinn pad_to_mtu, USENIX 2025 GFW research).

Connection flow: resolve target (pre-resolved or DNS) -> TCP connect with optional VPN protect + timeout -> bootstrap timeouts for handshake -> TLS -> build_ws_request (Sec-WebSocket-Protocol: binary) -> tungstenite WS handshake -> switch to relay timeouts (read=WS_READ_TIMEOUT 10ms, write=None).

7. Relay Loop (relay.rs)

ws_relay(client, ws, seed_request) -- bidirectional WS<->TCP.

Threading: main thread owns WebSocket (drains outbound queue + reads inbound), spawned ripdpi-ws-up thread reads client TCP into bounded sync_channel(16).

Init: send first 64 bytes as WS binary frame, then any seed_request[64..] remainder, then start bidirectional relay.

Constants: CLIENT_READ_TIMEOUT 250ms, OUTBOUND_QUEUE_CAPACITY 16, MAX_OUTBOUND_BURST 8, OUTBOUND_QUEUE_RETRY_DELAY 1ms, CLOSE_HANDSHAKE_TIMEOUT 5s.

Shutdown: cooperative via AtomicBool -- either thread sets it on EOF/error. drive_close_handshake() attempts graceful WS close. Ping/Pong handled automatically by tungstenite.

8. Encrypted DNS Bootstrap (ws_bootstrap.rs)

resolve_ws_tunnel_addr(dc, runtime_context, protect_path) resolves kws{n}.web.telegram.org via encrypted DNS (DoH/DoT), using ProxyRuntimeContext if available, falling back to default context. Prevents DNS-based blocking of the WS tunnel endpoint.

9. Diagnostics Integration (ripdpi-diagnostics-telegram/src/telegram.rs)

The telegram_availability probe includes telegram_ws_tunnel_probe(): TLS+WS handshake to DC2 without sending MTProto data. Reports wsTunnelStatus, wsTunnelRttMs, wsTunnelError. Feeds composite qualityScore (DL 3x, UL 2x, DC 1x, WS 1x). Wrapped in catch_unwind for panic recovery.

Telemetry: on_ws_tunnel_escalation(target, dc, success) fires on fallback escalation (ripdpi-android-telemetry-adapter/src/{observer,adaptive}.rs); runtime emission lives in ripdpi-proxy-runtime/src/runtime/state/ws.rs.

10. Updating Telegram DC Ranges

  1. Edit dc_from_ip() in native/rust/crates/ripdpi-ws-tunnel/src/dc.rs
  2. Add (octet0, octet1) => match o[2] { ... } arms
  3. Add boundary tests (first and last IP in range)
  4. If adding DC6+, update TelegramDc::from_raw() range checks
  5. Run cargo test --locked -p ripdpi-ws-tunnel
  6. Update DC endpoints in diagnostics probe target configuration

Static lookup table by design -- must work without network, and DC ranges change rarely (last: 185.76.0.0/16 added for DC2).

11. Common Issues

TLS handshake timeout: connect_timeout covers TCP connect + TLS/WS bootstrap. Too short for high-latency networks causes handshake failure. Relay socket uses separate 10ms read timeout after handshake succeeds.

Non-tunnelable DC: Media/CDN DCs (raw < 0) return UnmappableDc; runtime falls back to desync. Expected behavior.

Bootstrap DNS failure: BootstrapFailed returned; seed request preserved for desync fallback.

Relay backpressure: Outbound queue (16) fills when WS endpoint is slow. Uplink retries at 1ms intervals; sustained pressure blocks uplink (preferable to unbounded memory growth).

No IPv6: classify_target returns Passthrough for IPv6. Telegram does not serve MTProto over IPv6 in the Russian market.

Signals

GitHub stars
69
Forks
4
Last commit
Sep 2026

ahel review

  • S4info
    community integration — published by po4yka, not telegram

Automated review, not a security audit. Ruleset v1.

Advanced
Catalog kind
skill
Gateway key
ws-tunnel-telegram
Source
github.com/po4yka/ripdpi