MTProto, VLESS+Reality and a control panel — all on port 443, masked behind a real website you actually own.
Architecture · Setup · Troubleshooting · Credits · Русский
There's a known fix for MTProto proxies: an SYN rate limit in nftables, curing the reconnect storms Telegram clients hit under censorship. With one proxy on the box it's a one-line rule.
Add a VPN behind nginx's SNI routing on that same port 443, and the fix
has nowhere to go. Limiting :443 ahead of nginx can't distinguish MTProto
from VPN — no SNI exists yet at SYN time, so a ceiling sized for MTProto
throttles VPN traffic too. Limiting a local port behind nginx instead sees
only 127.0.0.1, since nginx already terminated the client's connection
and opened its own — a per-IP rule stops meaning anything.
Classify in the kernel, ahead of nginx — don't rate-limit there. nftables'
redirect is DNAT, not a proxy: it rewrites the destination port on the
same packet, so the client's real SYN, JA4T fingerprint included, survives
intact. iOS gets steered by that fingerprint past the HAProxy gate;
everyone else goes through nginx, where HAProxy limits by the real IP via
PROXY protocol. The limit lives on the MTProto branch, not the port — VPN
never touches it, and unauthenticated connections fall through to a real
site with a genuine Let's Encrypt certificate.
flowchart LR
C["Client<br/>always :443"] --> J{"nftables<br/>TCP SYN fingerprint"}
J -->|"iOS → :8443"| N["nginx<br/>listens on both ports<br/>parses SNI"]
J -->|"everyone else → :443"| N
N -->|"MTProto from :443"| H["HAProxy<br/>54 per 60s limit"]
H --> T["telemt · MTProto"]
N -->|"MTProto from :8443"| T
N -->|"VPN"| X["Xray · Reality"]
N -->|"panel"| P["3x-ui"]
N -->|"SNI unrecognized"| D["Connection dropped"]
T -.->|"no secret"| F["Decoy — a real website"]
X -.->|"unauthenticated"| F
The client always connects on :443 — :8443 only exists as the result of an in-kernel redirect (nftables), not as a separate entry point. The redirect exists to move iOS clients out from under the HAProxy limit that was choking them; iOS itself has no rate limit now. The HAProxy limit of 54 connections per 60 seconds applies only to non-iOS MTProto traffic. Full detail in 01-architecture.md.
Worth knowing even if you never build this stack.
- Classification beats rate-limiting: at SYN time there's no SNI yet, so a single ceiling either misses MTProto's reconnect storms or clips VPN traffic with a different profile — you need to know what a connection is before deciding whether to throttle it.
redirectin nftables is DNAT, not a proxy — it rewrites the destination port on the same packet, so the original SYN, JA4T fingerprint included, reaches the far side intact. A terminating proxy would erase it.- Because nginx re-originates every backend connection from
127.0.0.1, PROXY protocol isn't optional for anything behind it — HAProxy, telemt, and Xray all read the real client IP off the PROXY header, or a per-IP limit quietly becomes a per-nginx-process limit. - Self-steal masking falls out of the same design rather than being bolted on: unauthenticated connections land on a real site the operator owns, with a genuine Let's Encrypt certificate.
The full guide is written in Russian — read the steps in order.
| Step | |
|---|---|
| 01 | Architecture |
| 02 | Install |
| 03 | Certificates |
| 04 | Services |
| 05 | Troubleshooting |
All read-only — they inspect state, never change it.
sudo ./scripts/check-stack.sh # every layer at once
sudo ./scripts/diagnose-reality.sh 200 plug2.example.com # classify sessions from the logThe JA4T split builds on findings from two other projects, and the MTProto layer runs entirely on telemt. Full attribution in CREDITS.md.