25 KiB
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:
| Input | Value |
|---|---|
| Relay hostname | ________________ |
| Bridge deployment/version | ________________ |
| Relay binary commit/version | ________________ |
| Stock MTProxy version | ________________ |
| MTProxy secret | store outside this document |
| Long-poll enabled | yes / 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 byX-Frame-Options; HELLOpayload01,WELCOME, implicit 4 MiB windows, and all v1 frame types matchdocs/web-proxy-plan.md;- the polling carrier provides ordered retry/deduplication;
OPENcan 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
-
Build the exact candidate commit in Debug:
cmake --build out --config Debug --target Telegram -
Record the commit, build timestamp, OS version, native WebView engine/version, fallback browser name/version, and whether either is managed by enterprise policy.
-
Use a disposable Telegram test account and a separate portable/test profile. Do not overwrite an existing personal portable profile.
-
Preserve Debug logs for the run, but verify they contain no proxy secret or frame payload.
-
Start with WebView inspection and browser developer tools closed; either changes scheduling and would invalidate lifecycle observations.
-
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:
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
bridgequery 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-ancestorsCSP and no incompatibleX-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)
-
Launch the candidate.
-
Open Settings -> Advanced -> Connection type -> Proxy settings -> Add proxy.
-
Select
WEB. -
Enter the relay hostname only and the MTProxy secret.
-
Confirm that these invalid inputs are rejected without saving:
- a value containing
http://orhttps://; - 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.1and1.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
eeTLS-emulation MTProxy secret; - empty hostname or secret.
- a value containing
-
Save the valid entry and enable it.
-
Confirm no system-browser tab opens.
-
Confirm exactly one hidden platform WebView loads
https://<canonical-host>/?bridge=<43-char-capability>#android=<43-char-nonce>. -
Confirm the settings row moves from
connecting…toonlinewithin ten seconds. -
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:
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:
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:
- Fresh-account login, including code entry and 2FA if available. P0.
- Dialog/history load and live incoming updates. P0.
- Send and receive plain messages in private and group chats. P0.
- Send and receive stickers, reactions, edits, deletes, and read receipts. P1.
- Download thumbnails and several small media files. P0.
- Download one file larger than 1 GiB. P1.
- Upload one file large enough to exceed the 4 MiB window many times. P1.
- Stream a video while downloading another file. P1.
- Open media from CDN-backed storage and confirm shifted/CDN DC streams work. P1.
- Leave the client idle for 30 minutes, then send and receive immediately. P1.
- On all-other platforms (WebKitGTK), download a large file while polling is
throttled (rate-limit the link so the relay coalesces
DATAinto 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)
- Start at least 16 simultaneous media downloads across chats.
- Send messages continuously during the downloads.
- Start a large upload at the same time.
- If multi-account is available, sign in to two disposable accounts and generate traffic on both.
- Confirm all logical streams use the same native bridge and relay session. Repeat through fallback and confirm one authenticated local WebSocket/browser session.
- Confirm one slow or window-exhausted stream does not block unrelated streams.
- Cancel half the transfers and verify the corresponding
CLOSEs release relay and stock-MTProxy resources. - 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:
| Case | Expected result |
|---|---|
| Hidden WebView unsupported on the platform | fallback is offered immediately, once; no WebView is created and nothing is retried |
| Initial WebView cannot be created | fallback is offered immediately; no browser opens without confirmation; retry begins after 30 seconds |
| Initial WebView handshake stalls | the 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 probes | logical 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 offer | no browser opens; hidden WebView retries continue every 30 seconds |
| Confirm fallback offer | fresh fragment capability; one tab authenticates; Telegram reconnects |
| WebView retry succeeds during fallback | browser carrier closes only after WebView WELCOME; logical sockets reconnect through WebView |
| Close the fallback tab | fallback sockets disconnect; no tab auto-reopens; WebView retries continue |
| Refresh the fallback tab | consumed capability is not reusable; another explicit Open browser uses a fresh capability |
| Quit browser | same as fallback-tab loss; tdesktop remains responsive |
| Restart browser and use Open browser | clean fallback reconnection |
| Restart tdesktop with WEB saved/enabled | one hidden WebView starts; no browser tab opens; stale fallback tab cannot attach |
| Disable WEB | WebView, listener, and fallback socket close; normal connection policy resumes |
| Switch WEB A -> non-WEB -> WEB A | clean teardown and reactivation |
| Edit WEB hostname or secret | old transport closes; new settings take effect; no old relay traffic remains |
| System sleep 5 minutes | reconnect after wake without corruption or permanent spinner |
| Network down/up | WebView carrier and MTProto recover within normal retry bounds or offer fallback |
| RTC unavailable or disabled by browser policy | carrier 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:
- Drop an empty long-poll request or response. Verify bounded retry and continued Telegram usability.
- Drop one nonempty downlink response after the relay has assigned a cursor. Verify the next request replays it once and tdesktop receives bytes once.
- Drop an uplink response after the relay has processed the sequence. Verify retry deduplication prevents a second write to stock MTProxy.
- Add 1%, then 5%, packet loss and 200-500 ms latency. Verify ordered recovery.
- Restart the web relay while preserving or intentionally discarding session state; record expected bridge status and MTProto reconnection.
- Restart stock MTProxy only. Affected logical streams must close/reconnect without breaking the active carrier.
- 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. - Send
BYE. Current streams must fail and reconnect according to the bridge/server recovery policy. - 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. - 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.
- 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
HostandOrigin; - 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-LengthorTransfer-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 browserinvalidates 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)
- Save WEB, quit cleanly, relaunch, and verify hostname/secret/type survive with port fixed to 443.
- Switch among System, Disabled, SOCKS5/HTTP/MTProxy, and WEB; verify Qt's global application proxy is never set to the WEB hostname.
- 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.
- 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.
- Confirm WEB shares as
t.me/webproxyortg://webproxy, never astg://proxy, that thetg://webproxyconfirmation shows the sponsored-proxy warning, and that inactive WEB rows shownot testedwithout creating a checker, WebView, or browser during availability checks or proxy rotation. - 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:
| OS | Primary WebView | Fallback browser | Version | Result |
|---|---|---|---|---|
| Windows | WebView2 | Chrome | ||
| Windows | WebView2 | Edge | ||
| Windows | WebView2 | Firefox | ||
| macOS | WKWebView | Chrome | ||
| macOS | WKWebView | Safari | ||
| macOS | WKWebView | Firefox | ||
| all-other desktop | helper-process WebKitGTK | Chrome/Chromium | ||
| all-other desktop | helper-process WebKitGTK | Firefox |
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.
- Use a network where direct Telegram and ordinary MTProxy are demonstrably unreliable.
- Record those comparison results immediately before WEB testing.
- Confirm the relay's ordinary site is reachable in the chosen browser.
- Enable WEB and repeat login/history/message/media cases.
- Capture sanitized connection ownership and traffic metadata.
- 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.
- 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)
- Run the Go unit suite and race detector, including concurrent carrier, bootstrap, queue-fragmentation, downlink-frame-count, and goroutine-shutdown cases.
- Validate the shipped Caddyfile with the pinned Caddy build and exercise public root, bridge root, API, static asset, and error routes through Caddy.
- Confirm
/debug/pprof/is 404 on the admin listener by default and appears only whenenable_pprofis explicitly enabled. - Confirm the MTProxy source archive matches the pinned commit checksum and its
Makefile executes as
mtproxy, not root. - From shells running as
caddyandtproxy, verify the MTProxy command line is hidden by the supplied/procrestrictions. Record that root remains able to inspect the stock upstream-Sargument. - 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:
| Gate | Result |
|---|---|
| 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 testing | yes / no |