Просмотр исходного кода

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 недель назад
Родитель
Сommit
81e5a5ac82
4 измененных файлов: 191 добавлений и 80 удалений
  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 Просмотреть файл

52
 	)
52
 	)
53
 
53
 
54
 	tplODNSSNIMatch = template.Must(
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
 	tplEDNSSNIMatch = template.Must(
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
 	tplOFrontingDomain = template.Must(
64
 	tplOFrontingDomain = template.Must(
329
 }
332
 }
330
 
333
 
331
 func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) bool {
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
 		tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
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
 		return false
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
 		tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
346
 		tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
353
 			"description": "cannot detect public IP address",
347
 			"description": "cannot detect public IP address",
354
 			"error":       errors.New("cannot detect automatically and public-ipv4/public-ipv6 are not set in config"),
348
 			"error":       errors.New("cannot detect automatically and public-ipv4/public-ipv6 are not set in config"),
356
 		return false
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
 	tplEDNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
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
 	return false
404
 	return false

+ 20
- 43
internal/cli/run_proxy.go Просмотреть файл

209
 }
209
 }
210
 
210
 
211
 func warnSNIMismatch(conf *config.Config, ntw mtglib.Network, log mtglib.Logger) {
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
 		return
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
 		log.Warning("SNI-DNS check: cannot detect public IP address; set public-ipv4/public-ipv6 in config or run 'mtg doctor'")
225
 		log.Warning("SNI-DNS check: cannot detect public IP address; set public-ipv4/public-ipv6 in config or run 'mtg doctor'")
236
 		return
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
 		return
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
 	our := ""
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
 		if our != "" {
244
 		if our != "" {
268
 			our += "/"
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
 		BindStr("resolved", strings.Join(resolved, ", ")).
252
 		BindStr("resolved", strings.Join(resolved, ", ")).
276
 		BindStr("public_ip", our)
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
 	entry.Warning("SNI-DNS mismatch: secret hostname does not resolve to this server's public IP. " +
263
 	entry.Warning("SNI-DNS mismatch: secret hostname does not resolve to this server's public IP. " +

+ 91
- 0
internal/cli/sni_check.go Просмотреть файл

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 Просмотреть файл

11
 	"github.com/9seconds/mtg/v2/mtglib"
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
 func getIP(ntw mtglib.Network, protocol string) net.IP {
22
 func getIP(ntw mtglib.Network, protocol string) net.IP {
15
 	dialer := ntw.NativeDialer()
23
 	dialer := ntw.NativeDialer()
16
 	client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
24
 	client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
21
 		return essentials.WrapNetConn(conn), err
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
 	if err != nil {
43
 	if err != nil {
33
 		return nil
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
 		return nil
52
 		return nil
38
 	}
53
 	}
39
 
54
 
42
 		resp.Body.Close()              //nolint: errcheck
57
 		resp.Body.Close()              //nolint: errcheck
43
 	}()
58
 	}()
44
 
59
 
60
+	if resp.StatusCode != http.StatusOK {
61
+		return nil
62
+	}
63
+
45
 	data, err := io.ReadAll(resp.Body)
64
 	data, err := io.ReadAll(resp.Body)
46
 	if err != nil {
65
 	if err != nil {
47
 		return nil
66
 		return nil

Загрузка…
Отмена
Сохранить