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

Address review feedback on SNI-DNS check refactor

- Bound public-IP detection with a 10s timeout context. The HTTP
  fallback chain in getIP could otherwise block proxy startup
  indefinitely on slow endpoints; the old single DNS lookup could
  not. Plumbed via context through getIP/fetchPublicIP and added
  context.WithTimeout in warnSNIMismatch, checkSecretHost, and
  Access.Run.

- Emit a dedicated warning in warnSNIMismatch when the secret
  hostname resolves successfully but to zero addresses, mirroring
  the doctor's tplEDNSSNINoResolve branch instead of falling
  through to a mismatch warning with an empty resolved list.

- Allow configuring network.public-ip-endpoints (TOML) /
  publicIpEndpoints (JSON) so deployments can override the default
  list (ifconfig.co, icanhazip.com, ifconfig.me). The default is
  preserved when the option is omitted.
pull/474/head
Alexey Dolotov 1 день назад
Родитель
Сommit
fff48a0532

+ 8
- 2
internal/cli/access.go Просмотреть файл

@@ -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 {

+ 4
- 1
internal/cli/doctor.go Просмотреть файл

@@ -332,7 +332,10 @@ func (d *Doctor) checkFrontingDomain(ntw mtglib.Network) bool {
332 332
 }
333 333
 
334 334
 func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) bool {
335
-	res := runSNICheck(context.Background(), resolver, d.conf, ntw)
335
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
336
+	defer cancel()
337
+
338
+	res := runSNICheck(ctx, resolver, d.conf, ntw)
336 339
 
337 340
 	if res.ResolveErr != nil {
338 341
 		tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck

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

@@ -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"
@@ -213,7 +214,10 @@ func warnSNIMismatch(conf *config.Config, ntw mtglib.Network, log mtglib.Logger)
213 214
 		return
214 215
 	}
215 216
 
216
-	res := runSNICheck(context.Background(), net.DefaultResolver, conf, ntw)
217
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
218
+	defer cancel()
219
+
220
+	res := runSNICheck(ctx, net.DefaultResolver, conf, ntw)
217 221
 
218 222
 	if res.ResolveErr != nil {
219 223
 		log.BindStr("hostname", res.Host).
@@ -226,6 +230,12 @@ func warnSNIMismatch(conf *config.Config, ntw mtglib.Network, log mtglib.Logger)
226 230
 		return
227 231
 	}
228 232
 
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
237
+	}
238
+
229 239
 	if res.OK() {
230 240
 		return
231 241
 	}

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

@@ -48,7 +48,8 @@ func (r sniCheckResult) OK() bool {
48 48
 // runSNICheck resolves conf.Secret.Host and compares the result with the
49 49
 // server's public IPv4 and IPv6. Public IPs come from config first and fall
50 50
 // back to on-the-fly detection via ntw. IP detection for the two families
51
-// runs concurrently.
51
+// runs concurrently and honors ctx — callers should supply a deadline,
52
+// since the HTTP fallback can otherwise block startup indefinitely.
52 53
 func runSNICheck(ctx context.Context,
53 54
 	resolver *net.Resolver,
54 55
 	conf *config.Config,
@@ -75,19 +76,20 @@ func runSNICheck(ctx context.Context,
75 76
 		res.Resolved = append(res.Resolved, a.IP)
76 77
 	}
77 78
 
79
+	endpoints := resolvePublicIPEndpoints(conf.Network.PublicIPEndpoints)
78 80
 	wg := sync.WaitGroup{}
79 81
 
80 82
 	wg.Go(func() {
81 83
 		res.OurIPv4 = conf.PublicIPv4.Get(nil)
82 84
 		if res.OurIPv4 == nil {
83
-			res.OurIPv4 = getIP(ntw, "tcp4")
85
+			res.OurIPv4 = getIP(ctx, ntw, "tcp4", endpoints)
84 86
 		}
85 87
 	})
86 88
 
87 89
 	wg.Go(func() {
88 90
 		res.OurIPv6 = conf.PublicIPv6.Get(nil)
89 91
 		if res.OurIPv6 == nil {
90
-			res.OurIPv6 = getIP(ntw, "tcp6")
92
+			res.OurIPv6 = getIP(ctx, ntw, "tcp6", endpoints)
91 93
 		}
92 94
 	})
93 95
 

+ 31
- 8
internal/cli/utils.go Просмотреть файл

@@ -8,18 +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
-// 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{
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{
17 19
 	"https://ifconfig.co",
18 20
 	"https://icanhazip.com",
19 21
 	"https://ifconfig.me",
20 22
 }
21 23
 
22
-func getIP(ntw mtglib.Network, protocol string) net.IP {
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 {
23 46
 	dialer := ntw.NativeDialer()
24 47
 	client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
25 48
 		conn, err := dialer.DialContext(ctx, protocol, address)
@@ -29,8 +52,8 @@ func getIP(ntw mtglib.Network, protocol string) net.IP {
29 52
 		return essentials.WrapNetConn(conn), err
30 53
 	})
31 54
 
32
-	for _, endpoint := range publicIPEndpoints {
33
-		if ip := fetchPublicIP(client, endpoint); ip != nil {
55
+	for _, endpoint := range endpoints {
56
+		if ip := fetchPublicIP(ctx, client, endpoint); ip != nil {
34 57
 			return ip
35 58
 		}
36 59
 	}
@@ -38,8 +61,8 @@ func getIP(ntw mtglib.Network, protocol string) net.IP {
38 61
 	return nil
39 62
 }
40 63
 
41
-func fetchPublicIP(client *http.Client, endpoint string) net.IP {
42
-	req, err := http.NewRequest(http.MethodGet, endpoint, nil) //nolint: noctx
64
+func fetchPublicIP(ctx context.Context, client *http.Client, endpoint string) net.IP {
65
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
43 66
 	if err != nil {
44 67
 		return nil
45 68
 	}

+ 4
- 3
internal/config/config.go Просмотреть файл

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

@@ -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 {

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