# tproxy-server puts one WebView transport in front of a stock MTProxy

> The hosted half of a proof-of-concept WEB proxy type for Telegram: the app keeps normal MTProxy framing, multiplexes every connection through a single WebView session over a same-origin carrier, and the relay fans the streams back out to one official MTProxy on loopback. Caddy is the only thing on a public interface, and the repository ships no website on purpose.

**telegramdesktop/tproxy-server** — Proof-of-concept WEB proxy server for Telegram

- Repository: https://github.com/telegramdesktop/tproxy-server
- Stars: 361 · Forks: 39
- Language: Go
- License: not declared
- Published: 2026-09-17 · Updated: 2026-09-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/telegramdesktop-tproxy-server

## One WebView transport carries every stream, and the relay fans them back out

The chain is worth reading slowly, because each arrow changes who is responsible for what.

```text
Telegram app
  MTProto connections with the normal MTProxy transform
          |
          v
  local WEB proxy adapter
  one logical stream per app connection
          |
          v
  one WebView transport and authenticated relay session
  multiplexed frames in the selected HTTPS/WebSocket carrier
          |
          v
  tproxy-server -> one local TCP connection per stream -> official MTProxy
```

The app does not stop using MTProxy. Its connections keep the normal MTProxy transform, so the encryption and framing a Telegram client already speaks are unchanged. What changes is the transport: instead of opening TCP connections directly, the app hands them to a small local adapter that turns each app connection into one logical stream.

The adapter multiplexes those streams into a single WebView transport and one authenticated relay session. The server then reverses the operation, opening one local TCP connection per logical stream into a stock official MTProxy.

The phrase one WebView transport is qualified in the README because it is easy to misread: it means one logical carrier and one relay session for the app, not one HTTP request and not one backend connection per stream. The carrier profile decides how many physical connections that turns into, and the profile may use the original serialized HTTPS carrier, independent HTTPS request lanes per Telegram logical session, one multiplexed WebSocket, or an independent WebSocket per logical session.

That last distinction is the difference between a connection-oriented and a request-oriented design, and the README leaves the choice to the server profile rather than hard-coding it.

## The relay sees opaque bytes: no destination, no decryption, four frame types

What the server can and cannot do is stated in two sentences, and they are the security argument for the whole design.

The relay treats DATA as opaque bytes. It cannot choose a Telegram destination and it cannot decrypt the MTProxy stream. The relay never receives a client-selected backend address, so there is no path by which a user of the relay can aim it at an arbitrary host, and there is no plaintext to read even if it wanted to.

The multiplexing happens in four frame types. OPEN, DATA, WINDOW and CLOSE frames carry every app connection through the session, which is enough to reconstruct separate bidirectional streams on the far side without understanding what is inside them. WINDOW is the one worth noting, because it is flow control rather than payload, and its presence means the sender is not simply dumping an unbounded connection through the carrier.

The app configures exactly two things: a hostname and an MTProxy secret. It derives the bridge capability from those locally, and the raw secret is never exposed to JavaScript, which matters because the carrier runs inside a WebView. The WebView opens the bridge page, exchanges a short-lived bootstrap token for a relay session, and then runs whichever carrier mode the matching server profile selected.

The bridge itself is reachable only through that capability: the hostname remains a regular HTTPS website, requests without authentic relay credentials receive the public site, and only a request carrying a valid bridge capability reveals the bridge. PROTOCOL.md holds the normative wire contract for all of this.

## Caddy is the only process on a public interface, and 2398 never leaves loopback

The reference deployment is drawn as one diagram.

```text
Internet :80/:443 -> Caddy -> 127.0.0.1:8080 tproxy-server -> operator site
                                              |                (memory or loopback app)
                                              \-> 127.0.0.1:2398 official MTProxy
```

Three ports, and only one of them is reachable from outside. Caddy terminates TLS on 80 and 443. The relay listens on 127.0.0.1:8080. The official MTProxy client port is 127.0.0.1:2398.

What stays local is broader than the ports: the relay's admin endpoints and the MTProxy statistics. There is no external interface on either, so neither an operator console nor a metrics view is exposed by the layout.

Caddy proxies every path to the relay, which is what makes the camouflage work. In static mode the relay serves the whole site from memory, using standard Go conditional and range handling so byte ranges and caching headers behave as they would behind a normal file server. In application mode it delegates ordinary and unauthenticated requests to one private loopback web application, and intercepts a request that proves knowledge of a bridge or session token before that application ever sees it.

The ordering is the point. There is no separately hosted relay path for an unauthenticated prober to compare against the public site, so the absence of a relay endpoint is itself invisible. Only a request that already holds a valid capability learns that a bridge exists, and a request that already holds a valid session token gets the session.

## No deployable website is included, because a shared starter becomes a fingerprint

This is the most unusual decision in the repository, and the reasoning is given rather than implied. The repository deliberately does not include a deployable public website. If many operators installed the same starter, its body and assets would become an easy active-probing signature.

So the operator supplies the site. The recommended choice is a real loopback website or a stock static server configured with public_upstream, while the in-process public_dir mode remains available for simple sites. The documentation says as much when it lists the required file: only index.html is required, and a several-page site would normally also have about.html, privacy.html, 404.html, styles.css, a favicon and images.

Two operational details follow from the mode you choose. In application mode, anything database-backed works: accounts, forms, server rendering, an existing CMS, site APIs, SSE and WebSockets, with the application owning its framework, headers, cookies and persistence while the relay stays the only public gateway. In static mode the site is read once at start-up, so changing files under public_dir needs a relay restart.

One compatibility wrinkle is documented rather than assumed. Links should be exact, such as /about.html, or you explicitly select static_routes: "legacy" to get extensionless aliases back. Old configurations keep those aliases, which means two configurations of the same relay can serve the same site at different-looking URLs.

## An internationalised hostname typed by hand can derive a different capability on Windows

The DNS step carries the longest failure mode in the project, and it is about Unicode rather than networking.

Publish the hostname to users in its ACE form, the xn-- form, when it is an internationalised name. The desktop client stores and derives the capability from the A-label, and ACE input round-trips unchanged on every platform. A hand-typed Unicode host does not: one containing ß, ς, ZWJ or ZWNJ, or characters newer than Unicode 3.2, can be mapped differently by the Qt 5.15 build on Windows and the Qt 6 builds on macOS and Linux, and then derive a different capability. Same string typed twice, two different capabilities, and a client that cannot open the bridge.

The rest of the DNS guidance is conservative on purpose. Add an A record pointing at the server's public IPv4, and add an AAAA record only when the server really has working public IPv6. Do not put a CDN or an HTTP proxy in front of the first deployment.

Then wait until the record resolves from outside your network before handing the hostname to anyone.

```bash
dig +short A proxy.example.com
dig +short AAAA proxy.example.com
```

The secret is generated on your own machine rather than on the server.

```bash
openssl rand -hex 16
```

That produces 32 lowercase hexadecimal characters, and the exact value matters because it is entered in every client that uses this server and passed to the installer.

## The installer backs up the Caddyfile and then replaces the active Caddy configuration

The prerequisites list is short enough to be a checklist of things that will stop you later: a dedicated lowercase hostname such as proxy.example.com that you control, an x86_64 Linux server with a public IPv4 address, SSH access, systemd, and either Ubuntu 22.04+ or Debian 12+, root or passwordless sudo, public inbound TCP 80 and 443, one random 16-byte secret, and an operator-owned static site or web application bound to a private loopback port.

Note the architecture pin. x86_64 Linux is stated as a requirement rather than a suggestion, and the supported distributions are named with versions.

