Back to Tdesktop

Telegram Desktop WEB proxy: server-ready execution test plan

docs/web-proxy-test-plan.md

7.1.124.9 KB
Original Source

Telegram Desktop WEB proxy: server-ready execution test plan

Execute this document only after a hosted relay matching ../tproxy-server/PLAN.md is deployed. Record every requested value and artifact in one run log. Do not put the proxy secret, Telegram auth keys, message contents, or the loopback fragment capability in logs or screenshots.

1. Release gate

WEB is ready for wider testing only when all P0 and P1 cases below pass through the native WebView on every supported desktop platform, and the explicit system-browser fallback has no unexplained P0 failure in its browser matrix.

Severity:

  • P0: transport boundary, corruption, crash, login, basic send/receive, or total reconnect failure.
  • P1: large transfer, concurrency, bounded memory, lifecycle recovery, or carrier fallback failure.
  • P2: status text, browser-specific lifecycle annoyance, or non-blocking polish.

2. Inputs to obtain from the server operator

Record these before starting:

InputValue
Relay hostname________________
Bridge deployment/version________________
Relay binary commit/version________________
Stock MTProxy version________________
MTProxy secretstore outside this document
Long-poll enabledyes / no
Server log location________________
Server metrics endpoint/dashboard________________
Test start/end in UTC________________
Tester network and country________________

The operator must confirm:

  • the bridge root response can be framed by http://127.0.0.1:* and is not prevented by X-Frame-Options;
  • HELLO payload 01, WELCOME, implicit 4 MiB windows, and all v1 frame types match docs/web-proxy-plan.md;
  • the polling carrier provides ordered retry/deduplication;
  • OPEN can dial only the configured local stock MTProxy;
  • payload logging is disabled.

Stop if any item is false. A client run cannot produce a meaningful result against an incompatible bridge.

3. Client build and isolation

  1. Build the exact candidate commit in Debug:

    bash
    cmake --build out --config Debug --target Telegram
    
  2. Record the commit, build timestamp, OS version, native WebView engine/version, fallback browser name/version, and whether either is managed by enterprise policy.

  3. Use a disposable Telegram test account and a separate portable/test profile. Do not overwrite an existing personal portable profile.

  4. Preserve Debug logs for the run, but verify they contain no proxy secret or frame payload.

  5. Start with WebView inspection and browser developer tools closed; either changes scheduling and would invalidate lifecycle observations.

  6. Disable unrelated VPNs/proxies for baseline. Record DNS-over-HTTPS, browser proxy, and system proxy state.

4. Hosted endpoint preflight

Run from the client machine:

bash
curl -fsS -D /tmp/tproxy-site.headers -o /tmp/tproxy-site.body \
  https://RELAY_HOSTNAME/
curl -fsS -D /tmp/tproxy-invalid.headers -o /tmp/tproxy-invalid.body \
  'https://RELAY_HOSTNAME/?bridge=invalid'

Verify:

  • TLS certificate and hostname are valid with no warning or redirect to HTTP;
  • / looks like the intended ordinary site and contains no transport error details;
  • an invalid or missing bridge query returns the same ordinary site without transport-branded errors;
  • server unit tests pass both capability vectors from docs/web-proxy-plan.md;
  • the valid bridge response has a compatible frame-ancestors CSP and no incompatible X-Frame-Options, verified without recording its query;
  • transport APIs reject unsupported methods and malformed session identifiers;
  • server health shows the stock MTProxy backend reachable.

Delete the captured bodies after inspection if the deployment embeds any configuration. Keep sanitized headers with the run artifacts.

5. Configuration and first connection (P0)

  1. Launch the candidate.

  2. Open Settings -> Advanced -> Connection type -> Proxy settings -> Add proxy.

  3. Select WEB.

  4. Enter the relay hostname only and the MTProxy secret.

  5. Confirm that these invalid inputs are rejected without saving:

    • a value containing http:// or https://;
    • a hostname with an explicit port;
    • a value with username, path, query, or fragment;
    • an IPv4 or IPv6 address, including shorthand forms such as 127.1, 0x7f.1, 0177.0.0.1 and 1.2.3;
    • a single-label name such as localhost;
    • an invalid IDNA name, empty label, overlong label, or trailing dot;
    • invalid or unsupported MTProxy secret;
    • an ee TLS-emulation MTProxy secret;
    • empty hostname or secret.
  6. Save the valid entry and enable it.

  7. Confirm no system-browser tab opens.

  8. Confirm exactly one hidden platform WebView loads https://<canonical-host>/?bridge=<43-char-capability>#android=<43-char-nonce>.

  9. Confirm the settings row moves from connecting… to online within ten seconds.

  10. Confirm Telegram loads dialogs and receives updates without a Telegram WebView window, Qt widget container, taskbar item, or dock item appearing.

