Alexey Dolotov 1 giorno fa
parent
commit
b6298c5e71
Nessun account collegato all'indirizzo email del committer

+ 15
- 3
example.config.toml Vedi File

@@ -49,9 +49,10 @@ concurrency = 8192
49 49
 prefer-ip = "prefer-ipv6"
50 50
 
51 51
 # Public IP addresses of this server. Used by 'mtg access' to generate
52
-# proxy links and by 'mtg doctor' to validate SNI-DNS match.
53
-# If not set, mtg tries to detect them automatically via ifconfig.co.
54
-# Set these if ifconfig.co is unreachable from your server.
52
+# proxy links and by 'mtg doctor' / proxy startup to validate SNI-DNS match.
53
+# If not set, mtg tries to detect them automatically by querying the public
54
+# HTTPS endpoints listed in network.public-ip-endpoints (see below).
55
+# Set these explicitly if those endpoints are unreachable from your server.
55 56
 # public-ipv4 = "1.2.3.4"
56 57
 # public-ipv6 = "2001:db8::1"
57 58
 
@@ -200,6 +201,17 @@ proxies = [
200 201
     # "socks5://user:password@host:port"
201 202
 ]
202 203
 
204
+# HTTPS endpoints used to discover this server's public IPv4/IPv6 when
205
+# public-ipv4 / public-ipv6 are not set. Each must return the client's public
206
+# IP as a single address in the plain-text response body. mtg tries them in
207
+# order and uses the first that succeeds. The default is shown below; setting
208
+# this option overrides the default entirely.
209
+# public-ip-endpoints = [
210
+#     "https://ifconfig.co",
211
+#     "https://icanhazip.com",
212
+#     "https://ifconfig.me",
213
+# ]
214
+
203 215
 # network timeouts define different settings for timeouts. tcp timeout
204 216
 # define a global timeout on establishing of network connections. idle
205 217
 # means a timeout on pumping data between sockset when nothing is

+ 8
- 2
internal/cli/access.go Vedi File

@@ -1,6 +1,7 @@
1 1
 package cli
2 2
 
