From 1f8f063ec3f0fbef7ef627f24e0d25984e5e0991 Mon Sep 17 00:00:00 2001 From: dolonet Date: Fri, 10 Apr 2026 10:11:01 +0000 Subject: [PATCH 1/2] Warn about SNI/IP mismatch at mtg run startup 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: #444, #458 --- internal/cli/run_proxy.go | 65 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go index 5f0b3dee6..870914ee9 100644 --- a/internal/cli/run_proxy.go +++ b/internal/cli/run_proxy.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "os" + "strings" "github.com/9seconds/mtg/v2/antireplay" "github.com/9seconds/mtg/v2/events" @@ -207,7 +208,67 @@ func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStr return events.NewNoopStream(), nil } -func runProxy(conf *config.Config, version string) error { //nolint: funlen +func warnSNIMismatch(conf *config.Config, ntw mtglib.Network, log mtglib.Logger) { + host := conf.Secret.Host + if host == "" { + return + } + + addresses, err := net.DefaultResolver.LookupIPAddr(context.Background(), host) + if err != nil { + log.BindStr("hostname", host). + WarningError("SNI-DNS check: cannot resolve secret hostname", err) + return + } + + ourIP4 := conf.PublicIPv4.Get(nil) + if ourIP4 == nil { + ourIP4 = getIP(ntw, "tcp4") + } + + ourIP6 := conf.PublicIPv6.Get(nil) + if ourIP6 == nil { + ourIP6 = getIP(ntw, "tcp6") + } + + if ourIP4 == nil && ourIP6 == nil { + log.Warning("SNI-DNS check: cannot detect public IP address; set public-ipv4/public-ipv6 in config or run 'mtg doctor'") + return + } + + for _, addr := range addresses { + if (ourIP4 != nil && addr.IP.String() == ourIP4.String()) || + (ourIP6 != nil && addr.IP.String() == ourIP6.String()) { + return + } + } + + resolved := make([]string, 0, len(addresses)) + for _, addr := range addresses { + resolved = append(resolved, addr.IP.String()) + } + + our := "" + if ourIP4 != nil { + our = ourIP4.String() + } + + if ourIP6 != nil { + if our != "" { + our += "/" + } + + our += ourIP6.String() + } + + log.BindStr("hostname", host). + BindStr("resolved", strings.Join(resolved, ", ")). + BindStr("public_ip", our). + Warning("SNI-DNS mismatch: secret hostname does not resolve to this server's public IP. " + + "DPI may detect and block the proxy. See 'mtg doctor' for details") +} + +func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyclop logger := makeLogger(conf) logger.BindJSON("configuration", conf.String()).Debug("configuration") @@ -222,6 +283,8 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen return fmt.Errorf("cannot build network: %w", err) } + warnSNIMismatch(conf, ntw, logger) + blocklist, err := makeIPBlocklist( conf.Defense.Blocklist, logger.Named("blocklist"), From 491a355a612c23a8af749c9927452066d7fd5dcc Mon Sep 17 00:00:00 2001 From: dolonet Date: Mon, 13 Apr 2026 08:02:39 +0000 Subject: [PATCH 2/2] Require all detected IP families to match in SNI-DNS check 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. --- internal/cli/run_proxy.go | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go index 870914ee9..5d9e63e98 100644 --- a/internal/cli/run_proxy.go +++ b/internal/cli/run_proxy.go @@ -236,13 +236,23 @@ func warnSNIMismatch(conf *config.Config, ntw mtglib.Network, log mtglib.Logger) return } + v4Match := ourIP4 == nil + v6Match := ourIP6 == nil + for _, addr := range addresses { - if (ourIP4 != nil && addr.IP.String() == ourIP4.String()) || - (ourIP6 != nil && addr.IP.String() == ourIP6.String()) { - return + if ourIP4 != nil && addr.IP.String() == ourIP4.String() { + v4Match = true + } + + if ourIP6 != nil && addr.IP.String() == ourIP6.String() { + v6Match = true } } + if v4Match && v6Match { + return + } + resolved := make([]string, 0, len(addresses)) for _, addr := range addresses { resolved = append(resolved, addr.IP.String()) @@ -261,11 +271,20 @@ func warnSNIMismatch(conf *config.Config, ntw mtglib.Network, log mtglib.Logger) our += ourIP6.String() } - log.BindStr("hostname", host). + entry := log.BindStr("hostname", host). BindStr("resolved", strings.Join(resolved, ", ")). - BindStr("public_ip", our). - Warning("SNI-DNS mismatch: secret hostname does not resolve to this server's public IP. " + - "DPI may detect and block the proxy. See 'mtg doctor' for details") + BindStr("public_ip", our) + + if ourIP4 != nil { + entry = entry.BindStr("ipv4_match", fmt.Sprintf("%t", v4Match)) + } + + if ourIP6 != nil { + entry = entry.BindStr("ipv6_match", fmt.Sprintf("%t", v6Match)) + } + + entry.Warning("SNI-DNS mismatch: secret hostname does not resolve to this server's public IP. " + + "DPI may detect and block the proxy. See 'mtg doctor' for details") } func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyclop