docs/web-proxy-test-plan.md
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.
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:
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:
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;OPEN can dial only the configured local stock MTProxy;Stop if any item is false. A client run cannot produce a meaningful result against an incompatible bridge.
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.
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:
/ looks like the intended ordinary site and contains no transport error details;bridge query returns the same ordinary site without
transport-branded errors;docs/web-proxy-plan.md;frame-ancestors CSP and no
incompatible X-Frame-Options, verified without recording its query;Delete the captured bodies after inspection if the deployment embeds any configuration. Keep sanitized headers with the run artifacts.
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:
http:// or https://;127.1,
0x7f.1, 0177.0.0.1 and 1.2.3;localhost;ee TLS-emulation MTProxy secret;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… to online within 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:
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.
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:
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.
Run in order, checking both client behavior and relay/MTProxy stream metrics:
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:
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.
CLOSEs release relay and
stock-MTProxy resources.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.
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.
Coordinate these with the server operator:
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.BYE. Current streams must fail and reconnect according to the bridge/server
recovery policy.DATA/WINDOW/CLOSE.
The closed stream may discard late frames, but unrelated streams must remain live.Accept the fallback offer, then use a purpose-built local test client; do not paste the real capability into shell history.
Verify rejection of:
Host;Origin;Host and Origin;Content-Length or Transfer-Encoding;Then verify:
Open browser invalidates any unconsumed earlier capability;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;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.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.
This is the product hypothesis test and cannot be replaced by a lab run.
Pass means WEB works while both direct Telegram and ordinary MTProxy controls fail, without requiring a browser certificate exception or a nonstandard network setting.
/debug/pprof/ is 404 on the admin listener by default and appears only
when enable_pprof is explicitly enabled.mtproxy, not root.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.Attach or link:
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 |