3 3
 import (
4
+	"context"
4 5
 	"encoding/json"
5 6
 	"fmt"
6 7
 	"net"
@@ -8,6 +9,7 @@ import (
8 9
 	"os"
9 10
 	"strconv"
10 11
 	"sync"
12
+	"time"
11 13
 
12 14
 	"github.com/9seconds/mtg/v2/internal/config"
13 15
 	"github.com/9seconds/mtg/v2/internal/utils"
@@ -54,6 +56,10 @@ func (a *Access) Run(cli *CLI, version string) error {
54 56
 		return fmt.Errorf("cannot init network: %w", err)
55 57
 	}
56 58
 
59
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
60
+	defer cancel()
61
+
62
+	endpoints := resolvePublicIPEndpoints(conf.Network.PublicIPEndpoints)
57 63
 	wg := &sync.WaitGroup{}
58 64
 
59 65
 	wg.Go(func() {
@@ -62,7 +68,7 @@ func (a *Access) Run(cli *CLI, version string) error {
62 68
 			ip = conf.PublicIPv4.Get(nil)
63 69
 		}
64 70
 		if ip == nil {
65
-			ip = getIP(ntw, "tcp4")
71
+			ip = getIP(ctx, ntw, "tcp4", endpoints)
66 72
 		}
67 73
 
68 74
 		if ip != nil {
@@ -77,7 +83,7 @@ func (a *Access) Run(cli *CLI, version string) error {
77 83
 			ip = conf.PublicIPv6.Get(nil)
78 84
 		}
79 85
 		if ip == nil {
80
-			ip = getIP(ntw, "tcp6")
86
+			ip = getIP(ctx, ntw, "tcp6", endpoints)
81 87
 		}
82 88
 
83 89
 		if ip != nil {

+ 58
- 31
internal/cli/doctor.go Vedi File

@@ -53,10 +53,13 @@ var (
53 53
 	)
54 54
 
55 55
 	tplODNSSNIMatch = template.Must(
56
-		template.New("").Parse("  ✅ IP address {{ .ip }} matches secret hostname {{ .hostname }}\n"),
56
+		template.New("").Parse("  ✅ Secret hostname {{ .hostname }} matches our public IP ({{ .our }}); resolved: {{ .resolved }}\n"),
57 57
 	)
58 58
 	tplEDNSSNIMatch = template.Must(
59
-		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"),
59
+		template.New("").Parse("  ❌ Secret hostname {{ .hostname }} resolves to {{ .resolved }} but our public IP is {{ .our }}{{ if .families }} (mismatched families: {{ .families }}){{ end }}\n"),
60
+	)
61
+	tplEDNSSNINoResolve = template.Must(
62
+		template.New("").Parse("  ❌ Secret hostname {{ .hostname }} cannot be resolved to any address\n"),
60 63
 	)
61 64
 
62 65
 	tplOFrontingDomain = template.Must(
@@ -349,26 +352,20 @@ func (d *Doctor) checkFrontingDomain(ntw mtglib.Network) bool {
349 352
 }
350 353
 
351 354
 func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) bool {
352
-	addresses, err := resolver.LookupIPAddr(context.Background(), d.conf.Secret.Host)
353
-	if err != nil {
355
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
356
+	defer cancel()
357
+
358
+	res := runSNICheck(ctx, resolver, d.conf, ntw)
359
+
360
+	if res.ResolveErr != nil {
354 361
 		tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
355
-			"description": fmt.Sprintf("cannot resolve DNS name of %s", d.conf.Secret.Host),
356
-			"error":       err,
362
+			"description": fmt.Sprintf("cannot resolve DNS name of %s", res.Host),
363
+			"error":       res.ResolveErr,
357 364
 		})
358 365
 		return false
359 366
 	}
360 367
 
361
-	ourIP4 := d.conf.PublicIPv4.Get(nil)
362
-	if ourIP4 == nil {
363
-		ourIP4 = getIP(ntw, "tcp4")
364
-	}
365
-
366
-	ourIP6 := d.conf.PublicIPv6.Get(nil)
367
-	if ourIP6 == nil {
368
-		ourIP6 = getIP(ntw, "tcp6")
369
-	}
370
-
371
-	if ourIP4 == nil && ourIP6 == nil {
368
+	if !res.Known() {
372 369
 		tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
373 370
 			"description": "cannot detect public IP address",
374 371
 			"error":       errors.New("cannot detect automatically and public-ipv4/public-ipv6 are not set in config"),
@@ -376,25 +373,55 @@ func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) boo
376 373
 		return false
377 374
 	}
378 375
 
379
-	strAddresses := []string{}
380
-	for _, value := range addresses {
381
-		if (ourIP4 != nil && value.IP.String() == ourIP4.String()) ||
382
-			(ourIP6 != nil && value.IP.String() == ourIP6.String()) {
383
-			tplODNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
384
-				"ip":       value.IP,
385
-				"hostname": d.conf.Secret.Host,
386
-			})
387
-			return true
376
+	if len(res.Resolved) == 0 {
377
+		tplEDNSSNINoResolve.Execute(os.Stdout, map[string]any{ //nolint: errcheck
378
+			"hostname": res.Host,
379
+		})
380
+		return false
381
+	}
382
+
383
+	resolved := make([]string, 0, len(res.Resolved))
384
+	for _, ip := range res.Resolved {
385
+		resolved = append(resolved, `"`+ip.String()+`"`)
386
+	}
387
+
388
+	our := ""
389
+	if res.OurIPv4 != nil {
390
+		our = res.OurIPv4.String()
391
+	}
392
+
393
+	if res.OurIPv6 != nil {
394
+		if our != "" {
395
+			our += "/"
388 396
 		}
389 397
 
390
-		strAddresses = append(strAddresses, `"`+value.IP.String()+`"`)
398
+		our += res.OurIPv6.String()
399
+	}
400
+
401
+	if res.OK() {
402
+		tplODNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
403
+			"hostname": res.Host,
404
+			"resolved": strings.Join(resolved, ", "),
405
+			"our":      our,
406
+		})
407
+		return true
408
+	}
409
+
410
+	mismatched := []string{}
411
+
412
+	if res.OurIPv4 != nil && !res.IPv4Match {
413
+		mismatched = append(mismatched, "IPv4")
414
+	}
415
+
416
+	if res.OurIPv6 != nil && !res.IPv6Match {
417
+		mismatched = append(mismatched, "IPv6")
391 418
 	}
392 419
 
393 420
 	tplEDNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
394
-		"hostname": d.conf.Secret.Host,
395
-		"resolved": strings.Join(strAddresses, ", "),
396
-		"ip4":      ourIP4,
397
-		"ip6":      ourIP6,
421
+		"hostname": res.Host,
422
+		"resolved": strings.Join(resolved, ", "),
423
+		"our":      our,
424
+		"families": strings.Join(mismatched, ", "),
398 425
 	})
399 426
 
400 427
 	return false

+ 27
- 40
internal/cli/run_proxy.go Vedi File

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

+ 112
- 0
internal/cli/sni_check.go Vedi File

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

+ 49
- 7
internal/cli/utils.go Vedi File

@@ -8,10 +8,41 @@ import (
8 8
 	"strings"
9 9
 
10 10
 	"github.com/9seconds/mtg/v2/essentials"
11
+	"github.com/9seconds/mtg/v2/internal/config"
11 12
 	"github.com/9seconds/mtg/v2/mtglib"
12 13
 )
13 14
 
14
-func getIP(ntw mtglib.Network, protocol string) net.IP {
15
+// defaultPublicIPEndpoints is the fallback used when network.public-ip-endpoints
16
+// is not set in config. Each endpoint must return the client's public IP as a
17
+// single address in the plain-text response body.
18
+var defaultPublicIPEndpoints = []string{
19
+	"https://ifconfig.co",
20
+	"https://icanhazip.com",
21
+	"https://ifconfig.me",
22
+}
23
+
24
+// resolvePublicIPEndpoints returns the configured endpoint list, falling back
25
+// to defaultPublicIPEndpoints when none are configured.
26
+func resolvePublicIPEndpoints(configured []config.TypeHttpsURL) []string {
27
+	if len(configured) == 0 {
28
+		return defaultPublicIPEndpoints
29
+	}
30
+
31
+	out := make([]string, 0, len(configured))
32
+	for _, u := range configured {
33
+		if v := u.Get(nil); v != nil {
34
+			out = append(out, v.String())
35
+		}
36
+	}
37
+
38
+	if len(out) == 0 {
39
+		return defaultPublicIPEndpoints
40
+	}
41
+
42
+	return out
43
+}
44
+
45
+func getIP(ctx context.Context, ntw mtglib.Network, protocol string, endpoints []string) net.IP {
15 46
 	dialer := ntw.NativeDialer()
16 47
 	client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
17 48
 		conn, err := dialer.DialContext(ctx, protocol, address)
@@ -21,19 +52,26 @@ func getIP(ntw mtglib.Network, protocol string) net.IP {
21 52
 		return essentials.WrapNetConn(conn), err
22 53
 	})
23 54
 
24
-	req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) //nolint: noctx
25
-	if err != nil {
26
-		panic(err)
55
+	for _, endpoint := range endpoints {
56
+		if ip := fetchPublicIP(ctx, client, endpoint); ip != nil {
57
+			return ip
58
+		}
27 59
 	}
28 60
 
29
-	req.Header.Add("Accept", "text/plain")
61
+	return nil
62
+}
30 63
 
31
-	resp, err := client.Do(req)
64
+func fetchPublicIP(ctx context.Context, client *http.Client, endpoint string) net.IP {
65
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
32 66
 	if err != nil {
33 67
 		return nil
34 68
 	}
35 69
 
36
-	if resp.StatusCode != http.StatusOK {
70
+	req.Header.Set("Accept", "text/plain")
71
+	req.Header.Set("User-Agent", "curl/8")
72
+
73
+	resp, err := client.Do(req)
74
+	if err != nil {
37 75
 		return nil
38 76
 	}
39 77
 
@@ -42,6 +80,10 @@ func getIP(ntw mtglib.Network, protocol string) net.IP {
42 80
 		resp.Body.Close()              //nolint: errcheck
43 81
 	}()
44 82
 
83
+	if resp.StatusCode != http.StatusOK {
84
+		return nil
85
+	}
86
+
45 87
 	data, err := io.ReadAll(resp.Body)
46 88
 	if err != nil {
47 89
 		return nil

+ 4
- 3
internal/config/config.go Vedi File

@@ -71,9 +71,10 @@ type Config struct {
71 71
 			Interval TypeDuration    `json:"interval"`
72 72
 			Count    TypeConcurrency `json:"count"`
73 73
 		} `json:"keepAlive"`
74
-		DOHIP   TypeIP         `json:"dohIp"`
75
-		DNS     TypeDNSURI     `json:"dns"`
76
-		Proxies []TypeProxyURL `json:"proxies"`
74
+		DOHIP             TypeIP         `json:"dohIp"`
75
+		DNS               TypeDNSURI     `json:"dns"`
76
+		Proxies           []TypeProxyURL `json:"proxies"`
77
+		PublicIPEndpoints []TypeHttpsURL `json:"publicIpEndpoints"`
77 78
 	} `json:"network"`
78 79
 	Stats struct {
79 80
 		StatsD struct {

+ 4
- 3
internal/config/parse.go Vedi File

@@ -66,9 +66,10 @@ type tomlConfig struct {
66 66
 			Interval string `toml:"interval" json:"interval,omitempty"`
67 67
 			Count    uint   `toml:"count" json:"count,omitempty"`
68 68
 		} `toml:"keep-alive" json:"keepAlive,omitempty"`
69
-		DOHIP   string   `toml:"doh-ip" json:"dohIp,omitempty"`
70
-		DNS     string   `toml:"dns" json:"dns,omitempty"`
71
-		Proxies []string `toml:"proxies" json:"proxies,omitempty"`
69
+		DOHIP             string   `toml:"doh-ip" json:"dohIp,omitempty"`
70
+		DNS               string   `toml:"dns" json:"dns,omitempty"`
71
+		Proxies           []string `toml:"proxies" json:"proxies,omitempty"`
72
+		PublicIPEndpoints []string `toml:"public-ip-endpoints" json:"publicIpEndpoints,omitempty"`
72 73
 	} `toml:"network" json:"network,omitempty"`
73 74
 	Stats struct {
74 75
 		StatsD struct {

Loading…
Annulla
Salva