Pass conditions:

  • no crash, assertion, TLS warning, CORS/LNA prompt, mixed-content error, or iframe refusal;
  • server sees one relay session and the expected MTProxy stream connections;
  • the WebView requests exactly the canonical bridge URL and then scrubs its query and fragment from history;
  • the page, DOM, network request headers, and console never contain the MTProxy secret;
  • closing proxy settings does not affect the connection.

Verify both canonical link forms with the same hostname and secret:

text
https://t.me/webproxy?server=proxy.example.com&secret=000102030405060708090a0b0c0d0e0f
tg://webproxy?server=proxy.example.com&secret=000102030405060708090a0b0c0d0e0f

Each must show a confirmation containing the canonical hostname and secret, no port or status row, and one connect action. Dismissing it must not save or enable the proxy. Sharing a saved WEB entry must reproduce the public form with server and secret, without port. After disabling the saved proxy, its row must show not tested; re-enabling that exact entry must replace it with the live transport state. Inactive WEB rows must never start a proxy checker, WebView, or browser tab.

Repeat once with an IDN hostname on a TLD that Qt renders in Unicode (.de or .com, e.g. bücher.de, not .example): the entry is stored as xn--bcher-kva.de, the hidden WebView connects, and the capability derived for it is byte-identical on a Qt 5.15 Windows build and a Qt 6 macOS/Linux build. Also confirm that xn--strae-oqa.example is stored unchanged on both.

Repeat once with a profile secret that is valid MTProxy syntax but is not configured on staging. The ordinary-site fallback must match other root responses. The row must show connecting… while the hidden WebView is retried with growing delays (2 s, 4 s, 8 s); after the third consecutive failure — at most ~2 min including the extended handshake — the client must offer to open the proxy page in the browser, and keep retrying the hidden WebView every 30 seconds afterwards. Cancel the offer and confirm no browser opens; then accept it and confirm exactly one numeric http://127.0.0.1:<ephemeral>/#<capability> tab opens, and that no loopback port was listening before that click.

6. Network-origin invariant (P0)

While WEB is the only enabled Telegram proxy, collect connection ownership with an OS tool. On macOS, for example:

bash
lsof -nP -iTCP -sTCP:ESTABLISHED | egrep 'Telegram|Chrome|Chromium|Edge|Safari|Firefox'

Also capture a short packet trace or firewall connection log if permitted.

Verify:

  • in the primary mode, Telegram Desktop's WebView/WebContent process owns the HTTPS connection to the relay and no loopback WebSocket is connected;
  • Telegram's ordinary MTProto sockets have no external connection to the relay, stock MTProxy, or Telegram DCs; the relay connection belongs only to the platform WebView engine;
  • a WEB connection failure does not trigger tdesktop's HTTP time-sync fallback;
  • DNS resolution of the relay is attributable to the WebView/system resolver, not a custom proxy/DC resolver in tdesktop;
  • external carrier traffic remains inside the WebView-owned TLS connection.

Repeat after explicitly accepting browser fallback. In that mode Telegram Desktop must connect only to 127.0.0.1:<ephemeral> for this transport, and the browser must own the external HTTPS connection to the relay.

Account for unrelated Telegram HTTP traffic such as update checks before declaring a failure. The invariant applies to the MTProto transport, not every auxiliary HTTP request made by the application.

7. Functional traffic (P0/P1)

Run in order, checking both client behavior and relay/MTProxy stream metrics:

  1. Fresh-account login, including code entry and 2FA if available. P0.
  2. Dialog/history load and live incoming updates. P0.
  3. Send and receive plain messages in private and group chats. P0.
  4. Send and receive stickers, reactions, edits, deletes, and read receipts. P1.
  5. Download thumbnails and several small media files. P0.
  6. Download one file larger than 1 GiB. P1.
  7. Upload one file large enough to exceed the 4 MiB window many times. P1.
  8. Stream a video while downloading another file. P1.
  9. Open media from CDN-backed storage and confirm shifted/CDN DC streams work. P1.
  10. Leave the client idle for 30 minutes, then send and receive immediately. P1.
  11. On all-other platforms (WebKitGTK), download a large file while polling is throttled (rate-limit the link so the relay coalesces DATA into 1 MiB frames, i.e. ~1.4 MiB base64 native messages); the download must complete with a matching hash and no carrier restart. P0.

