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

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
 package cli
1
 package cli
2
 
2
 
3
 import (
3
 import (
4
+	"context"
4
 	"encoding/json"
5
 	"encoding/json"
5
 	"fmt"
6
 	"fmt"
6
 	"net"
7
 	"net"
8
 	"os"
9
 	"os"
9
 	"strconv"
10
 	"strconv"
10
 	"sync"
11
 	"sync"
12
+	"time"
11
 
13
 
12
 	"github.com/9seconds/mtg/v2/internal/config"
14
 	"github.com/9seconds/mtg/v2/internal/config"
13
 	"github.com/9seconds/mtg/v2/internal/utils"
15
 	"github.com/9seconds/mtg/v2/internal/utils"
54
 		return fmt.Errorf("cannot init network: %w", err)
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
 	wg := &sync.WaitGroup{}
63
 	wg := &sync.WaitGroup{}
58
 
64
 
59
 	wg.Go(func() {
65
 	wg.Go(func() {
62
 			ip = conf.PublicIPv4.Get(nil)
68
 			ip = conf.PublicIPv4.Get(nil)
63
 		}
69
 		}
64
 		if ip == nil {
70
 		if ip == nil {
65
-			ip = getIP(ntw, "tcp4")
71
+			ip = getIP(ctx, ntw, "tcp4", endpoints)
66
 		}
72
 		}
67
 
73
 
68
 		if ip != nil {
74
 		if ip != nil {
77
 			ip = conf.PublicIPv6.Get(nil)
83
 			ip = conf.PublicIPv6.Get(nil)
78
 		}
84
 		}
79
 		if ip == nil {
85
 		if ip == nil {
80
-			ip = getIP(ntw, "tcp6")
86
+			ip = getIP(ctx, ntw, "tcp6", endpoints)
81
 		}
87
 		}
82
 
88
 
83
 		if ip != nil {
89
 		if ip != nil {

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

332
 }
332
 }
333
 
333
 
334
 func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) bool {
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
 	if res.ResolveErr != nil {
340
 	if res.ResolveErr != nil {
338
 		tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
341
 		tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck

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

6
 	"net"
6
 	"net"
7
 	"os"
7
 	"os"
8
 	"strings"
8
 	"strings"
9
+	"time"
9
 
10
 
10
 	"github.com/9seconds/mtg/v2/antireplay"
11
 	"github.com/9seconds/mtg/v2/antireplay"
11
 	"github.com/9seconds/mtg/v2/events"
12
 	"github.com/9seconds/mtg/v2/events"
213
 		return
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
 	if res.ResolveErr != nil {
222
 	if res.ResolveErr != nil {
219
 		log.BindStr("hostname", res.Host).
223
 		log.BindStr("hostname", res.Host).
226
 		return
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
 	if res.OK() {
239
 	if res.OK() {
230
 		return
240
 		return
231
 	}
241
 	}

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

48
 // runSNICheck resolves conf.Secret.Host and compares the result with the
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
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
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
 func runSNICheck(ctx context.Context,
53
 func runSNICheck(ctx context.Context,
53
 	resolver *net.Resolver,
54
 	resolver *net.Resolver,
54
 	conf *config.Config,
55
 	conf *config.Config,
75
 		res.Resolved = append(res.Resolved, a.IP)
76
 		res.Resolved = append(res.Resolved, a.IP)
76
 	}
77
 	}
77
 
78
 
79
+	endpoints := resolvePublicIPEndpoints(conf.Network.PublicIPEndpoints)
78
 	wg := sync.WaitGroup{}
80
 	wg := sync.WaitGroup{}
79
 
81
 
80
 	wg.Go(func() {
82
 	wg.Go(func() {
81
 		res.OurIPv4 = conf.PublicIPv4.Get(nil)
83
 		res.OurIPv4 = conf.PublicIPv4.Get(nil)
82
 		if res.OurIPv4 == nil {
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
 	wg.Go(func() {
89
 	wg.Go(func() {
88
 		res.OurIPv6 = conf.PublicIPv6.Get(nil)
90
 		res.OurIPv6 = conf.PublicIPv6.Get(nil)
89
 		if res.OurIPv6 == nil {
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
 	"strings"
8
 	"strings"
9
 
9
 
10
 	"github.com/9seconds/mtg/v2/essentials"
10
 	"github.com/9seconds/mtg/v2/essentials"
11
+	"github.com/9seconds/mtg/v2/internal/config"
11
 	"github.com/9seconds/mtg/v2/mtglib"
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
 	"https://ifconfig.co",
19
 	"https://ifconfig.co",
18
 	"https://icanhazip.com",
20
 	"https://icanhazip.com",
19
 	"https://ifconfig.me",
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
 	dialer := ntw.NativeDialer()
46
 	dialer := ntw.NativeDialer()
24
 	client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
47
 	client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (essentials.Conn, error) {
25
 		conn, err := dialer.DialContext(ctx, protocol, address)
48
 		conn, err := dialer.DialContext(ctx, protocol, address)
29
 		return essentials.WrapNetConn(conn), err
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
 			return ip
57
 			return ip
35
 		}
58
 		}
36
 	}
59
 	}
38
 	return nil
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
 	if err != nil {
66
 	if err != nil {
44
 		return nil
67
 		return nil
45
 	}
68
 	}

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

71
 			Interval TypeDuration    `json:"interval"`
71
 			Interval TypeDuration    `json:"interval"`
72
 			Count    TypeConcurrency `json:"count"`
72
 			Count    TypeConcurrency `json:"count"`
73
 		} `json:"keepAlive"`
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
 	} `json:"network"`
78
 	} `json:"network"`
78
 	Stats struct {
79
 	Stats struct {
79
 		StatsD struct {
80
 		StatsD struct {

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

66
 			Interval string `toml:"interval" json:"interval,omitempty"`
66
 			Interval string `toml:"interval" json:"interval,omitempty"`
67
 			Count    uint   `toml:"count" json:"count,omitempty"`
67
 			Count    uint   `toml:"count" json:"count,omitempty"`
68
 		} `toml:"keep-alive" json:"keepAlive,omitempty"`
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
 	} `toml:"network" json:"network,omitempty"`
73
 	} `toml:"network" json:"network,omitempty"`
73
 	Stats struct {
74
 	Stats struct {
74
 		StatsD struct {
75
 		StatsD struct {

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