-
Notifications
You must be signed in to change notification settings - Fork 357
mtg FAQ
A collection of recurring questions, errors, and confusions taken straight from the issue tracker. Each entry links to the original thread so you can read the full context.
If your problem is not here, also see:
- Surviving Active Probing — why proxies get DPI-blocked and how to harden deployment.
-
Monitoring and Diagnostics — Prometheus metrics,
mtg doctor, debug logs. - SNI Router Setup — sharing port 443 with a real web server.
The hostname in the secret is the fronting domain: the SNI mtg announces in its FakeTLS handshake, and the host it connects to when domain fronting kicks in.
mtg generate-secret your.domain # base64 form
mtg generate-secret --hex your.domain # hex form (starts with ee...)The two forms encode the same secret; use whichever your client wants. Pick a domain that resolves to your proxy server's IP — see Q2.
Symptom. Validate SNI-DNS match ❌, even though the proxy itself
works.
Cause. You generated the secret with someone else's domain
(google.com, storage.googleapis.com, etc.). mtg's FakeTLS
ServerHello says "I am google.com" but the IP belongs to your VPS — a
trivial mismatch any DPI box can spot.
Fix. Use a domain you own (or a free one like *.duckdns.org,
*.nip.io) and point its A/AAAA records at your VPS, then regenerate
the secret with that domain. mtg doctor will then pass and mtg run
will no longer warn at startup (#461).
References: #444, Surviving Active Probing.
No. v2 only supports FakeTLS (ee…) secrets. Plain (dd…) and
secured-only secrets from v1 are rejected:
mtg: error: incorrect secret: incorrect first byte of secret: 0xd9
Generate a new FakeTLS secret with mtg generate-secret. See #286, #285.
Partially. The bot expects a 32-hex-character secret — that's the legacy format mtg no longer supports. You can still register the proxy host and port with the bot for ad-tag purposes; the bot complaining about the secret length is normal. See #295, #36.
Symptom. Errors only from one platform (often Android), other clients connect fine.
Cause. A buggy Telegram client release that ships a broken or non-standard ClientHello — the canonical example is Telegram Desktop 6.7.2 (tdesktop#30513), and there were similar regressions on Android in the past (#132, #41).
Fix. Update the client. This is not an mtg bug. References: #325, #387.
Symptom. Mac/iOS connect, Windows/Android don't.
Cause. TLS-fragmented ClientHello. DPI-bypass tools (ByeDPI and similar) and some platforms split the handshake across multiple TLS records; older mtg versions assumed the whole ClientHello arrives in a single record and failed HMAC verification.
Fix. Upgrade to the latest 2.x. Issue #431 added
record reassembly (reassembleTLSHandshake), refactored in
#433. Original report:
#323.
Symptom. Client connects, gets disconnected after a short while; log shows clock-drift complaints.
Cause. mtg's FakeTLS embeds a timestamp and rejects messages that
fall outside tolerate-time-skewness (default 5s, see
example.config.toml). Either the server clock is wrong (NTP not
running) or the client is misbehaving.
Fix.
- Verify the server time:
timedatectl,chronyc tracking. - Bump the tolerance if you have legitimately drifty clients:
tolerate-time-skewness = "30s".
This is expected. curl sends a real TLS ClientHello, mtg recognises
it as not-MTProto, and forwards the connection to the fronting domain
(domain fronting fallback in mtglib/proxy.go). If the fronting
domain's TLS terminates with a different cert, curl will complain. Use
-k for testing. See #280.
Symptom. Repeated cannot dial to the fronting domain warnings in
logs.
Cause. mtg uses DNS-over-HTTPS (default Cloudflare) and ignores
the system resolver. If https://1.1.1.1 is blocked from your VPS
(common in some jurisdictions), every fronting lookup fails.
Fix. Configure a reachable resolver in [network]:
[network]
dns = "https://8.8.8.8" # or tls://1.1.1.1, or udp://9.9.9.9, or "" for systemSymptom. error: incorrect bind-to parameter: empty host: :443 or
too many colons in address.
Cause / fix. mtg requires an explicit host part. For dual stack:
bind-to = "[::]:443" # listens on both IPv4 and IPv6 on LinuxBare :443 is rejected. References: #308, #298, #460.
Symptom. Logs full of dial tcp6 [2a0a:f280:…]:443: cannot assign requested address, thousands of stuck connections, CPU pegged.
Cause. Partial IPv6: the host has an IPv6 address but Telegram's v6 prefixes are unreachable. mtg keeps retrying.
Fix. Force IPv4 in the config (the literal value accepted by mtg's
TypePreferIP, see internal/config/type_prefer_ip.go):
prefer-ip = "only-ipv4"If the symptoms persist, upgrade — #464 fixed the connection-leak loop. Related: #475.
Symptom. Most chats work, some media fails to load; warnings mention DC 203.
Cause. DC 203 is Telegram's CDN cluster. mtg's
mtglib/internal/dc/public_config_updater.go only collects addresses
for DC 203 (case 203: // CDN DC); the test fixtures pin
91.105.192.100:443 (v4) and [2a0a:f280:0203:000a:5000::100]:443
(v6). If those endpoints are unreachable from your ASN, every CDN
lookup fails.
Fix.
- Make sure outbound 443/TCP to the CDN endpoints is open.
- Set
allow-fallback-on-unknown-dc = trueso mtg routes the request via the default DC instead of failing —mtglib/proxy.go(p.allowFallbackOnUnknownDCbranch around line 240) substitutesdc.DefaultDCwhen no addresses are known. See #216, #210.
References: #369, #310, #208, #407, #470.
Symptom. mtg doctor or mtg run fails to start because it cannot
auto-detect the server IP.
Fix. Set the IP yourself in the config (these keys are documented
in example.config.toml):
public-ipv4 = "1.2.3.4"
public-ipv6 = "2001:db8::1"This bypasses ifconfig.co entirely. References: #405, #418.
Symptom. Connecting from the same LAN as the proxy (phone on home
Wi-Fi → home server) silently fails — the client gets the fronting
website instead of Telegram. Logs show
{"level":"info","ip":"10.x.x.x","logger":"proxy","message":"ip was blacklisted"}.
Cause. The default [defense.blocklist] URL
(firehol_level1.netset) is a bogon list; it includes RFC1918 ranges
(10/8, 172.16/12, 192.168/16). Your LAN IP matches and gets blocked.
Fix. Either disable the blocklist:
[defense.blocklist]
enabled = false…or pick a narrower list (e.g. firehol_abusers_1d), or use hairpin
NAT so LAN clients hit the public IP. See #466, #467.
[defense.anti-replay] keeps a stable bloom filter
(antireplay/stable_bloom_filter.go, default 1 MiB,
DefaultStableBloomFilterErrorRate = 0.001) of ClientHello
SessionIDs. When a SessionID is seen twice, mtg logs
replay attack has been detected!, emits an
EventReplayAttack, and drops the client into the domain-fronting
fallback (mtglib/proxy.go, doFakeTLSHandshake). This blocks
trivial replays of a captured handshake.
If you need to disable it (e.g. for load testing):
[defense.anti-replay]
enabled = falseDon't disable in production — it's a cheap defense against the most common probing technique. References: #129, #281, #76.
See the dedicated guide Surviving Active Probing. Short version: make sure your secret's hostname resolves to your VPS's IP, run a real TLS service on the same port via SNI routing, and read #458, #440, #111.
Cause. The container path must be absolute.
Fix. Use /config.toml on the right side of -v:
docker run -d -v "$PWD/config.toml:/config.toml" \
-p 443:3128 --name mtg-proxy --restart=unless-stopped \
nineseconds/mtg:2The default container entrypoint is mtg run, which needs a config; if
none is mounted it dies. For one-off commands, override:
docker run --rm nineseconds/mtg:2 generate-secret --hex your.domain
docker run --rm -v "$PWD/config.toml:/config.toml" \
nineseconds/mtg:2 doctor /config.toml
docker exec mtg-proxy /mtg access /config.tomlNote the /mtg (the binary) inside docker exec, not bare mtg. See
#246, #296, #313.
Symptom. A few hundred concurrent users and accept: too many open files appears; the proxy stops accepting connections.
Cause. DynamicUser=true runs the service with the default
per-service LimitNOFILE, which is too low for a busy proxy.
Fix. The README's reference unit
(/etc/systemd/system/mtg.service) sets LimitNOFILE=65536:
[Service]
ExecStart=/usr/local/bin/mtg run /etc/mtg.toml
Restart=always
RestartSec=3
DynamicUser=true
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_BIND_SERVICEsystemctl daemon-reload && systemctl restart mtg. Raise it further
if you see the error again at higher concurrency. See
#388,
#378,
#258.
Not directly — only one process can bind(443). Put an SNI router in
front:
- nginx
streamwithssl_preread, - HAProxy with
req_ssl_sniACL, - sslh in transparent mode,
…that routes your-mtg-domain to mtg and everything else to your web
server. A docker-compose example with HAProxy + Caddy lives in
contrib/sni-router/ (#462).
See SNI Router Setup for a walkthrough. Background:
#312,
#162,
#152.
Symptom. proxies = ["socks5://…"] set under [network], but
traffic still leaves directly and mtg doctor shows the proxy as
unused.
Cause. Pre-fix, the v1 network package wrapped only some call
paths; network.Dial and network.MakeHTTPClient skipped the SOCKS5
proxy and dialled directly.
Fix. Upgrade to a release that contains
#367 (merged 2026-03-16),
which routes both Dial and MakeHTTPClient through the configured
proxy — see network/v2/. Config shape (note proxies belongs under
[network]):
[network]
dns = "https://1.1.1.1"
proxies = ["socks5://user:pass@host:1080"]Verify in debug logs: "proxies":["socks5://…"]. See #468,
#350,
#315,
#392.
Symptom. Text and images fine, video and large file uploads stall.
Cause history.
- 2.1.3–2.1.4: a buffer-management regression broke uploads (#234, #233).
- 2.1.11: another media regression (#423, #343).
- DC 203 unreachable (see Q12).
Fix. Upgrade to the latest 2.x — the regressions above were
closed by later releases. If still slow, check prefer-ip, idle
timeouts, and DC 203 reachability. References: #270, #283,
#265, #406.
mtg exposes Prometheus metrics by default:
[stats.prometheus]
enabled = true
bind-to = "127.0.0.1:3129"
http-path = "/"
metric-prefix = "mtg"curl http://127.0.0.1:3129/ shows counters. There is no built-in
text dashboard (#473).
See Monitoring and Diagnostics for full
metric names and a Grafana setup. For statsd see
#27,
#103.
mtg access /path/to/config.toml
# inside docker:
docker exec mtg-proxy /mtg access /config.tomlThis prints both tg:// and t.me/proxy?… URLs and a QR. References:
#159, #372, #190.
Not supported in v2 upstream. v1 had multi-secrets mode but it was
removed when the codebase was rewritten (#180,
#67, #376,
#81; rationale in #376).
Two options:
- mtg-multi — fork that upstream's own README points to for this case. Same Go codebase, additive secret table, drop-in compatible config.
- Run multiple mtg instances behind the SNI router (Q20) if you'd rather stay on stock upstream.
Cause. Out-of-date Go toolchain or stale module cache. mtg dropped
make in favour of mise, and the README's
Build from sources
section is the canonical recipe.
Fix.
# install mise (the project's tool manager)
curl https://mise.run | sh
# inside the repo
mise install # picks up Go from .mise.toml + mise.lock
mise tasks run build # = `go build`, see [tasks.build] in .mise.tomlmtg switched from make to mise (#329).
References: #119, #114, #212.
Symptom. Crash like
listen tcp 127.0.0.1:3129: bind: cannot assign requested address.
Cause. Either the stats port (default 127.0.0.1:3129) is already
in use, or bind-to references an IP that doesn't exist on this host
(e.g. you copied the config from another server).
Fix. Use 0.0.0.0:443 or a real interface IP, and verify nothing
else holds the stats port (ss -lntp | grep 3129). References: #167,
#106, #316.
Stop the proxy, edit secret = "ee…" in the config, restart. Existing
clients will need a new link generated by mtg access. Note that the
hostname part of a secret is fixed by the domain — to "rotate" you
mostly rotate the random bytes, not the host. See #300, #313.
v2. v1 was never supported in this implementation. See #168.
When opening a new issue, please include:
- mtg version (
mtg --versionor container tag); - redacted
config.toml(drop the secret); - output of
mtg doctor /path/to/config.toml; - a sample of debug logs (
debug = true) showing the failure; - whether the problem is client-specific (which client, which version, which OS).
That makes triage about ten times faster.