Kaynağa Gözat

Consolidate SNI-DNS check and tighten doctor

The runtime warning (warnSNIMismatch) and the diagnostic command
(doctor checkSecretHost) previously implemented the same SNI-DNS
check with different logic: the runtime path was tightened in #461
to require every detected IP family to match, but the doctor still
accepted any single match. The two now agree.

Changes:

- Extract the shared check into internal/cli/sni_check.go, returning
  the resolved addresses and a per-family match status.
- Rewrite warnSNIMismatch and checkSecretHost on top of the helper.
- Doctor output now reports the mismatched IP family (IPv4, IPv6, or
  both) and lists the server's public IP alongside the DNS result.
- getIP falls back through a short list of public-IP endpoints
  (ifconfig.co, icanhazip.com, ifconfig.me) instead of relying on
  a single third-party service.
pull/474/head
dolonet 2 hafta önce
ebeveyn
işleme
81e5a5ac82
4 değiştirilmiş dosya ile 191 ekleme ve 80 silme
  1. 55
    31
      internal/cli/doctor.go
  2. 20
    43
      internal/cli/run_proxy.go
  3. 91
    0
      internal/cli/sni_check.go
  4. 25
    6
      internal/cli/utils.go

+ 55
- 31
internal/cli/doctor.go Dosyayı Görüntüle

@@ -52,10 +52,13 @@ var (
52 52
 	)
53 53
 
54 54
 	tplODNSSNIMatch = template.Must(
55
-		template.New("").Parse("  ✅ IP address {{ .ip }} matches secret hostname {{ .hostname }}\n"),
55
+		template.New("").Parse("  ✅ Secret hostname {{ .hostname }} matches our public IP ({{ .our }}); resolved: {{ .resolved }}\n"),
56 56
 	)
57 57
 	tplEDNSSNIMatch = template.Must(
58
-		template.New("").Parse("  ❌ Hostname {{ .hostname }} {{ if .resolved }}is resolved to {{ .resolved }} addresses, not {{ if .ip4 }}{{ .ip4 }}{{ else }}{{ .ip6 }}{{ end }}{{ else }}cannot be resolved to any host{{ end }}\n"),
58
+		template.New("").Parse("  ❌ Secret hostname {{ .hostname }} resolves to {{ .resolved }} but our public IP is {{ .our }}{{ if .families }} (mismatched families: {{ .families }}){{ end }}\n"),
59
+	)
60
+	tplEDNSSNINoResolve = template.Must(
61
+		template.New("").Parse("  ❌ Secret hostname {{ .hostname }} cannot be resolved to any address\n"),
59 62
 	)
60 63
 
61 64
 	tplOFrontingDomain = template.Must(
@@ -329,26 +332,17 @@ func (d *Doctor) checkFrontingDomain(ntw mtglib.Network) bool {
329 332
 }
330 333
 
331 334
 func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) bool {
332
-	addresses, err := resolver.LookupIPAddr(context.Background(), d.conf.Secret.Host)
333
-	if err != nil {
335
+	res := runSNICheck(context.Background(), resolver, d.conf, ntw)
336
+
337
+	if res.ResolveErr != nil {
334 338
 		tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
335
-			"description": fmt.Sprintf("cannot resolve DNS name of %s", d.conf.Secret.Host),
336
-			"error":       err,
339
+			"description": fmt.Sprintf("cannot resolve DNS name of %s", res.Host),
340
+			"error":       res.ResolveErr,
337 341
 		})
338 342
 		return false
339 343
 	}
340 344
 
341
-	ourIP4 := d.conf.PublicIPv4.Get(nil)
342
-	if ourIP4 == nil {
343
-		ourIP4 = getIP(ntw, "tcp4")
344
-	}
345
-
346
-	ourIP6 := d.conf.PublicIPv6.Get(nil)
347
-	if ourIP6 == nil {
348
-		ourIP6 = getIP(ntw, "tcp6")
349
-	}
350
-
351
-	if ourIP4 == nil && ourIP6 == nil {
345
+	if !res.Known() {
352 346
 		tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
353 347
 			"description": "cannot detect public IP address",
354 348
 			"error":       errors.New("cannot detect automatically and public-ipv4/public-ipv6 are not set in config"),
@@ -356,25 +350,55 @@ func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) boo
356 350
 		return false
357 351
 	}