The installer carries its own warning, and it is the one to read before running it. It is intended for a clean server on which Caddy may own ports 80 and 443. It backs up an existing /etc/caddy/Caddyfile, and then it replaces the active Caddy configuration. If the server already hosts other sites, the manual integration section is the path instead.

That is a backup followed by an overwrite, in that order, which is fine on a dedicated box and destructive on a shared one. The repository layout matches the shape of the software: cmd/ for the entry points, internal/ for the implementation, deploy/ for the systemd and Caddy pieces, and two example files, config.example.json and profiles.example.json, for the relay configuration and the carrier profiles.

## One Go dependency, a proof-of-concept label, and no licence file at the root

Two structural facts frame everything else.

The first is the dependency list. go.mod declares go 1.20 and a single requirement, github.com/gorilla/websocket v1.5.3. For a proxy that terminates TLS-adjacent traffic, multiplexes streams and serves a website from memory, one external dependency is a deliberate design choice rather than an accident of an early prototype, and it makes the audit surface small enough to read.

The second is the honesty of the documentation set. PLAN.md holds the architecture, the limits, the implementation rationale and the remaining proof-of-concept work. PROTOCOL.md is the normative wire contract. PUBLIC_SITE.md defines the operator-owned website extension points, HARDENING.md and BASE_PATH.md cover hardening and path handling, ANDROID.md describes an experimental client and IOS.md a plan. Nothing in that list claims more than it has.

What is missing is a licence. There is no LICENSE file among the top-level entries, which are the documentation set, cmd/, internal/, deploy/, the two example files, go.mod, go.sum, .gitignore and README.md. For a repository you would run on a server you own, resolve the terms before you deploy it.

There are no GitHub releases either, so there is no version to pin. The last push was on 2026-09-29.

## Conclusion

Adopt tproxy-server if you operate infrastructure for users who cannot reach Telegram directly, if you can dedicate a hostname and an x86_64 Linux server to it, and if you are willing to run the published wire contract rather than a mature proxy. Do not treat it as a product: the README calls it a proof of concept, PLAN.md lists the limits and the remaining work, the Android client is experimental and the iOS side is a plan, and the licence is unstated because no LICENSE file sits at the repository root. Verify three things first: that the relay you point users at serves a real website of your own rather than the bundled starter, because the project omits a deployable site for exactly that reason, that your hostname is published to users in its ACE form so the derived capability matches on every platform, and that the installer is not going to take over a Caddy configuration that already serves other sites. The last push was on 2026-09-29 and there are no GitHub releases.

## FAQ

### What is tproxy-server and how does it reach Telegram?

It is the hosted half of a proof-of-concept WEB proxy type for Telegram. The app keeps its normal MTProxy framing, sends every connection through one app-owned WebView transport as multiplexed frames, and the relay opens one local TCP connection per stream into a stock official MTProxy on the server.

### Can the tproxy-server relay see what I am sending through Telegram?

No. The relay treats DATA frames as opaque bytes: it cannot decrypt the MTProxy stream, and it never receives a client-selected backend address, so it cannot choose a destination either.

### What server do I need to run tproxy-server?

An x86_64 Linux server with a public IPv4 address, SSH access, systemd, Ubuntu 22.04+ or Debian 12+, root or passwordless sudo, and inbound TCP 80 and 443. You also need a dedicated hostname you control, a 16-byte random secret and your own website bound to a loopback port.

### Why does tproxy-server not include a website I can deploy?

Because if many operators installed the same starter, its body and assets would become an easy active-probing signature. You supply a real loopback site or stock static server as public_upstream, or use the in-process public_dir mode with at least an index.html.

## Sources

- [Issues](https://github.com/telegramdesktop/tproxy-server/issues)
- [README](https://github.com/telegramdesktop/tproxy-server/blob/master/README.md)
- [telegramdesktop/tproxy-server on GitHub](https://github.com/telegramdesktop/tproxy-server)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/telegramdesktop-tproxy-server