For large transfers record:

  • bytes and final content hash;
  • average and minimum throughput;
  • tdesktop, WebView/WebContent, fallback browser when used, and relay peak memory;
  • number of logical streams and stock-MTProxy sockets;
  • reconnect/retry count;
  • pending-uplink overflow or flow-control errors.

Run the same payload once through direct MTProxy and once through tdesktop's built-in HTTP transport under the same controlled capacity/RTT when those controls are reachable. On a controlled link with at least 100 Mbit/s capacity, WEB should sustain at least 40 Mbit/s at 200 ms web-engine-to-relay RTT and 20 Mbit/s at 500 ms, through the hidden WebView. Repeat the browser-specific target with the fallback tab in both foreground and ordinarily hidden states. Also report WEB/direct-HTTP ratios for message round-trip p50/p95 and bulk transfer; route differences must be reported separately from transport overhead.

No transfer may corrupt, silently truncate, duplicate an upload, or grow client memory without returning toward baseline after completion.

8. Multiplexing and concurrency (P1)

  1. Start at least 16 simultaneous media downloads across chats.
  2. Send messages continuously during the downloads.
  3. Start a large upload at the same time.
  4. If multi-account is available, sign in to two disposable accounts and generate traffic on both.
  5. Confirm all logical streams use the same native bridge and relay session. Repeat through fallback and confirm one authenticated local WebSocket/browser session.
  6. Confirm one slow or window-exhausted stream does not block unrelated streams.
  7. Cancel half the transfers and verify the corresponding CLOSEs release relay and stock-MTProxy resources.
  8. Let the remainder finish and compare hashes.

Monitor thread sanitizer output if a TSan build is practical. Otherwise run for at least two hours while watching for cross-thread QObject warnings, stale stream delivery, stream-id mixups, growing queues, and use-after-free crashes.

Add a focused destruction race run that repeatedly opens and destroys logical WEB sockets while the worker concurrently delivers connected, DATA, CLOSE, and failure notifications. Run it under ASan and TSan where supported. No notification may begin after synchronous stream unregistration returns, and no callback may run against a socket whose destructor has started.

9. WebView, browser, and transport lifecycle (P0/P1)

The Settings code externalweb toggles a process-local test override. The first invocation blocks and tears down the hidden WebView carrier; the second removes the block and starts a fresh WebView candidate immediately. Each invocation shows its new state in a toast. Use it to drive the fallback and recovery cases without changing the proxy or network.

Execute each case from a connected baseline:

CaseExpected result
Hidden WebView unsupported on the platformfallback is offered immediately, once; no WebView is created and nothing is retried
Initial WebView cannot be createdfallback is offered immediately; no browser opens without confirmation; retry begins after 30 seconds
Initial WebView handshake stallsthe deadline is extended while the bridge reports connecting/reconnecting, up to 45 s; the row stays connecting…; fallback is offered only after the third consecutive failure
Active WebView stops answering probeslogical sockets disconnect within ten seconds; the row shows connecting…; a retry starts after 2 s and no fallback box appears on the first failure
Cancel fallback offerno browser opens; hidden WebView retries continue every 30 seconds
Confirm fallback offerfresh fragment capability; one tab authenticates; Telegram reconnects
WebView retry succeeds during fallbackbrowser carrier closes only after WebView WELCOME; logical sockets reconnect through WebView
Close the fallback tabfallback sockets disconnect; no tab auto-reopens; WebView retries continue
Refresh the fallback tabconsumed capability is not reusable; another explicit Open browser uses a fresh capability
Quit browsersame as fallback-tab loss; tdesktop remains responsive
Restart browser and use Open browserclean fallback reconnection
Restart tdesktop with WEB saved/enabledone hidden WebView starts; no browser tab opens; stale fallback tab cannot attach
Disable WEBWebView, listener, and fallback socket close; normal connection policy resumes
Switch WEB A -> non-WEB -> WEB Aclean teardown and reactivation
Edit WEB hostname or secretold transport closes; new settings take effect; no old relay traffic remains
System sleep 5 minutesreconnect after wake without corruption or permanent spinner
Network down/upWebView carrier and MTProto recover within normal retry bounds or offer fallback
RTC unavailable or disabled by browser policycarrier remains usable; lifecycle guard failure is silent and bounded

