// DevOps
Telemt: a web proxy for Telegram (WEB mode) — how it works and how to install
Published on 2026-09-19
Link to check how the installed TeleMT works
A new proxy type WEB has appeared in Telegram. The client opens a built-in WebView, loads a service page from the server, and forwards MTProto through it inside ordinary HTTPS requests or WebSocket. Telemt supports this mode starting from version 3.5.1. This article describes how the mode works and how to deploy it. All configurations were tested on version 3.5.7.
The WEB mode differs from Fake TLS from the Telemt installation article as follows:
| Fake TLS | WEB | |
|---|---|---|
| Domain | someone else’s, used as a mask | your own |
| Certificate | not needed | real, on the reverse proxy |
| Who terminates TLS | Telemt | Caddy, NGINX or HAProxy |
| What is visible externally | TLS connection with foreign SNI | HTTPS requests to your site |
| Secret format | ee | plain or dd |
| Link | tg://proxy?server=…&port=…&secret=ee… | tg://webproxy?server=…&secret=dd… |
Both modes can run in the same Telemt process at the same time.
How it works
Telegram client (WebView)
| HTTPS or WSS, port 443
v
Caddy / NGINX / HAProxy — terminates TLS, forwards Host and a single address in X-Forwarded-For
| HTTP/1.1 over a private network or loopback
v
WEB-listener Telemt
|-- request with valid credentials --> MTProto-relay --> Telegram datacenters
`-- any other request --> decoy site (decoy)In this scheme Telemt does not terminate TLS. It accepts plain HTTP from the reverse proxy and selects a virtual host (vhost) by the Host header with its profiles and decoy site.
Exchange sequence:
- Capability. From the user’s secret and the hostname the client computes a 32-byte value: HMAC-SHA256, key — the secret (for
ddmode with a leading 0xdd byte), data — the stringtdesktop-web-proxy-bridge-v1\nand the hostname. The result is encoded in base64url and sent in the requestGET /?bridge=<43 chars>. The secret itself is not sent over the network. - Bridge page. Telemt compares the capability with all vhost profiles in constant time. On a match it returns an HTML page with a script and a one-time bootstrap token valid for 120 seconds. The page runs inside the client’s WebView and exchanges data with it via
MessagePort. The CSP policy allows the page to connect only to its own host. - Session. The page sends
POST /api/v1/sessionwith the bootstrap token and receives a session token. - Data transfer. Outgoing data are sent via
POST /api/v1/uprequests with a sequence number in theX-Up-Seqheader. Incoming data the client receives via longGET /api/v1/down(long poll, default 25 seconds) with a cursor inX-Down-Cursor. In WebSocket modes this pair is replaced byGET /api/v1/ws. - Frames. Data are packed into frames with an 8-byte header: type, 24-bit stream identifier, length. Frame types:
OPEN,DATA,CLOSE,WINDOW,PING,PONG, controlHELLO,WELCOME,BYE. - Streams. One WEB session carries many logical streams. Each stream is a separate MTProxy connection: Telemt performs a standard MTProto handshake for it with the secret of the user specified in the profile and forwards the stream to the same relay that serves normal connections (directly to datacenters or via a Middle Proxy).
A request without valid credentials, with wrong format, or with an unknown path is forwarded by Telemt to the decoy site. Therefore externally the domain behaves like a regular website: / returns a page, a non-existent path returns this site’s 404.
Carriers
Carrier — the method by which frames are transferred between the bridge page and Telemt. There are four:
| Carrier | How it transfers | Requirements |
|---|---|---|
https | a single sequential channel: POST /up and long poll GET /down | HTTP/1.1 or HTTP/2 |
https-lanes | a separate “upload — long poll” pair per logical stream | HTTP/2 on the public side |
websocket | one WebSocket for all streams | HTTP/1.1 Upgrade on the public side |
websocket-lanes | a separate WebSocket per stream | HTTP/1.1 Upgrade |
The web.carrier parameter sets the default carrier. If web.carriers contains a non-empty array, the client and server will negotiate a carrier from that list when creating a session, and web.carrier remains the fallback.
Telegram for iOS supports only https. If clients on different platforms use the link, carrier = "https" is the only value that works everywhere. Negotiation is disabled in this case: the carriers parameter is not set.
Prerequisites
- Telemt version 3.5.1 or newer.
- A separate domain name (FQDN) with an A record pointing to the server’s public IP.
- Port 443 on that IP. The client does not accept another port:
tg://webproxylinks have no port. - A reverse proxy with a valid certificate: Caddy, NGINX or HAProxy.
- A decoy site: a directory with static files or an HTTP server on a private network.
- A Telegram client that supports proxy type
WEB.
If Telemt already runs in Fake TLS mode on port 443 on the server, the port must be split. Options are described in the section “Port 443: WEB and Fake TLS on one server”.
Step 1. Directories and secret
install -d -m 0750 /opt/telemt-web/config /opt/telemt-web/public
cd /opt/telemt-web
openssl rand -hex 16The secret is 32 hex characters, same as for a regular MTProxy. The dd prefix is not written in the config; Telemt will add it to the link itself.
Place the decoy site files in the public directory, at least index.html. Telemt reads the directory at startup and on config reload. Symbolic links and paths outside the directory are rejected.
Step 2. Config config/config.toml
[general]
use_middle_proxy = false
log_level = "normal"
[general.modes]
classic = false
secure = true
tls = false
[general.links]
show = ["web-user"]
[server]
port = 443
metrics_port = 9090
metrics_whitelist = ["127.0.0.1/32", "::1/128"]
[server.api]
enabled = true
listen = "0.0.0.0:9091"
whitelist = ["127.0.0.1/32", "172.30.0.0/24"]
auth_header = "Bearer replace-with-a-random-string"
read_only = false
[[server.listeners]]
ip = "0.0.0.0"
# WEB-listener: plain HTTP, only for the reverse proxy
[[server.listeners]]
ip = "0.0.0.0"
port = 18080
transport = "web"
proxy_protocol = false
reuse_allow = false
web_client_ip_source = "x_forwarded_for"
web_trusted_proxy_cidrs = ["172.30.0.2/32"]
[web]
enabled = true
carrier = "https"
[[web.vhosts]]
host = "proxy.example.com"
public_addr = "203.0.113.10:443"
[web.vhosts.decoy]
mode = "static_directory"
directory = "/var/lib/telemt/public"
index = "index.html"
[[web.vhosts.profiles]]
user = "web-user"
secret_mode = "dd"
max_sessions = 8
max_streams = 512
max_streams_per_session = 64
[access.users]
web-user = "0123456789abcdef0123456789abcdef"Parameter notes:
transport = "web"turns the listener into an HTTP receiver for WEB mode. For itproxy_protocol = falseandreuse_allow = falseare mandatory.web_trusted_proxy_cidrs— addresses of reverse proxies from which Telemt accepts theX-Forwarded-Forheader. The list cannot be empty; the/0network is rejected. In the example this is the Caddy container address. From other addresses the header is ignored.host— the lowercase domain name, without port. Telemt accepts theHostheader only asproxy.example.comorproxy.example.com:443. For any otherHostit returns 404 without contacting the decoy site.public_addr— the public IP that the domain points to, with port 443. This is the reverse proxy address, not Telemt’s. It enters the parameters of the internal MTProto route, so it must match the actual address that clients connect to.secret_mode—plainordd. Secrets of formateeare not supported in WEB mode.usermust exist in[access.users]. One user may have both WEB profiles and regular MTProxy links with the same secret.max_sessions,max_streams,max_streams_per_sessionlimit the profile. The limits apply to logical streams, not to HTTP connections.
The decoy site can also be served from a separate HTTP server:
[web.vhosts.decoy]
mode = "http_upstream"
upstream = "http://10.0.0.10:9010"Only http:// with an IP address from a private network, loopback or link-local is allowed in upstream. Domain names and Telemt’s public address are rejected.
Step 3. docker-compose.yml
Telemt and Caddy run in the same Docker network with fixed addresses. A fixed Caddy address is needed for web_trusted_proxy_cidrs.
services:
telemt:
image: ghcr.io/telemt/telemt:3.5.7
container_name: telemt
restart: unless-stopped
working_dir: /run/telemt
command: ["/etc/telemt/config.toml"]
ports:
- "127.0.0.1:9090:9090"
- "127.0.0.1:9091:9091"
volumes:
- ./config:/etc/telemt:rw
- ./public:/var/lib/telemt/public:ro
tmpfs:
- /run/telemt:rw,mode=1777,size=4m
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
read_only: true
security_opt:
- no-new-privileges:true
ulimits:
nofile:
soft: 65536
hard: 262144
networks:
web:
ipv4_address: 172.30.0.3
caddy:
image: caddy:2
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
networks:
web:
ipv4_address: 172.30.0.2
networks:
web:
ipam:
config:
- subnet: 172.30.0.0/24
volumes:
caddy_data:Port 18080 is not published externally: the WEB-listener accepts HTTP without encryption and transport-level authentication, so only the reverse proxy must have access to it. The config directory is mounted writable because the Control API overwrites the file when configuration changes.
Step 4. Caddy
proxy.example.com {
reverse_proxy telemt:18080
}Caddy will obtain the certificate by itself. No additional settings are required for the following reasons:
- Caddy replaces the incoming
X-Forwarded-Forwith the client’s address if the client is not listed intrusted_proxies. Telemt receives a single address, as required. - The connection to Telemt uses HTTP/1.1, WebSocket Upgrade is passed without extra configuration.
- Retries on upstream error are disabled by default. They must not be enabled: the bridge page retries requests itself, and retries on the reverse proxy would break sequencing.
- Request logging is disabled by default. Do not enable it for this site: the query string contains the capability, and the
Authorizationheader contains tokens.
Telemt receives the entire site. If the reverse proxy sends only /api/v1/* paths and requests with ?bridge= to Telemt and serves the rest itself, responses to ordinary and service requests will be produced by different servers, and this difference can be detected externally. Splitting paths is acceptable when a real site already runs on the domain and cannot be moved behind Telemt. In that case the same site should be configured as http_upstream in [web.vhosts.decoy] so that rejected service requests get its responses.
Step 5. NGINX variant
Configuration from Telemt documentation. The map block is placed in the http context.
map $http_upgrade $telemt_connection_upgrade {
default upgrade;
'' '';
}
upstream telemt_web {
server 127.0.0.1:18080;
keepalive 64;
}
server {
listen 443 ssl;
http2 on;
server_name proxy.example.com;
access_log off;
ssl_certificate /etc/letsencrypt/live/proxy.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/proxy.example.com/privkey.pem;
client_max_body_size 2m;
location / {
proxy_pass http://telemt_web;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $telemt_connection_upgrade;
proxy_connect_timeout 5s;
proxy_send_timeout 65s;
proxy_read_timeout 65s;
proxy_request_buffering off;
proxy_buffering off;
proxy_next_upstream off;
}
}Key points here:
X-Forwarded-Foris set via$remote_addr, not$proxy_add_x_forwarded_for. Telemt accepts exactly one address.client_max_body_sizemust be at leastweb.limits.max_body_bytes(default 2 MiB).- Read and send timeouts should be longer than the long poll interval. 65 seconds covers the defaults.
proxy_next_upstream offandaccess_log off— for the same reasons as described for Caddy.- For
https-lanesHTTP/2 is required on the public side; for WebSocket HTTP/1.1 availability is required.
If NGINX runs on the host and Telemt in Docker, publish the listener port only to loopback (127.0.0.1:18080:18080) and specify the Docker bridge gateway address in web_trusted_proxy_cidrs — in the example above it’s 172.30.0.1/32. When installing Telemt without Docker the listener is placed on 127.0.0.1, and 127.0.0.1/32 should be in the trusted list.
Step 6. Start and the link
cd /opt/telemt-web
docker compose up -d
docker compose logs telemt | grep -A2 "WEB proxy links"A link will appear in the log:
MAESTRO: WEB proxy links
MAESTRO: User: web-user (Dd)
MAESTRO: WEB: tg://webproxy?server=proxy.example.com&secret=dd0123456789abcdef0123456789abcdefLinks are printed for users from [general.links].show and only at full process startup. After adding a profile via config reload the link must be assembled manually: tg://webproxy?server=<domain>&secret=dd<secret>. For secret_mode = "plain" the prefix is not added.
The line Listening on TCP endpoint addr=0.0.0.0:18080 transport=Web in the log confirms the listener is up.
Step 7. Checks
Externally the domain should behave like a normal site:
curl -sI https://proxy.example.com/ # 200, decoy site
curl -sI https://proxy.example.com/no-such-page # 404 of the decoy site
curl -sI 'https://proxy.example.com/?bridge=fake' # 200, decoy site
curl -s -o /dev/null -w '%{http_code}\n' \
-X POST https://proxy.example.com/api/v1/session # 404, no tokenA 404 on POST /api/v1/session without a token is expected behavior, not an error.
WEB mode status is available in the Control API:
export TELEMT_API_AUTH="Bearer replace-with-a-random-string"
curl -s http://127.0.0.1:9091/v1/runtime/web/status \
-H "Authorization: ${TELEMT_API_AUTH}" | jq '.data | {lifecycle, listeners, ingress}'Working state: lifecycle equals running, ingress.accepting_connections equals true. The list of active sessions — GET /v1/runtime/web/sessions.
Prometheus metrics for WEB mode are prefixed with telemt_web_. For monitoring, four are sufficient: telemt_web_tcp_accept_total, telemt_web_session_incarnations_total, telemt_web_streams_total, telemt_web_carrier_bytes_total.
The final check — add the link to the Telegram client and ensure the connection is established and messages and media are transferred.
Port 443: WEB and Fake TLS on one server
Fake TLS mode from the article “Telemt: installing MTProxy for Telegram in Docker on port 443 with Fake TLS” has Telemt itself occupy port 443, while WEB mode requires the same port for the reverse proxy. On a single IP this is solved by routing by SNI before TLS is terminated. Connections with the mask domain SNI go to Telemt, connections with your domain SNI go to the reverse proxy.
Example for NGINX stream module:
stream {
map $ssl_preread_server_name $backend_443 {
github.com 127.0.0.1:8443; # Telemt, Fake TLS
proxy.example.com 127.0.0.1:4443; # Caddy or NGINX http
default 127.0.0.1:4443;
}
server {
listen 443;
ssl_preread on;
proxy_pass $backend_443;
}
}Telemt and the reverse proxy in this setup listen on ports 8443 and 4443 on loopback. The reverse proxy behind such a router sees address 127.0.0.1 instead of the client address. To make IP-based limits in Telemt work, the client address must be passed via PROXY protocol: proxy_protocol on; in the server block of the stream module and accept PROXY protocol on the reverse proxy and the Fake TLS listener.
The SNI list in the router and the tls_domain value in Telemt are maintained separately. When changing the mask domain both must be updated.
Other options — a second IP on the server or a separate server for WEB mode.
Separate front scheme
There is a variant with a separate front: the public address and the certificate live on one server, while Telemt runs on another one in a private network and has no public address. The scheme is:
client --> front (public IP, Caddy, certificate)
| private network
v
Telemt server, listener 10.0.0.58:18080
|-- relay --> Telegram
`-- decoy --> http://10.0.0.10:9010 (site in the same network)Differences from a single-server installation:
- The listener is placed on a private network address, not loopback:
ip = "10.0.0.58". - The firewall on the Telemt server allows port 18080 only from the front’s address. The front’s address with
/32mask is specified inweb_trusted_proxy_cidrs. public_addrcontains the front’s public IP.- The decoy site is a separate NGINX container with a static page, connected as
http_upstream. - One listener serves multiple domains: one
[[web.vhosts]]block per domain, selection is byHostheader. On the front areverse_proxy 10.0.0.58:18080block is sufficient for each domain.
Operational findings
Change of public IP. If the front’s address changes, public_addr must be updated immediately after the change. This is done via the Control API without restart:
curl -s http://127.0.0.1:9091/v1/config -H "Authorization: ${TELEMT_API_AUTH}" \
| jq '.data.web.vhosts' > vhosts.json
# change public_addr in vhosts.json
jq -n --slurpfile v vhosts.json '{web: {vhosts: $v[0]}}' \
| curl -s -X PATCH 'http://127.0.0.1:9091/v1/config?reload=drain&timeout_secs=30' \
-H "Authorization: ${TELEMT_API_AUTH}" \
-H 'Content-Type: application/json' -d @-Without the reload parameter the request only writes the file and returns runtime_reload_required: true. With reload=drain Telemt immediately applies the new configuration and responds with 202; the response should have restart_required: false.
Nested tables in PATCH are merged by fields, while arrays are replaced entirely. A request that supplies only public_addr for a vhost will remove its profiles and decoy site. Therefore the current web.vhosts array is read first, one field is changed, and the entire array is sent back.
What requires a restart. The listener composition and all [web.limits] values are applied only on process restart. Everything else — vhost, profiles, decoy site, carrier, timeouts — is applied by config reload:
curl -s -X POST http://127.0.0.1:9091/v1/system/reload \
-H "Authorization: ${TELEMT_API_AUTH}" \
-H 'Content-Type: application/json' \
-d '{"mode":"drain","timeout_secs":30,"failure_policy":"rollback"}'If the response field deferred_process_fields contains server.listeners or web.limits, the configuration is saved but these parameters will take effect after restart.
Profile count limit. When adding WEB profiles for all instance users (about 80) the process failed to start with WEB profiles exceed web.limits.max_profiles. The limit must be raised and the process restarted:
[web.limits]
max_profiles = 200Control API. While the API listened only on loopback it worked without a token. When access from the front was needed, auth_header and whitelist with specific addresses were added. Whitelist checks the TCP connection address and does not consider X-Forwarded-For.
Revoking access. The request POST /v1/users/<name>/disable immediately closes active WEB sessions for the user. After rotating the secret (rotate-secret) capabilities are recalculated automatically and the old link stops working.
Multiple processes. Bootstrap token and session registries are stored in a single process’s memory. If multiple Telemt instances sit behind one domain, all requests from one client must reach the same instance.
Typical errors
| Symptom | What to check |
|---|---|
| Client does not accept the link | The link must not contain a port. The domain is a valid FQDN, externally on port 443, secret mode plain or dd. |
Every request returns 404 not found 10 bytes long | Host header does not match host in [[web.vhosts]]. Only domain and domain:443 are allowed. The reverse proxy must forward the original Host. |
| A correct link does not work, requests go to the decoy site | Match of host, secret mode in the link and in the profile, reverse proxy address in web_trusted_proxy_cidrs, a single address in X-Forwarded-For. |
| A self-signed certificate is served instead of the domain certificate | The domain is not included in SNI routing before the reverse proxy and falls to the default backend. |
| Connection drops at regular intervals | Reverse proxy timeouts are less than web.timeouts.long_poll_secs. |
| WebSocket does not establish, decoy site response received | Reverse proxy does not forward Connection: Upgrade, Upgrade: websocket and Sec-WebSocket-Protocol unchanged. |
| Configuration changed, listener behavior unchanged | Listener and [web.limits] changes require restart, see deferred_process_fields. |
| Does not connect on iOS, works on other platforms | Set carrier = "https". |
After DNS creation curl reports Could not resolve host, while dig shows the address | Negative result is still in the OS DNS cache. On macOS: sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder. |
Parameter reference
| Parameter | Purpose |
|---|---|
[[server.listeners]].transport = "web" | HTTP listener for WEB mode. |
web_client_ip_source = "x_forwarded_for" | Source of the client address. No other values. |
web_trusted_proxy_cidrs | Addresses of reverse proxies allowed to pass X-Forwarded-For. |
[web].enabled | Enables WEB mode. |
[web].carrier | Default carrier: https, https-lanes, websocket, websocket-lanes. |
[web].carriers | Array for carrier negotiation. Missing or false — negotiation disabled. |
[[web.vhosts]].host | Vhost domain. |
[[web.vhosts]].public_addr | Public IP of the domain with port 443. |
[web.vhosts.decoy] | Decoy site: static_directory or http_upstream. |
[[web.vhosts.profiles]] | User, secret mode and profile limits. |
[web.limits] | Limits for memory, connections, sessions and profiles. Applied on restart. |
[web.timeouts] | Timeouts, including long_poll_secs = 25 and bootstrap_lifetime_secs = 120. |
[web.debug] | Collect diagnostics for the /web-status page in the Control API. |
Full parameter list is in Telemt documentation: docs/WEB/WEB_PROXY.en.md and docs/Config_params/CONFIG_PARAMS.en.md. The Russian version lags behind English; consult the English one.
Summary
WEB mode moves MTProto into regular HTTPS on your own domain. TLS is terminated by the reverse proxy with a real certificate, Telemt accepts HTTP from it and serves a decoy site for everything that fails verification. Requirements: a domain, port 443, a listener with transport = "web", a [[web.vhosts]] block with correct public_addr, and a reverse proxy that forwards the whole site to Telemt. If Fake TLS remains on the server, port 443 is split by SNI routing. Telegram calls via MTProxy do not work in this mode.
Need a turnkey web proxy for Telegram?
I will set up Telemt in WEB mode, reverse proxy, decoy site and monitoring. Write to me — I'll reply on a business day.
Написать в Telegram →// Reviews
Related reviews
The collaboration left an extremely positive impression, primarily because of the professionalism and the approach to resolving issues as they arose.
The experience of working together left an extremely positive impression, above all because of the professionalism and the approach to solving the issues that arose.
Jitsi Meet: a personal Zoom — setup in Docker and on a VPS
2025-11-11 · ★ 5/5
I needed to get n8n, Redis, and the database working. I had hired another contractor before and everything kept breaking. I hired Mikhail, and the next day everything was working quickly, like clockwork!
There was a task to get n8n, redis and the database working. I had previously ordered from another contractor, it kept breaking all the time. Ordered from Mikhail, the next day everything started working fast, like …
n8n installation on your VPS server. Configuration of n8n, Docker, AI, Telegram
2025-09-24 · ★ 5/5
Thank you for the fast and excellent work. Everything was done promptly and just as needed!
Thank you for the quick and good work. Everything was done promptly and as needed!
n8n installation on your VPS server. Configuration of n8n, Docker, AI, Telegram
2025-09-06 · ★ 5/5
Quick solution — I highly recommend Mikhail as a contractor! I tried to build a similar configuration myself and even followed AI advice, which ended up costing a lot of time and money (due to server downtime). So my advice: hire professionals — it's cheaper =) Thanks to Mikhail for his professionalism.
Quick fix for the problem, I recommend Mikhail as a contractor to everyone! I tried to assemble a similar configuration myself and following advice from neural networks, which resulted in a lot of wasted effort and …
n8n installation on your VPS server. Configuration of n8n, Docker, AI, Telegram.
2025-08-25 · ★ 5/5
Mikhail completed the setup of another VPS. He quickly and professionally bypassed certain hosting providers' restrictions.
Mikhail completed the setup of another VPS. Quickly, professionally bypassing certain limitations of hosting providers.
n8n installation on your VPS server. n8n, Docker, AI, Telegram setup
2025-08-12 · ★ 5/5
Great job, thank you! Mikhail is a true professional — I recommend him!
Excellent work, thank you! Mikhail is a professional in his field, I recommend him!
N8n installation on your VPS server. Setup of n8n, Docker, AI, Telegram
2025-07-03 · ★ 5/5
// Contact
Need help?
Get in touch with me and I'll help solve the problem
I reply within one business day (03:00-13:00 GMT)
Или оставьте заявку здесь:
// Related