sni-router: break domain-fronting loop#478
Conversation
| > (Caddy's pinned address). Caddy may refuse the mixed-family header | ||
| > and log the docker-network address instead of the real client IP for | ||
| > that connection. Telegram traffic is unaffected. |
| `docker-compose.yml` (mtg's `domain-fronting.ip` only accepts a literal | ||
| IP, not a hostname, hence the static `sni` network). `proxy-protocol = |
There was a problem hiding this comment.
Is it fundamental mtg's restriction? Maybe try to fix it there?
There was a problem hiding this comment.
Not fundamental — TypeIP just calls net.ParseIP, but the rest of the dial path is hostname-capable. Opened #480 to add a sibling [domain-fronting].host that accepts hostname or IP. Once it lands this PR shrinks to a host = "web" line and the static subnet/pin go away.
There was a problem hiding this comment.
#480 I suggest to have this one first, so the whole PR could be simplified
There was a problem hiding this comment.
So we can shrink it now, right?
|
|
||
| networks: | ||
| sni: | ||
| driver: bridge |
There was a problem hiding this comment.
Is the bridge driver necessary?
|
Just to clarify - is this problem happens only if both the hostname and the domain are fully equal, or also if they just partially intersect - so even if the hostname is a.b.com and the domain is b.com? |
|
Good question — it's not about the names overlapping, it's about DNS. HAProxy matches the SNI exactly ( So in your Pushed a small README tweak (bcfacec) leading with "the trigger is DNS, not name equality" so the doc doesn't imply the matching-name case is the only one. |
|
Could we add a loop detection to the mtg runtime and/or it's config check doctor mode? |
|
Good idea, but I'd rather not expand 478 (it's config + docs only) — happy to track it as a separate issue/PR. Sketch of a feasible runtime check: when Caveat worth being upfront about: the check only sees the direct case. An SNI-router on a separate IP that ultimately routes back to mtg would slip through, since mtg's outbound dial lands on a "foreign" IP. A precise detector would need an out-of-band marker on the fronting connection, which MTProto doesn't expose cleanly. So 80% coverage from a cheap check, the rest stays a documentation problem. Shall I open a follow-up issue with this scope? |
|
Sure, thanks. |
|
Wanna hear @9seconds's opinion on that before diving in :) |
|
Sounds good — happy to wait on #480. Once it lands, this PR collapses to:
I'll rebase and force-push the simplified version after #480 merges. Separately, on the runtime loop-detection idea raised above (#478 (comment)) — would you like me to open a follow-up issue with the "resolve secret hostname at startup, warn if it matches a local interface, non-fatal" sketch? Easy to track, easy to scope, but I didn't want to open it without your nod. |
bcfacec to
bf501a8
Compare
When the secret's domain resolves back to this server (the SNI-router default), mtg's fallback fronting dial lands on HAProxy, the SNI matches the secret, HAProxy routes the connection back to mtg -> loop. Set [domain-fronting].host = "web" in mtg-config.toml so mtg dials Caddy directly via compose-network DNS, bypassing HAProxy. Requires mtg >= 2.4 (9seconds#480 added hostname acceptance for the fronting target). README gains a "Fronting loop" section explaining the cause.
bf501a8 to
0fdf6cb
Compare
|
Pushed the simplified version (force-pushed):
Net diff is now +45 / 2 files. |
mtg now also sends PROXY v2 on the fronting dial (introduced in the previous commit via [domain-fronting].proxy-protocol = true), so the "Real client IPs" section's sync list must include that fourth piece. Without it, an operator who disables Caddy's PROXY listener wrapper without also flipping [domain-fronting].proxy-protocol will leave mtg sending an unparsed PROXY v2 prefix to Caddy on every fronted probe.
|
Quick self-review pass before this lands on your queue:
|
|
One correction on a side claim in the previous self-review note (#478 (comment)): I wrote that PROXY v2 "stays same-family for the common path." That holds only while the client's family matches the resolved family for Compose's default network is single-stack IPv4, so an IPv6 client fronted to Caddy yields a mixed-family PROXY v2 header (TCPv6 source, IPv4 destination encoded as IPv4-mapped). PROXY v2 spec marks that as invalid, but Caddy's If pristine IPv6 logging matters for someone's deployment, they can |
Summary
Follow-up to #462. When the secret's domain resolves back to this server (the SNI-router default), mtg's fallback fronting dial lands on HAProxy, HAProxy sees the SNI matching the secret and routes the connection back to mtg → loop.
Reported by @gaudima in #462 (comment).
Fix
Pin
[domain-fronting].host = "web"inmtg-config.tomlso mtg dials the Caddy container directly via compose-network DNS, bypassing HAProxy. Requires mtg ≥ 2.4 (#480 added hostname acceptance for the fronting target — already merged).mtg-config.toml:README gains a "Fronting loop" section explaining the cause. The existing "Real client IPs" sync list grows to a fourth piece (
[domain-fronting].proxy-protocol), since mtg now also writes a PROXY v2 header on the fronting dial.Net diff: +51 −3 / 2 files (
README.md,mtg-config.toml). Nodocker-compose.ymlchanges — compose-network DNS handles the routing without a static subnet.Test plan
curl https://DOMAIN/returns Caddy's contentcurl --resolve DOMAIN:443:HOST_IP -k -I https://DOMAIN/(probe simulation: SNI matches the secret, no MTProto handshake) — connection terminates against Caddy without looping; Caddy's access log shows the real client IPFollow-up
Runtime/doctor self-loop detection (sketched up-thread) tracked separately so this PR stays config + docs only.