For Chromium browsers, confirm the page has one open RTCDataChannel between two same-page peer connections in chrome://webrtc-internals, with the selected ICE pair confined to 127.0.0.1 and no STUN or TURN server. In chrome://discards, record the fallback tab's automatic freeze/discard eligibility and reasons; active WebRTC should protect it from normal automatic freezing. Then leave the tab hidden for at least 15 minutes with developer tools closed while continuously exchanging Telegram traffic. Carrier traffic must continue without minute-scale stalls. Repeat once with browser energy saving enabled. Manual or urgent discard may still terminate the fallback carrier and must recover through an explicit Open browser as described above. Run the corresponding 15-minute hidden test through the native WebView first; it must not depend on the fallback page's RTC lifecycle guard.

10. Carrier reliability and server faults (P1)

Coordinate these with the server operator:

  1. Drop an empty long-poll request or response. Verify bounded retry and continued Telegram usability.
  2. Drop one nonempty downlink response after the relay has assigned a cursor. Verify the next request replays it once and tdesktop receives bytes once.
  3. Drop an uplink response after the relay has processed the sequence. Verify retry deduplication prevents a second write to stock MTProxy.
  4. Add 1%, then 5%, packet loss and 200-500 ms latency. Verify ordered recovery.
  5. Restart the web relay while preserving or intentionally discarding session state; record expected bridge status and MTProto reconnection.
  6. Restart stock MTProxy only. Affected logical streams must close/reconnect without breaking the active carrier.
  7. Return malformed frame length, unknown frame type, invalid stream zero usage, zero/invalid WINDOW, and data beyond granted credit in a controlled staging environment. The client must close the carrier/streams cleanly, remain responsive, and show no memory error.
  8. Send BYE. Current streams must fail and reconnect according to the bridge/server recovery policy.
  9. Race client-side close against backend EOF and delayed DATA/WINDOW/CLOSE. The closed stream may discard late frames, but unrelated streams must remain live.
  10. Stall downlink reads and send highly fragmented one-byte/empty-control patterns. Verify the relay enforces byte and item budgets, returns at most 4096 frames in one body, coalesces adjacent credit, and returns near baseline heap use afterward.
  11. Issue bridge requests past the per-IP burst/rate and unused-token limits. Other source IPs must retain bounded access, expired tokens must release their slots, and the public fallback must not expose why a request was limited.

11. Explicit browser-fallback loopback validation tests (P0)

Accept the fallback offer, then use a purpose-built local test client; do not paste the real capability into shell history.

Verify rejection of:

  • a connection to a non-loopback interface (the port must not be listening there);
  • wrong Host;
  • absent, wrong, or cross-origin Origin;
  • duplicate HTTP header names, including Host and Origin;
  • invalid WebSocket key/version/upgrade headers;
  • an unmasked client frame;
  • invalid fragmentation or control-frame fragmentation;
  • a WebSocket message larger than 2 MiB;
  • an HTTP header block larger than 16 KiB;
  • a GET with Content-Length or Transfer-Encoding;
  • a silent connection or unauthenticated WebSocket held past ten seconds;
  • first message not being an auth object;
  • wrong, reused, expired, or empty capability;
  • binary data before authentication;
  • non-status text after authentication.

Then verify:

  • a consumed capability cannot authenticate a second socket;
  • minting via Open browser invalidates any unconsumed earlier capability;
  • a newly authenticated browser replaces the old one and forces logical reconnect;
  • GET / never includes the bridge capability (bridge=) or MTProxy secret; the bridge URL arrives only as the {"t":"bridge"} text message on the authenticated WebSocket;
  • no loopback port is listening while the fallback was never requested, and the listener closes once a tab has authenticated, when the capability expires unused, and when the hidden WebView takes over;
  • pings before authentication are not answered;
  • the local protocol cannot request an arbitrary host or port;
  • malformed input causes bounded close/failure, not a crash or growing buffer.
  • the loopback parent CSP contains a fresh nonce and does not permit arbitrary inline script.

12. Persistence and compatibility (P1)

  1. Save WEB, quit cleanly, relaunch, and verify hostname/secret/type survive with port fixed to 443.
  2. Switch among System, Disabled, SOCKS5/HTTP/MTProxy, and WEB; verify Qt's global application proxy is never set to the WEB hostname.
  3. Corrupt a copy of the serialized proxy type to an unknown future value and verify the candidate skips it instead of crashing. Never modify the only real settings file.
  4. Launch an older binary against a disposable copy of settings containing WEB and document its behavior. The new binary handles unknown types; old binary behavior may still require a release-note warning.
  5. Confirm WEB shares as t.me/webproxy or tg://webproxy, never as tg://proxy, that the tg://webproxy confirmation shows the sponsored-proxy warning, and that inactive WEB rows show not tested without creating a checker, WebView, or browser during availability checks or proxy rotation.
  6. Confirm a saved and enabled WEB proxy creates one hidden WebView at application startup and opens no browser tab. Force the WebView to fail, cancel the offer, and confirm restart/retry still never opens a tab without confirmation.