358 352
 
359
-	strAddresses := []string{}
360
-	for _, value := range addresses {
361
-		if (ourIP4 != nil && value.IP.String() == ourIP4.String()) ||
362
-			(ourIP6 != nil && value.IP.String() == ourIP6.String()) {
363
-			tplODNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
364
-				"ip":       value.IP,
365
-				"hostname": d.conf.Secret.Host,
366
-			})
367
-			return true
353
+	if len(res.Resolved) == 0 {
354
+		tplEDNSSNINoResolve.Execute(os.Stdout, map[string]any{ //nolint: errcheck
355
+			"hostname": res.Host,
356
+		})
357
+		return false
358
+	}
359
+
360
+	resolved := make([]string, 0, len(res.Resolved))
361
+	for _, ip := range res.Resolved {
362
+		resolved = append(resolved, `"`+ip.String()+`"`)
363
+	}
364
+
365
+	our := ""
366
+	if res.OurIPv4 != nil {
367
+		our = res.OurIPv4.String()
368
+	}
369
+
370
+	if res.OurIPv6 != nil {
371
+		if our != "" {
372
+			our += "/"
368 373
 		}
369 374
 
370
-		strAddresses = append(strAddresses, `"`+value.IP.String()+`"`)
375
+		our += res.OurIPv6.String()
376
+	}
377
+
378
+	if res.OK() {
379
+		tplODNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
380
+			"hostname": res.Host,
381
+			"resolved": strings.Join(resolved, ", "),
382
+			"our":      our,
383
+		})
384
+		return true
385
+	}
386
+
387
+	mismatched := []string{}
388
+
389
+	if res.OurIPv4 != nil && !res.IPv4Match {
390
+		mismatched = append(mismatched, "IPv4")
391
+	}
392
+
393
+	if res.OurIPv6 != nil && !res.IPv6Match {
394
+		mismatched = append(mismatched, "IPv6")
371 395
 	}
372 396
 
373 397
 	tplEDNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
374
-		"hostname": d.conf.Secret.Host,
375
-		"resolved": strings.Join(strAddresses, ", "),
376
-		"ip4":      ourIP4,
377
-		"ip6":      ourIP6,
398
+		"hostname": res.Host,
399
+		"resolved": strings.Join(resolved, ", "),
400
+		"our":      our,
401
+		"families": strings.Join(mismatched, ", "),
378 402
 	})
379 403
 
380 404
 	return false

+ 20
- 43
internal/cli/run_proxy.go Dosyayı Görüntüle

@@ -209,78 +209,55 @@ func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStr
209 209
 }
210 210
 
