MTproxy-reanimation

title
MTproxy-reanimation
type
toolbox
summary
nftables SYN limiter and server-side TCP desync for Telemt MTProto proxies, driven from one bash menu
tags
shell, networking, censorship-circumvention, dpi, nftables, telegram, watchlist
language
Shell (bash)
license
MIT
created
2026-07-29
updated
2026-07-29

MTproxy-reanimation ("Ρ€Π΅Π°Π½ΠΈΠΌΠ°Ρ‚ΠΎΡ€") fixes one specific failure: a Telegram MTProto proxy whose clients cannot complete the first TCP connection. The proxy itself works; the handshake is what gets killed. The tool is a bash menu called mtpr that installs nftables rules to survive that, plus a small amount of safe tuning for Telemt and MTProxyMax. It does not replace either β€” it sits beside them and manages kernel-level rules and a few config values.

Installation locates the proxy on its own: MTProxyMax, a Docker container, a systemd unit, or a bare local process. It reads the Telemt config to find the API port ([server.api], default 9091), backs the config up before touching it, and offers to restore from that backup on uninstall. Telemt Panel configs are explicitly left alone.

Picking the netfilter hook

The first non-obvious thing it does is choose the hook by deployment shape. Telemt on the host or on host networking gets rules in input. Telemt in a Docker bridge network gets forward, because the packets are being routed to the container rather than delivered locally. For the bridge case there are two variants: a simple mode that matches the port only, and a precise Docker mode that matches the container's internal IP and runs a watcher service (mtpr-bridge-watch.service) to follow it when the address changes. The README's advice is to try precise first.

SYN limiter: Classic and Smart

The original mechanism is a per-source inbound SYN rate limit. Classic mode is one meter for everyone:

tcp dport <PORT>
tcp flags & (syn | ack) == syn
meter { ip saddr timeout 60s limit rate over 1/second burst 1 packets }
counter drop

Limiting inbound connection attempts to fix a connectivity problem reads backwards until you see what it is for. A Telegram client under interference retries the handshake aggressively across several candidate paths; throttling the flood makes it settle on a working one faster instead of thrashing. The Classic mode's weakness is that it uses drop, so a rate-limited client sits in a retransmit timeout and connection setup takes 10–20 seconds.

Smart By-MEKO, the recommended mode, splits clients into two branches and changes the rejection. iOS clients are identified by a TCP SYN fingerprint β€” raw offset matches into the TCP header, matching option layout and window values rather than heuristics:

@th,108,20 0x2ffff  @th,160,16 0x204  @th,192,16 0x103
@th,224,24 0x10108  @th,320,32 0x4020000

iOS gets a loose meter (rate 15/second, burst 30) and a TCP reset when it exceeds it; everything else gets a strict one (rate 54/minute, burst 1) and, by default, reject with icmp type host-unreachable. Both branches share a single port. The point of ICMP over drop is that Telegram treats an explicit rejection as a fast failure and moves to another connection immediately, which the README credits with cutting connect time to 3–8 seconds and, specifically, removing a long stall when sending media from Android and Desktop clients. Either branch's limit can be disabled outright. An older fingerprinting method β€” ip ttl < 65 AND meta length 64 β€” is kept selectable for compatibility.

The fingerprint approach came from MTPROTO-FIX-By-MEKO, which the README credits for both the iOS/Android split and the fast-reject idea.

Zapret2 mode: the server does the desync

Version 1.2.0 (July 2026) added a different mechanism entirely, built on zapret2's nfqws2 packet mangler with Lua scripts. It replaces the SYN limiter rather than supplementing it β€” turning it on disables the limiter automatically.

The sequence, all executed on the server against its own outbound packets:

  1. The SYN+ACK goes out with window = 1400, so the client is forced to fragment its first data packet instead of sending the ClientHello whole.
  2. Empty ACKs from the server carry window = 10 until the client has actually sent payload, keeping the pressure on.
  3. The client's first data packet β€” the Fake-TLS ClientHello β€” is cut into three pieces. The first goes out normally, the third goes out normally at its offset, and the middle one goes out with a deliberately corrupt checksum.
  4. A middlebox sees an incomplete, out-of-order ClientHello it cannot parse, and the corrupt segment is dropped before it reassembles.
  5. The client retransmits the middle piece, the connection completes, and everything after that passes untouched β€” released via fwmark plus conntrack marks and an instance_cutoff in the Lua script.

This is the mirror image of tls-desync-fake-clienthello. There, the client confuses the DPI by sending a decoy ClientHello with a low TTL before the real one; here the server confuses it by making sure no complete ClientHello ever crosses the middlebox in one readable piece. Same target β€” the SNI-inspecting path in something like tspu β€” opposite end of the wire. It shares the split/disorder/badsum vocabulary with the wider zapret family, and unlike gecit's eBPF approach it needs nothing installed on the client.

iOS clients are exempted: they are fingerprinted at SYN, marked with fwmark = 0x40000, and sent through rawsend_dissect_segmented without window manipulation, because the window games degrade their connections. Earlier versions just passed them (VERDICT_PASS), which let the packets loop back into the netfilter queue.

The nftables table is four chains, arranged so that already-processed packets skip requeueing:

table ip MTProto {
    chain predefrag   # output priority -401: fwmark accept + notrack
    chain output      # route mangle: ct mark set for marked packets
    chain postrouting # srcnat+1: ct mark accept + queue
    chain prerouting  # mangle: ct mark accept + queue
}

The wscale constraint

The bypass only works if the advertised receive window is genuinely small, and TCP window scaling stands in the way. The effective window is win_ACK Γ— 2^wscale, and the kernel picks wscale from the receive buffer size, so a server tuned for throughput cannot advertise a small enough window at all:

wscale 2^wscale usable win ACK effective window
7 128 10 1280 bytes
9 512 2 1024 bytes
11 2048 β€” impossible

At wscale β‰₯ 11 (a 64 MB+ buffer) there is no integer that lands under 1400, and the installer prints instructions for shrinking net.core.rmem_max instead. This check runs at install and again under the diagnostics menu item, and it has to be redone whenever kernel buffer settings change. It is a nice example of a circumvention technique whose viability is decided by an unrelated sysctl.

Tuning and the rest of the menu

Beyond the netfilter work it manages three Telemt timeouts β€” tg_connect 30, client_handshake 90, client_keepalive 120 β€” and the client_mss / client_mss_bulk values, which the README recommends disabling under both Smart and Zapret2 modes since they solve the same problem worse. It pulls per-user tg://proxy?... links from the Telemt API through jq, filtering out IPv6 entries and honouring public_host/public_port. The two older iOS workarounds (TCP keepalive sysctls; a separate port 4443 with MSS=92 and a transparent redirect) have been demoted to a "legacy settings" submenu, obsoleted by both current modes.

One piece of advice in the README is worth extracting because it has nothing to do with the tool: the domain used for Fake-TLS must support post-quantum hybrid key exchange (X25519MLKEM768), or iOS clients will not connect at all. No amount of nftables tuning fixes that.

Caveats

The repository was created on 2026-06-10 and is one author's bash, at 294 stars and 5 forks. It installs via curl | sudo bash, writes systemd units, and edits kernel-level firewall rules on a live server β€” read the script first. Everything above is the README's account, in Russian, of behaviour on the author's own deployments; there are no independent measurements of the 3–8 second figure or the media-sending stall. Docker bridge mode is untested against zapret2 per the README's own compatibility note. And the moving target is upstream: Telemt 3.4.18+ added features that overlap what the reanimator does, and the README says to pick one or the other rather than run both.

Tagged watchlist β€” see watchlist. Censorship-circumvention tooling has a short half-life, and this one is pinned to a single proxy implementation that is absorbing its functionality.

For the client side of the same problem, see xray-tutorial and happ-proxy; for routing across the resulting proxies, mihomo and wirez.

MIT, ~294β˜… as of 2026-07-29.