Skip to content

Latest commit

 

History

History
124 lines (95 loc) · 4.86 KB

File metadata and controls

124 lines (95 loc) · 4.86 KB
mtproto-reality-stack

MTProto, VLESS+Reality and a control panel — all on port 443, masked behind a real website you actually own.


License: MIT Ubuntu 26.04 Xray-core nftables shellcheck

Architecture · Setup · Troubleshooting · Credits · Русский


The problem

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.

The fix

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.

Architecture

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
Loading

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.

Key findings

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.
  • redirect in 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.

Setup

The full guide is written in Russian — read the steps in order.

Step
01 Architecture
02 Install
03 Certificates
04 Services
05 Troubleshooting

Scripts

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 log

Credits

The JA4T split builds on findings from two other projects, and the MTProto layer runs entirely on telemt. Full attribution in CREDITS.md.

License

MIT