-
Notifications
You must be signed in to change notification settings - Fork 358
Alternatives
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, andprotocol/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/MTProxyis the third realistic option but it has been effectively unmaintained for years and is mentioned only in passing.
- Repo: https://github.com/9seconds/mtg
- Language: Go
- License: MIT (
LICENSE:1) - Original author: Sergey Arkhipov (
9seconds). - Last commit on
masterat 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/securedmodes are deliberately rejected (seeexample.config.toml:18-21).
- 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
ddprefix, FakeTLS witheeprefix —README.md:35-38), with multi-user support, per-user ad-tags, hot reload, and a heavy investment in masking-shape hardening.
- Repo: https://github.com/SagerNet/sing-box
- Language: Go
- License: GPLv3-or-later (
LICENSE:1-6) - Maintainer:
nekohasekaiand 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.
- Repo: https://github.com/TelegramMessenger/MTProxy
- C, no permissive license header on most files.
- Last meaningful commit: 2020. Reference implementation; do not deploy in 2026 unless you are patching it yourself.
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.
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 = truemtg'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 emulationIn 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.
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:
doDomainFrontingatmtglib/proxy.go:296-329, called frommtglib/proxy.go:199(bad ClientHello) andmtglib/proxy.go:206(replay hit). -
telemt: same idea, more knobs.
handle_bad_clientatsrc/proxy/masking.rs:625, dispatched from the handshake state machine viaHandshakeResult::BadClientatsrc/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.
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.
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.
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.
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]]).
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).
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
notifycrate. 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.
mtg ships both Prometheus and statsd integrations:
stats/prometheus.gostats/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).
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.
| 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.)
- 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).
-
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.
-
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 atsrc/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 viagh api repos/telemt/telemt/contents/src/proxy/tests).
-
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.rsis 2,124 lines. Powerful but easy to misconfigure.
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.
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.
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.
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.
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".
Use mtg (MIT, LICENSE:1). telemt's "TELEMT LICENSE 3.3"
(LICENSE:1) is permissive but custom and not OSI-approved.
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.
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.
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 keyVerified 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.
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.
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.
# 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 MTProtoIf 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.