Skip to content
Alexey Dolotov edited this page Apr 25, 2026 · 1 revision

If you want to run a Telegram MTProto proxy in 2026, you have a small shortlist. This page is a source-verified comparison so you can pick the right tool. Every capability claim has a file:line citation against a pinned commit; you can reproduce it yourself.

Pinned commits used for this comparison (verified 2026-04-25 against each project's default branch):

Project Repo Default branch Commit Date
mtg github.com/9seconds/mtg master e5ce720a2d7c3f552dbde95aefb5fb6752e30ab0 2026-04-21
telemt github.com/telemt/telemt main 8874396ba5c04a293eaa774893afba0f31ab37c5 2026-04-24
sing-box github.com/SagerNet/sing-box testing b2d5fb62290b9a648a04693a2d32ebf253d684ae 2026-04-24

Important up-front note about sing-box. sing-box has no MTProto inbound or outbound as of b2d5fb62. GitHub code search (q=mtproto+repo:SagerNet/sing-box) returns 0 hits, and protocol/ lists 24 protocols: anytls, block, cloudflare, direct, dns, group, http, hysteria, hysteria2, mixed, naive, redirect, shadowsocks, shadowtls, socks, ssh, tailscale, tor, trojan, tuic, tun, vless, vmess, wireguard. None is MTProto.

sing-box is included here as a reference point for "what people sometimes pair with an MTProto proxy" — usually as a SOCKS5 upstream — not as a competitor. The two real choices for the proxy itself are mtg and telemt. The official TelegramMessenger/MTProxy is the third realistic option but it has been effectively unmaintained for years and is mentioned only in passing.


1. Overview

mtg

  • Repo: https://github.com/9seconds/mtg
  • Language: Go
  • License: MIT (LICENSE:1)
  • Original author: Sergey Arkhipov (9seconds).
  • Last commit on master at time of writing: e5ce720 (2026-04-21).
  • Releases in the last 90 days (since 2026-01-25): 15 tagged releases, latest v2.2.8 (2026-04-07).
  • Scope: dedicated MTProto proxy, FakeTLS-only. Legacy simple/secured modes are deliberately rejected (see example.config.toml:18-21).

telemt

  • Repo: https://github.com/telemt/telemt
  • Language: Rust (Tokio async runtime)
  • License: TELEMT LICENSE 3.3 — that is the literal title in the file (LICENSE:1: ######## TELEMT LICENSE 3.3 #########). Permissive, MIT-like wording, not OSI-approved. Double-check with your legal team if that matters.
  • Maintainers: GitHub org telemt, pseudonymous.
  • Last commit: 8874396 (2026-04-24).
  • Releases in the last 90 days (since 2026-01-25): 30 tagged releases, latest 3.4.6 (2026-04-24).
  • Scope: MTProto proxy supporting all three official modes (Classic, Secure with dd prefix, FakeTLS with ee prefix — README.md:35-38), with multi-user support, per-user ad-tags, hot reload, and a heavy investment in masking-shape hardening.

sing-box

  • Repo: https://github.com/SagerNet/sing-box
  • Language: Go
  • License: GPLv3-or-later (LICENSE:1-6)
  • Maintainer: nekohasekai and contributors; very active.
  • Last commit: b2d5fb6 (2026-04-24).
  • Does not implement MTProto. Useful as a SOCKS5 upstream in front of mtg/telemt for traffic shaping or as a Telegram client-side proxy chain element, but it cannot terminate Telegram clients.

MTProxy (official, for context)


2. Feature matrix

Legend: implemented = yes, partial = caveat applies, no = not present, n/a = not applicable.

Capability mtg telemt sing-box
MTProto Classic (no obfuscation) no yes n/a (no MT)
MTProto Secure (dd prefix) no yes n/a
MTProto FakeTLS (ee prefix, ClientHello) yes yes n/a
FakeTLS server-side cert/length emulation partial: static yes: dynamic n/a
Active-probe fallback (relay on bad probe) yes yes (masking) n/a
Multi-user / multi-secret on one port no (see mtg-multi fork) yes n/a
Per-user ad_tag no yes n/a
Replay protection yes yes n/a
IPv6 listen + dial yes yes yes
PROXY protocol (HAProxy front-end) yes yes yes
Hot config reload no (restart) partial no (restart)
Stats / Prometheus metrics yes (Prom+statsd) yes (Prom HTTP) partial (Clash API)
Built-in IP blocklist yes (FireHOL) partial (mask) yes (rule-set)
Docker image yes (official) yes (official) yes (official)

The rest of this section justifies each row with file:line citations.

2.1 MTProto modes

mtg is FakeTLS-only. example.config.toml:18-21:

A secret. Please remember that mtg supports only FakeTLS mode, legacy simple and secured mode are prohibited. For you it means that secret should either be base64-encoded or starts with ee.

This is a deliberate hardening choice — Classic and Secure modes are trivially fingerprintable.

telemt supports all three modes, gated by config (config.toml:18-21):

[general.modes]
classic = false
secure = false
tls = true

2.2 FakeTLS handshake fidelity

mtg's FakeTLS path uses a fixed fake certificate / fake ServerHello keyed off the secret's SNI: fake.SendServerHello is called at mtglib/proxy.go:213. Record-length noise comes from the doppelganger (mtglib/proxy.go:210-211).

telemt has real-cert TLS emulation gated at src/proxy/handshake.rs:1441 (if config.censorship.tls_emulation { ... }). Config knobs at config.toml:55-57:

mask = true
tls_emulation = true         # Fetch real cert lengths and emulate TLS records
tls_front_dir = "tlsfront"   # Cache directory for TLS emulation

In practice this means telemt's TLS surface is a closer impostor of the declared domain, at the cost of a periodic outbound fetch. mtg's approach is simpler and never reaches out except on actual fronting, which some operators prefer.

2.3 The "domain fronting" / "masking" fallback

This is the row this whole page exists to set straight. See section 5 below for the long-form rebuttal. Short version:

  • mtg: on a failed FakeTLS handshake (bad ClientHello, replay detected), the connection is rewound and transparently relayed to the secret's host on port 443. Code: doDomainFronting at mtglib/proxy.go:296-329, called from mtglib/proxy.go:199 (bad ClientHello) and mtglib/proxy.go:206 (replay hit).

  • telemt: same idea, more knobs. handle_bad_client at src/proxy/masking.rs:625, dispatched from the handshake state machine via HandshakeResult::BadClient at src/proxy/client.rs:646, :675, :743, :1247, :1285, :1385. Configurable actions on unknown SNI: Drop, Mask, RejectHandshake, Accept (src/proxy/handshake.rs:1139-1198). Includes log-normal timing normalisation (src/proxy/masking.rs:259-313, sample_lognormal_percentile_bounded) and shape-padding to make the fronted stream's byte volume statistically harder to fingerprint (src/proxy/masking.rs:148-211, maybe_write_shape_padding).

  • sing-box: nothing to compare — no MTProto inbound.

Both mtg and telemt RELAY the failed connection to a real backend. Neither just closes the socket.

2.4 Multi-user / multi-secret

mtg is single-secret by design. mtglib/proxy.go:41 (secret Secret) — a single struct, not a map. The upstream README explains the rationale and explicitly points readers who need multi-secret to the mtg-multi fork, which keeps the upstream codebase intact and adds a secret table on top (README.md:90-97). Without that fork, the alternative is to run multiple mtg instances behind an SNI router.

telemt supports multi-user natively. config.toml:59-61:

[access.users]
# format: "username" = "32_hex_chars_secret"
hello = "00000000000000000000000000000000"

The handshake walks the user table looking for a key that decrypts the ClientHello — decode_user_secrets_in at src/proxy/handshake.rs:831, called from :1394 and :1819. It also has a constant-time backoff on auth-probe failures keyed by client IP (src/proxy/handshake.rs:46-70, AUTH_PROBE_* constants), which is a concrete defence telemt has and mtg does not.

2.5 Per-user ad_tag

telemt: yes. [access.user_ad_tags] is hot-reloadable (src/config/hot_reload.rs:10, :121, :828-831).

mtg: not in the open-source upstream.

2.6 Replay protection

mtg uses a stable Bloom filter keyed on the ClientHello SessionID: antireplay/stable_bloom_filter.go:16 (SeenBefore), integrated at mtglib/proxy.go:203-208 — on a hit, the connection is treated like a failed handshake and routed to domain fronting (note: NOT closed).

telemt has a ReplayChecker struct at src/stats/mod.rs:2490 with its impl block at :2582 and explicit tests for basic, duplicate, expiration, and zero-window behaviours at :2898-2924, plus a dedicated adversarial test file src/stats/tests/replay_checker_security_tests.rs.

Both are functional. telemt's checker is more aggressively tested in adversarial scenarios; mtg's has been in production for years.

2.7 IPv6

Both bind to :: happily.

mtg: prefer-ip = "prefer-ipv6" in example.config.toml:49 selects the dial preference; listen is just bind-to = "[::]:port".

telemt: explicit IPv6 detection state in src/network/probe.rs:22-30 (struct NetworkProbe with fields detected_ipv6, reflected_ipv6, ipv6_is_bogon, ipv6_nat_detected, ipv6_usable). Multi-listener config at config.toml:46-47 ([[server.listeners]]).

2.8 PROXY protocol

mtg: opt-in listener-side flag at example.config.toml:33 (proxy-protocol-listener); plus PROXY-v1/v2 emission to fronting backend via mtglib/proxy.go:310-311 (newConnProxyProtocol), gated on domainFrontingProxyProtocol.

telemt: emits PROXY v1 or v2 to the masking backend depending on config (src/proxy/masking.rs:597-622, build_mask_proxy_header).

2.9 Hot reload

mtg requires a restart for any config change. No file watcher, no SIGHUP handler.

telemt has a partial hot-reload subsystem. See src/config/hot_reload.rs:1-23:

Hot-reload: watches the config file via inotify (Linux) / FSEvents (macOS) / ReadDirectoryChangesW (Windows) using the notify crate. SIGHUP is also supported on Unix as an additional manual trigger.

What can be reloaded without restart: | Section | Field | Effect | | general | log_level | Filter updated | | access | user_ad_tags | Next connection | | general | ad_tag | Next connection | | general | desync_all_full | Applied immediately | | network | dns_overrides | Applied immediately | | access | All user/quota fields | Effective immediately |

Listener bindings, censorship/TLS settings, and network family selection are NOT hot — they require restart, and the daemon emits a warning when you try. This is conservative behaviour, not a bug.

2.10 Stats / metrics

mtg ships both Prometheus and statsd integrations:

  • stats/prometheus.go
  • stats/statsd.go

Each is enabled in config under [stats.prometheus] (example.config.toml:372-380) or [stats.statsd] (example.config.toml:359-369).

telemt has a built-in HTTP /metrics endpoint (Prometheus exposition format) plus a custom /beobachten endpoint that publishes classifier statistics. See src/metrics.rs:48:

info!("Metrics endpoint: http://{}/metrics and /beobachten", addr);

Both can be scraped by a standard Prometheus. telemt exposes more internal counters relevant to censorship-resilience tuning; mtg exposes the classic proxy KPIs (active streams, traffic, replay hits).

2.11 IP block/allowlists

mtg: ipblocklist/ package (firehol.go, noop.go, with testdata), supports FireHOL list ingestion, configured at example.config.toml:310-335 ([defense.blocklist]) and :342-356 ([defense.allowlist]).

telemt: relies on the masking subsystem plus per-IP backoff for auth probes (AUTH_PROBE_* constants in src/proxy/handshake.rs:46-70). No first-class file/list-driven blocklist; you compose with iptables or fail2ban.

2.12 Docker / release cadence

Project Docker image Releases in last 90 days (since 2026-01-25)
mtg nineseconds/mtg 15 tagged
telemt telemt/telemt 30 tagged
sing-box sagernet/sing-box active (alpha + stable channels)

(Counts pulled from gh api repos/$repo/releases on 2026-04-25.)


3. Architectural notes

3.1 Where mtg shines

  • Smallest attack surface. FakeTLS only, single secret, no embedded HTTP API. If you want a single binary doing one thing well, mtg is it.
  • MIT license (LICENSE:1) with a long history. Auditable, redistributable, unambiguous.
  • Stable doppelganger for outgoing traffic to the fronting host: pre-opens connections to amortise the TCP handshake under bursty probing. Code under mtglib/internal/doppel/ (ganger.go, scout.go, scout_conn.go, scout_conn_collected.go).
  • Mature Prometheus + statsd integrations (stats/prometheus.go, stats/statsd.go).
  • Anti-replay using a stable Bloom filter — bounded memory, predictable false-positive rate (antireplay/stable_bloom_filter.go:16).

3.2 Where mtg is weaker

  • No multi-user in upstream. Run one process per secret (mtglib/proxy.go:41), or use the mtg-multi fork that upstream itself recommends for this use case (README.md:90-97).
  • Static FakeTLS shape. Record-length noise is randomised (mtglib/proxy.go:210-211) but the cert is fixed and the lengths do not follow the real upstream.
  • No hot reload. Restart on every config change.

3.3 Where telemt shines

  • Multi-user with per-user ad_tag, hot-reload of users (src/config/hot_reload.rs:10, :121).
  • Aggressive anti-fingerprinting: log-normal timing normalisation (src/proxy/masking.rs:259-313), shape-padding (src/proxy/masking.rs:148-211), HTTP/2 preface detection / HTTP-probe classification (src/proxy/masking.rs:101-124, :332-354), per-IP auth-probe backoff (src/proxy/handshake.rs:46-70).
  • Real TLS emulation via src/tls_front/ — closer impostor of the declared domain (gated at src/proxy/handshake.rs:1441).
  • Self-target loop guard that detects when the masking fallback resolves back to the proxy's own listener (src/proxy/masking.rs:571-589, is_mask_target_local_listener_async) — defence against config foot-guns.
  • Test culture. src/proxy/tests/ contains 100+ dedicated adversarial / red-team / fuzz test files (count via gh api repos/telemt/telemt/contents/src/proxy/tests).

3.4 Where telemt is weaker

  • License is non-OSI. Custom "TELEMT LICENSE 3.3" (LICENSE:1). Permissive in spirit but not OSI-approved; requires legal review for corporate deployment.
  • Anonymous maintainership. GitHub org telemt, pseudonymous. Code is open and auditable but there is no named accountable party. For some threat models this is a feature, for others a deal-breaker.
  • Fast-moving codebase. 30 tagged releases in the last 90 days — expect occasional regressions on main. Stick to tagged releases.
  • More config surface. src/config/types.rs is 2,124 lines. Powerful but easy to misconfigure.

3.5 Where sing-box fits

sing-box is not a Telegram MTProto proxy. Its place in this picture is one of two:

  • Client-side: as the Telegram client's transport, where sing-box dials your mtg/telemt MTProxy as an outbound and routes the rest of the device's traffic elsewhere.
  • Server-side SOCKS5 upstream: mtg/telemt forward to a SOCKS5 endpoint provided by sing-box, which then handles the egress (Hysteria2, TUIC, etc.). This adds a hop that breaks naive flow-correlation by domain.

Neither use case competes with mtg or telemt on the inbound port.


4. Decision tree

"I just want to run a proxy for myself and a few friends."

Use mtg. One binary, one secret, MIT license, sane defaults. Pair it with HAProxy/sslh on port 443 (see Surviving Active Probing) and you are done.

"I run a community of 50+ users, each needs their own credential."

Two viable options:

  • mtg-multi (recommended by mtg upstream itself, README.md:90-97) — same Go codebase as mtg, additive secret table, drop-in compatible with upstream config. Best if you want to stay close to the mtg ecosystem.
  • telemt — multi-user is first-class (src/proxy/handshake.rs:831, config.toml:59-61), hot-reload means you can add/remove users without dropping live connections (src/config/hot_reload.rs:1-23), per-user ad_tags help with revenue-sharing if that is relevant. Best if you also want stronger anti-fingerprinting out of the box.

"I'm behind Cloudflare Spectrum / a CDN that does TCP passthrough."

Either works. mtg is simpler to reason about. If your upstream domain changes shape (cert renewal, server migration), telemt's tls_emulation (src/proxy/handshake.rs:1441) will track it; mtg will need a config refresh.

"My threat model is Iran/China and I need maximum probe survival."

Use telemt, with these turned on (verified config keys exist in src/config/types.rs):

  • tls_emulation = true (types.rs: field in CensorshipConfig)
  • mask = true
  • shape-padding and timing normalisation knobs under [censorship]

But: realise that no userspace proxy can hide your kernel TCP fingerprint. See Surviving Active Probing section "TCP fingerprint of the proxy host".

"I work at $BIGCORP and need an OSI-approved license."

Use mtg (MIT, LICENSE:1). telemt's "TELEMT LICENSE 3.3" (LICENSE:1) is permissive but custom and not OSI-approved.

"I want a single binary that does MTProto + Shadowsocks + Hysteria + WireGuard."

You cannot have all of them in one binary. Run sing-box for the SS / Hysteria / WG side and mtg or telemt for MTProto, on different ports or behind an SNI router.


5. Debunking the "mtg closes connection" claim

Issue #458 contains a claim that mtg "just closes the connection on a failed handshake" while telemt "relays transparently to a real site." The first half is false. Both projects relay. Here is the evidence:

mtg, mtglib/proxy.go:

// line 188-201
func (p *Proxy) doFakeTLSHandshake(ctx *streamContext) bool {
    rewind := newConnRewind(ctx.clientConn)

    clientHello, err := fake.ReadClientHello(
        rewind,
        p.secret.Key[:],
        p.secret.Host,
        p.tolerateTimeSkewness,
    )
    if err != nil {
        p.logger.InfoError("cannot read client hello", err)
        p.doDomainFronting(ctx, rewind)   // <-- NOT a close
        return false
    }
    ...
}

// line 296-329
func (p *Proxy) doDomainFronting(ctx *streamContext, conn *connRewind) {
    p.eventStream.Send(p.ctx, NewEventDomainFronting(ctx.streamID))
    conn.Rewind()
    nativeDialer := p.network.NativeDialer()
    fConn, err := nativeDialer.DialContext(ctx, "tcp", p.DomainFrontingAddress())
    ...
    relay.Relay(
        ctx,
        ctx.logger.Named("domain-fronting"),
        connIdleTimeout{Conn: frontConn, tracker: tracker},
        connIdleTimeout{Conn: conn, tracker: tracker},
    )
}

The bytes the probe sent are rewound, a TCP connection is opened to the secret's host (p.secret.Host) on port 443, and the two are relayed end-to-end with relay.Relay. This happens on:

  • malformed ClientHello (mtglib/proxy.go:198-201)
  • replay-cache hit (mtglib/proxy.go:203-208)

In both cases the probe sees the real site's TLS handshake, cert, and HTTP responses — exactly the same outcome telemt advertises as "masking."

What is genuinely different between mtg and telemt:

Aspect mtg telemt
Where the bytes go TCP to secret.Host:443 TCP/Unix to configurable mask_host:mask_port (types.rs:1695-1698)
Default target the SNI in the secret tls_domain from config (config.toml:53)
Timing normalisation none log-normal sample (masking.rs:259-313)
Byte-volume shape padding none bucketed + above-cap blur (masking.rs:148-211)
Self-target loop detection none is_mask_target_local_listener_async (masking.rs:571-589)
HTTP probe classification telemetry none is_http_probe + detect_client_type (masking.rs:101, :332)

So a fair rephrasing of the issue-thread claim would be: mtg does relay on failed handshake, but its relay is unstyled — same bytes, same timing as the real backend, no extra padding or jitter. telemt adds active anti-fingerprinting on top of the relay. Whether you need that depends on your adversary.


6. Migration notes

mtg → telemt

An mtg secret is ee + 32 hex (key) + hex (SNI bytes). In telemt, the user entry is just the 32 hex of the key, and the SNI moves to [censorship] tls_domain:

# telemt config.toml
[censorship]
tls_domain = "your.fronting.domain"   # the SNI from the mtg secret

[access.users]
yourname = "00112233445566778899aabbccddeeff"   # the 32-hex key

Verified field mapping (telemt fields all exist in src/config/types.rs at the SHA above):

mtg config key telemt config key
[domain-fronting] port (example.config.toml:124) [censorship] mask_port (types.rs:1698)
[domain-fronting] ip (example.config.toml:120) [censorship] mask_host (types.rs:1695) and/or [network] dns_overrides (types.rs:353)
[stats.prometheus] bind-to (example.config.toml:376) [server] metrics_listen (types.rs:1458)
[defense.anti-replay] (example.config.toml:290-300) ReplayChecker is on by default (stats/mod.rs:2582)

Hot reload is a one-way upgrade; you'll appreciate it on day two.

telemt → mtg

You will lose: multi-user (mtg is single-secret per mtglib/proxy.go:41), per-user ad_tags, hot reload, TLS emulation, masking shape hardening. You will gain: smaller binary, MIT license, fewer config knobs.

For each [access.users] entry you need a separate mtg process and a separate port (or an SNI router that splits by user-specific SNI, which is awkward because all your users share one tls_domain). In practice: pick your power user, migrate that one to mtg, accept that you need a second tool for the rest.

mtg ↔ sing-box

Not applicable — they don't overlap. If you currently use sing-box as a Telegram client transport pointed at someone else's MTProxy, and you want to run your own server, you still need mtg or telemt on the server; sing-box keeps its current role on the client.


7. Reproducing this comparison

# mtg
git clone https://github.com/9seconds/mtg
cd mtg && git checkout e5ce720a2d7c3f552dbde95aefb5fb6752e30ab0
$EDITOR mtglib/proxy.go +188

# telemt
git clone https://github.com/telemt/telemt
cd telemt && git checkout 8874396ba5c04a293eaa774893afba0f31ab37c5
$EDITOR src/proxy/masking.rs +625
$EDITOR src/proxy/handshake.rs +1139

# sing-box
git clone https://github.com/SagerNet/sing-box
cd sing-box && git checkout b2d5fb62290b9a648a04693a2d32ebf253d684ae
grep -ri mtproto .   # should print nothing
ls protocol/         # 24 entries, none MTProto

If anything in this page no longer matches HEAD on a project's default branch when you read it, file a wiki issue. The pinned SHAs above are the ground truth for every claim.

Clone this wiki locally