13. WebView and fallback-browser matrix

At minimum execute sections 5, 6, 7 (small traffic), 9, and 11 on each supported combination available:

OSPrimary WebViewFallback browserVersionResult
WindowsWebView2Chrome
WindowsWebView2Edge
WindowsWebView2Firefox
macOSWKWebViewChrome
macOSWKWebViewSafari
macOSWKWebViewFirefox
all-other desktophelper-process WebKitGTKChrome/Chromium
all-other desktophelper-process WebKitGTKFirefox

For the primary path, verify the WebView is native, hidden, unique per process, and has no Telegram RpWindow or embedded widget. On all-other platforms verify the WebKitGTK helper process remains the owner and that the restricted carrier still comes up in the Flatpak, Snap and AppImage packages (the 4.0/4.1 API path now enables the WebKit web-process sandbox). For fallback, pay special attention to iframe CSP, loopback WebSocket Origin, mixed-content rules, background-tab throttling, confirmation-only browser launch, and managed-browser policies.

Restricted-profile probe, on every platform, with the debug build connected through the hidden WebView: the debug log must not contain Restricted WebView profile probe failed (the client evaluates typeof RTCPeerConnection === 'undefined' && typeof WebTransport === 'undefined' && typeof WebAssembly === 'undefined' in the top frame and in a freshly created about:blank iframe after the first bridge message). Additionally, with the webview inspector available on a development build of the bridge page, confirm that new WebSocket('wss://example.com') throws or fails, fetch('https://example.com') is blocked, and navigator.mediaDevices, navigator.credentials and Notification are undefined. On Windows, capture the network during connect and confirm no SmartScreen (*.smartscreen.microsoft.com) traffic from the WebView2 process of the restricted profile.

14. Unreliable-MTProto network field test (P0)

This is the product hypothesis test and cannot be replaced by a lab run.

  1. Use a network where direct Telegram and ordinary MTProxy are demonstrably unreliable.
  2. Record those comparison results immediately before WEB testing.
  3. Confirm the relay's ordinary site is reachable in the chosen browser.
  4. Enable WEB and repeat login/history/message/media cases.
  5. Capture sanitized connection ownership and traffic metadata.
  6. Confirm the observable external client is the platform WebView and all carrier requests are ordinary same-origin HTTPS; no public WebSocket is required. Repeat once through explicit fallback and confirm the observable client then becomes the selected browser.
  7. Repeat at two times of day and, if possible, through two access providers.

Pass means WEB works while both direct Telegram and ordinary MTProxy controls fail, without requiring a browser certificate exception or a nonstandard network setting.

15. Deployment-boundary checks (P1)

  1. Run the Go unit suite and race detector, including concurrent carrier, bootstrap, queue-fragmentation, downlink-frame-count, and goroutine-shutdown cases.
  2. Validate the shipped Caddyfile with the pinned Caddy build and exercise public root, bridge root, API, static asset, and error routes through Caddy.
  3. Confirm /debug/pprof/ is 404 on the admin listener by default and appears only when enable_pprof is explicitly enabled.
  4. Confirm the MTProxy source archive matches the pinned commit checksum and its Makefile executes as mtproxy, not root.
  5. From shells running as caddy and tproxy, verify the MTProxy command line is hidden by the supplied /proc restrictions. Record that root remains able to inspect the stock upstream -S argument.
  6. Start sessions with active backend reads and writes, then stop the relay. Shutdown must complete inside its configured deadline with no backend goroutine left.

16. Exit criteria and run report

Attach or link:

  • client/server/bridge/MTProxy versions;
  • sanitized server headers and logs;
  • Debug build result;
  • functional and lifecycle checklist;
  • transfer hashes and performance/memory table;
  • connection-owner evidence;
  • long-poll retry/replay evidence;
  • loopback and protocol validation results;
  • WebView and fallback-browser matrix;
  • unreliable-MTProto network comparisons and outcome;
  • every defect with severity, exact reproduction, expected/actual result, timestamps, and relevant sanitized logs.

Final decision:

GateResult
All P0 passed
All P1 passed or explicitly waived
Logs contain no secrets or payloads
Memory and queues bounded
Long-poll reliability proven
Unreliable-MTProto network result confirmed
Ready for wider testingyes / no