211 211
 func warnSNIMismatch(conf *config.Config, ntw mtglib.Network, log mtglib.Logger) {
212
-	host := conf.Secret.Host
213
-	if host == "" {
212
+	if conf.Secret.Host == "" {
214 213
 		return
215 214
 	}
216 215
 
217
-	addresses, err := net.DefaultResolver.LookupIPAddr(context.Background(), host)
218
-	if err != nil {
219
-		log.BindStr("hostname", host).
220
-			WarningError("SNI-DNS check: cannot resolve secret hostname", err)
221
-		return
222
-	}
216
+	res := runSNICheck(context.Background(), net.DefaultResolver, conf, ntw)
223 217
 
224
-	ourIP4 := conf.PublicIPv4.Get(nil)
225
-	if ourIP4 == nil {
226
-		ourIP4 = getIP(ntw, "tcp4")
227
-	}
228
-
229
-	ourIP6 := conf.PublicIPv6.Get(nil)
230
-	if ourIP6 == nil {
231
-		ourIP6 = getIP(ntw, "tcp6")
218
+	if res.ResolveErr != nil {
219
+		log.BindStr("hostname", res.Host).
220
+			WarningError("SNI-DNS check: cannot resolve secret hostname", res.ResolveErr)
221
+		return
232 222
 	}
233 223
 
234
-	if ourIP4 == nil && ourIP6 == nil {
224
+	if !res.Known() {
235 225
 		log.Warning("SNI-DNS check: cannot detect public IP address; set public-ipv4/public-ipv6 in config or run 'mtg doctor'")
236 226
 		return
237 227
 	}
238 228
 
239
-	v4Match := ourIP4 == nil
240
-	v6Match := ourIP6 == nil
241
-
242
-	for _, addr := range addresses {
243
-		if ourIP4 != nil && addr.IP.String() == ourIP4.String() {
244
-			v4Match = true
245
-		}
246
-
247
-		if ourIP6 != nil && addr.IP.String() == ourIP6.String() {
248
-			v6Match = true
249
-		}
250
-	}
251
-
252
-	if v4Match && v6Match {
229
+	if res.OK() {
253 230
 		return
254 231
 	}
255 232
 
256
-	resolved := make([]string, 0, len(addresses))
257
-	for _, addr := range addresses {
258
-		resolved = append(resolved, addr.IP.String())
233
+	resolved := make([]string, 0, len(res.Resolved))
234
+	for _, ip := range res.Resolved {
235
+		resolved = append(resolved, ip.String())
259 236
 	}
260 237
 
261 238
 	our := ""
262
-	if ourIP4 != nil {
263
-		our = ourIP4.String()
239
+	if res.OurIPv4 != nil {
240
+		our = res.OurIPv4.String()
264 241
 	}
265 242
 
266
-	if ourIP6 != nil {
243
+	if res.OurIPv6 != nil {
267 244
 		if our != "" {
268 245
 			our += "/"
269 246
 		}
270 247
 
271
-		our += ourIP6.String()
248
+		our += res.OurIPv6.String()
272 249
 	}
273 250
 
274
-	entry := log.BindStr("hostname", host).
251
+	entry := log.BindStr("hostname", res.Host).
275 252
 		BindStr("resolved", strings.Join(resolved, ", ")).
276 253
 		BindStr("public_ip", our)
277 254
 
278
-	if ourIP4 != nil {
279
-		entry = entry.BindStr("ipv4_match", fmt.Sprintf("%t", v4Match))
255
+	if res.OurIPv4 != nil {
256
+		entry = entry.BindStr("ipv4_match", fmt.Sprintf("%t", res.IPv4Match))
280 257
 	}
281 258
 
282
-	if ourIP6 != nil {
283
-		entry = entry.BindStr("ipv6_match", fmt.Sprintf("%t", v6Match))
259
+	if res.OurIPv6 != nil {
260
+		entry = entry.BindStr("ipv6_match", fmt.Sprintf("%t", res.IPv6Match))
284 261
 	}
285 262
 
286 263
 	entry.Warning("SNI-DNS mismatch: secret hostname does not resolve to this server's public IP. " +

+ 91
- 0
internal/cli/sni_check.go Dosyayı Görüntüle

@@ -0,0 +1,91 @@
1
+package cli
2
+
3
+import (
4
+	"context"
5
+	"net"
6
+
7
+	"github.com/9seconds/mtg/v2/internal/config"
8
+	"github.com/9seconds/mtg/v2/mtglib"
9
+)
10
+
11
+// sniCheckResult captures the outcome of comparing the secret hostname's DNS
12
+// records with this server's public IP addresses.
13
+//
14
+// IPv4Match/IPv6Match are true when either a matching record was found, or
15
+// when the corresponding public IP could not be detected — in which case
16
+// there is nothing to compare against.
17
+type sniCheckResult struct {
18
+	Host       string
19
+	Resolved   []net.IP
20
+	OurIPv4    net.IP
21
+	OurIPv6    net.IP
22
+	IPv4Match  bool
23
+	IPv6Match  bool
24
+	ResolveErr error
25
+}
26
+
27
+// Known reports whether at least one public IP family was detected.
28
+func (r sniCheckResult) Known() bool {
29
+	return r.OurIPv4 != nil || r.OurIPv6 != nil
30
+}
31
+
32
+// OK reports whether every detected public IP family matches a resolved
33
+// record. A partial match (one family matches, another does not) is not OK.
34
+func (r sniCheckResult) OK() bool {
35
+	return r.ResolveErr == nil && r.IPv4Match && r.IPv6Match
36
+}
37
+
38
+// runSNICheck resolves conf.Secret.Host and compares the result with the
39
+// server's public IPv4 and IPv6. Public IPs come from config first and fall
40
+// back to on-the-fly detection via ntw.
41
+func runSNICheck(ctx context.Context,
42
+	resolver *net.Resolver,
43
+	conf *config.Config,
44
+	ntw mtglib.Network,
45
+) sniCheckResult {
46
+	res := sniCheckResult{Host: conf.Secret.Host}
47
+
48
+	if res.Host == "" {
49
+		res.IPv4Match = true
50
+		res.IPv6Match = true
51
+
52
+		return res
53
+	}
54
+
55
+	addrs, err := resolver.LookupIPAddr(ctx, res.Host)
56
+	if err != nil {
57
+		res.ResolveErr = err
58
+
59
+		return res
60
+	}
61
+
62
+	res.Resolved = make([]net.IP, 0, len(addrs))
63
+	for _, a := range addrs {
64
+		res.Resolved = append(res.Resolved, a.IP)
65
+	}
66
+
67
+	res.OurIPv4 = conf.PublicIPv4.Get(nil)
68
+	if res.OurIPv4 == nil {
69
+		res.OurIPv4 = getIP(ntw, "tcp4")
70
+	}
71
+
72
+	res.OurIPv6 = conf.PublicIPv6.Get(nil)
73
+	if res.OurIPv6 == nil {
74
+		res.OurIPv6 = getIP(ntw, "tcp6")
75
+	}
76
+
77
+	res.IPv4Match = res.OurIPv4 == nil
78
+	res.IPv6Match = res.OurIPv6 == nil
79
+
80
+	for _, ip := range res.Resolved {
81
+		if res.OurIPv4 != nil && ip.String() == res.OurIPv4.String() {
82
+			res.IPv4Match = true
83
+		}
84
+
85
+		if res.OurIPv6 != nil && ip.String() == res.OurIPv6.String() {
86
+			res.IPv6Match = true
87
+		}
88
+	}
89
+
90
+	return res
91
+}

+ 25
- 6
internal/cli/utils.go Dosyayı Görüntüle

@@ -11,6 +11,14 @@ import (
11 11
 	"github.com/9seconds/mtg/v2/mtglib"
12 12
 )
13 13
 
14
+// publicIPEndpoints are tried in order. Each must return the client's public
15
+// IP as a single address in the plain-text response body.
16
+var publicIPEndpoints = []string{
17
+	"https://ifconfig.co",
18
+	"https://icanhazip.com",
19
+	"https://ifconfig.me",
20
+}
21
+
14 22
 func getIP(ntw mtglib.Network, protocol string) net.IP {
15 23
 	dialer := ntw.NativeDialer()
16 24
 	client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
@@ -21,19 +29,26 @@ func getIP(ntw mtglib.Network, protocol string) net.IP {
21 29
 		return essentials.WrapNetConn(conn), err
22 30
 	})
23 31
 
24
-	req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) //nolint: noctx
25
-	if err != nil {
26
-		panic(err)
32
+	for _, endpoint := range publicIPEndpoints {
33
+		if ip := fetchPublicIP(client, endpoint); ip != nil {
34
+			return ip
35
+		}
27 36
 	}
28 37
 
29
-	req.Header.Add("Accept", "text/plain")
38
+	return nil
39
+}
30 40
 
31
-	resp, err := client.Do(req)
41
+func fetchPublicIP(client *http.Client, endpoint string) net.IP {
42
+	req, err := http.NewRequest(http.MethodGet, endpoint, nil) //nolint: noctx
32 43
 	if err != nil {
33 44
 		return nil
34 45
 	}
35 46
 
36
-	if resp.StatusCode != http.StatusOK {
47
+	req.Header.Set("Accept", "text/plain")
48
+	req.Header.Set("User-Agent", "curl/8")
49
+
50
+	resp, err := client.Do(req)
51
+	if err != nil {
37 52
 		return nil
38 53
 	}
39 54
 
@@ -42,6 +57,10 @@ func getIP(ntw mtglib.Network, protocol string) net.IP {
42 57
 		resp.Body.Close()              //nolint: errcheck
43 58
 	}()
44 59
 
60
+	if resp.StatusCode != http.StatusOK {
61
+		return nil
62
+	}
63
+
45 64
 	data, err := io.ReadAll(resp.Body)
46 65
 	if err != nil {
47 66
 		return nil

Loading…
İptal
Kaydet