WebSocket Tunnel for Telegram (ripdpi-ws-tunnel)
SkillCommunicationUse 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.
No other account needed.
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 immediatelyFallback-- 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 IPshould_ws_tunnel_fallback()--WsTunnelMode::Fallback+ known Telegram IPrun_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:
| Prefix | 3rd octet | DC | Prefix | 3rd octet | DC |
|---|---|---|---|---|---|
| 149.154 | 160-163 | 1 | 91.108 | 56-59 | 5 |
| 149.154 | 164-167 | 2 | 91.108 | 8-11 | 3 |
| 149.154 | 168-171 | 3 | 91.108 | 12-15 | 4 |
| 149.154 | 172-175 | 1 | 91.108 | fallback | 2 |
| 149.154 | fallback | 2 | 91.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) ->
has_valid_encrypted_prefix(init)-- rejects0xeffirst byte, TLS prefix, HTTP methods (GET/HEAD/POST/OPTI), transport tags (0xdddddddd/eeeeeeee/efefefef)decrypt_init_packet(init)-- AES-256-CTR: key=[8..40], IV=[40..56]has_allowed_protocol_tag(decrypted)-- bytes [56..60]:[0xdd;4]/[0xee;4]/[0xef;4]- DC extraction from bytes [60..64] as
i32LE ->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, ALPNhttp/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-profilescrate wrapsSSL_CTXat 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-rsnot 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
- Edit
dc_from_ip()innative/rust/crates/ripdpi-ws-tunnel/src/dc.rs - Add
(octet0, octet1) => match o[2] { ... }arms - Add boundary tests (first and last IP in range)
- If adding DC6+, update
TelegramDc::from_raw()range checks - Run
cargo test --locked -p ripdpi-ws-tunnel - 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