Skip to content

Warn about SNI/IP mismatch at proxy startup#461

Open
dolonet wants to merge 2 commits into9seconds:masterfrom
dolonet:feature/sni-mismatch-warning
Open

Warn about SNI/IP mismatch at proxy startup#461
dolonet wants to merge 2 commits into9seconds:masterfrom
dolonet:feature/sni-mismatch-warning

Conversation

@dolonet
Copy link
Copy Markdown
Contributor

@dolonet dolonet commented Apr 10, 2026

Summary

The SNI-DNS validation that mtg doctor already performs is now also
run once at mtg run startup. If the secret hostname does not resolve
to the server's public IP, a warning is logged so that operators notice
the misconfiguration before DPI silently blocks the proxy.

The check is best-effort and never prevents the proxy from starting:

  • If the public IP cannot be detected, a short warning is emitted.
  • If DNS resolution fails, a warning with the error is emitted.
  • On a match, nothing is logged (no noise in normal operation).

Motivation

Most operators only discover the SNI/IP mismatch after their proxy has
already been blocked. mtg doctor catches this, but many people skip
it — especially in docker/systemd setups where you deploy and forget.
A startup warning surfaces the problem at exactly the right moment.

Discussed in #458 (comment by @9seconds: "Наверное да, хорошая идея").

Changes

  • internal/cli/run_proxy.go: new warnSNIMismatch() function, called
    after the network is initialised. Reuses getIP() from utils.go
    (same logic as doctor.go:checkSecretHost).

Test plan

  • go build ./... — clean
  • go vet ./internal/cli/... — clean
  • Manually verified: with a mismatched hostname the warning appears in
    the log; with a matching hostname no warning is emitted.

Refs: #444, #458

The SNI-DNS validation that exists in 'mtg doctor' is now also run at
proxy startup.  If the secret hostname does not resolve to the server's
public IP, a warning is logged so that operators notice the
misconfiguration before DPI silently blocks the proxy.

The check is best-effort: if the public IP cannot be detected or the
hostname cannot be resolved, a brief warning is emitted and the proxy
starts normally.

Refs: 9seconds#444, 9seconds#458
}

for _, addr := range addresses {
if (ourIP4 != nil && addr.IP.String() == ourIP4.String()) ||
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a feeling that both addresses must match. If we have 1 matching IPv6 but IPv4 mismatch, it could lead to major problems, given that most of e2e connectivity is still made with IPv4

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — fixed in 491a355.

The check now requires each detected IP family to be present in the DNS response: a matching AAAA no longer masks a mismatched A, and vice versa. If only one family is detected (e.g. IPv6 not configured), only that family is required. The warning also reports per-family match status (ipv4_match=false / ipv6_match=true) so operators can tell which record is wrong.

Side note: doctor.go:checkSecretHost has the same OR-logic and would have the same blind spot. Happy to align it in this PR for consistency, or split into a follow-up — whichever you prefer.

Previously the check returned OK if any resolved address matched
either the public IPv4 or IPv6. A matching AAAA could mask a
mismatched A record (and vice versa), which is a problem because
most client connectivity is still IPv4: a partial match would
silently pass the warning while DPI still blocks the proxy.

Now each detected IP family must appear in the DNS response; the
warning also reports per-family match status so operators can tell
which record is wrong.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants