Parcourir la source

Merge pull request #480 from dolonet/feat/domain-fronting-host

config: accept hostname for [domain-fronting] target
master
Sergei Arkhipov il y a 23 heures
Parent
révision
8d143d7bde
Aucun compte lié à l'adresse e-mail de l'auteur

+ 15
- 4
example.config.toml Voir le fichier

@@ -112,11 +112,22 @@ allow-fallback-on-unknown-dc = false
112 112
 # required.
113 113
 [domain-fronting]
114 114
 # By default, mtg resolves the fronting hostname (from the secret) via DNS
115
-# to establish a TCP connection. If DNS resolution of that hostname is blocked,
116
-# you can specify an IP address to connect to directly. The hostname is still
117
-# used for SNI in the TLS handshake.
115
+# to establish a TCP connection. If that resolution is blocked, or loops
116
+# back to this server (e.g. mtg sits behind an SNI router whose DNS points
117
+# at itself), override the destination here.
118 118
 #
119
-# default value is not set (DNS resolution is used).
119
+# Use `host` — accepts a hostname or a literal IP. Hostnames are resolved
120
+# at dial time, so a dual-stack DNS record can reach the right backend
121
+# address family for IPv4 or IPv6 clients.
122
+#
123
+# The hostname from the secret is still used for SNI in the TLS handshake.
124
+#
125
+# default value is not set (the secret's hostname is used).
126
+# host = "fronting-backend"
127
+
128
+# Deprecated: use `host`. If `ip` is set, mtg logs a warning at startup
129
+# and ignores the value (domain-fronting falls back to the secret's
130
+# hostname unless `host` is also set).
120 131
 # ip = "10.10.10.11"
121 132
 
122 133
 # FakeTLS uses domain fronting protection. So it needs to know a port to

+ 14
- 3
internal/cli/doctor.go Voir le fichier

@@ -146,7 +146,18 @@ func (d *Doctor) checkDeprecatedConfig() bool {
146 146
 			"when":        "2.3.0",
147 147
 			"old":         "domain-fronting-ip",
148 148
 			"old_section": "",
149
-			"new":         "ip",
149
+			"new":         "host",
150
+			"new_section": "domain-fronting",
151
+		})
152
+	}
153
+
154
+	if d.conf.DomainFronting.IP.Value != nil {
155
+		ok = false
156
+		tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
157
+			"when":        "2.4.0",
158
+			"old":         "ip",
159
+			"old_section": "domain-fronting",
160
+			"new":         "host",
150 161
 			"new_section": "domain-fronting",
151 162
 		})
152 163
 	}
@@ -318,8 +329,8 @@ func (d *Doctor) checkNetworkAddresses(ntw mtglib.Network, addresses []string) e
318 329
 
319 330
 func (d *Doctor) checkFrontingDomain(ntw mtglib.Network) bool {
320 331
 	host := d.conf.Secret.Host
321
-	if ip := d.conf.GetDomainFrontingIP(nil); ip != "" {
322
-		host = ip
332
+	if override := d.conf.GetDomainFrontingHost(); override != "" {
333
+		host = override
323 334
 	}
324 335
 
325 336
 	port := d.conf.GetDomainFrontingPort(mtglib.DefaultDomainFrontingPort)

+ 13
- 1
internal/cli/run_proxy.go Voir le fichier

@@ -287,11 +287,23 @@ func warnSNIMismatch(conf *config.Config, ntw mtglib.Network, log mtglib.Logger)
287 287
 		"DPI may detect and block the proxy. See 'mtg doctor' for details")
288 288
 }
289 289
 
290
+func warnDeprecatedDomainFronting(conf *config.Config, log mtglib.Logger) {
291
+	if conf.DomainFrontingIP.Value != nil {
292
+		log.Warning(`config option "domain-fronting-ip" is deprecated and ignored; use "host" in [domain-fronting] instead`)
293
+	}
294
+
295
+	if conf.DomainFronting.IP.Value != nil {
296
+		log.Warning(`config option "ip" in [domain-fronting] is deprecated and ignored; use "host" instead`)
297
+	}
298
+}
299
+
290 300
 func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyclop
291 301
 	logger := makeLogger(conf)
292 302
 
293 303
 	logger.BindJSON("configuration", conf.String()).Debug("configuration")
294 304
 
305
+	warnDeprecatedDomainFronting(conf, logger)
306
+
295 307
 	eventStream, err := makeEventStream(conf, logger)
296 308
 	if err != nil {
297 309
 		return fmt.Errorf("cannot build event stream: %w", err)
@@ -343,7 +355,7 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen, cyc
343 355
 		Secret:                      conf.Secret,
344 356
 		Concurrency:                 conf.GetConcurrency(mtglib.DefaultConcurrency),
345 357
 		DomainFrontingPort:          conf.GetDomainFrontingPort(mtglib.DefaultDomainFrontingPort),
346
-		DomainFrontingIP:            conf.GetDomainFrontingIP(nil),
358
+		DomainFrontingHost:          conf.GetDomainFrontingHost(),
347 359
 		DomainFrontingProxyProtocol: conf.GetDomainFrontingProxyProtocol(false),
348 360
 		PreferIP:                    conf.PreferIP.Get(mtglib.DefaultPreferIP),
349 361
 		AutoUpdate:                  conf.AutoUpdate.Get(false),

+ 12
- 2
internal/cli/simple_run.go Voir le fichier

@@ -17,8 +17,9 @@ type SimpleRun struct {
17 17
 	Concurrency         uint64        `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"`                        //nolint: lll
18 18
 	TCPBuffer           string        `kong:"name='tcp-buffer',short='b',default='4KB',help='Deprecated and ignored'"`                                                 //nolint: lll
19 19
 	PreferIP            string        `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"` //nolint: lll
20
-	DomainFrontingPort  uint64        `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"`                        //nolint: lll
21
-	DomainFrontingIP    string        `kong:"name='domain-fronting-ip',help='An IP address to use for domain fronting instead of resolving the hostname via DNS.'"`    //nolint: lll
20
+	DomainFrontingPort  uint64        `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"`                                                                  //nolint: lll
21
+	DomainFrontingHost  string        `kong:"name='domain-fronting-host',help='Hostname or IP to dial for domain fronting instead of resolving the secret hostname.'"`                                           //nolint: lll
22
+	DomainFrontingIP    string        `kong:"name='domain-fronting-ip',help='Deprecated: use --domain-fronting-host. Setting this flag logs a warning at startup and the value is ignored.'"`                    //nolint: lll
22 23
 	DOHIP               net.IP        `kong:"name='doh-ip',short='n',default='1.1.1.1',help='IP address of DNS-over-HTTP to use.'"`                                    //nolint: lll
23 24
 	Timeout             time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"`                                                    //nolint: lll
24 25
 	Socks5Proxies       []string      `kong:"name='socks5-proxy',short='s',help='Socks5 proxies to use for network access.'"`                                          //nolint: lll
@@ -48,6 +49,15 @@ func (s *SimpleRun) Run(cli *CLI, version string) error { //nolint: cyclop,funle
48 49
 		return fmt.Errorf("incorrect domain-fronting-port: %w", err)
49 50
 	}
50 51
 
52
+	if s.DomainFrontingHost != "" {
53
+		if err := conf.DomainFronting.Host.Set(s.DomainFrontingHost); err != nil {
54
+			return fmt.Errorf("incorrect domain-fronting-host: %w", err)
55
+		}
56
+	}
57
+
58
+	// --domain-fronting-ip is deprecated; the value is parsed only so the
59
+	// runtime check in runProxy can detect it and emit the warn-and-ignore
60
+	// log message. The value never reaches the dial path.
51 61
 	if s.DomainFrontingIP != "" {
52 62
 		if err := conf.DomainFrontingIP.Set(s.DomainFrontingIP); err != nil {
53 63
 			return fmt.Errorf("incorrect domain-fronting-ip: %w", err)

+ 3
- 9
internal/config/config.go Voir le fichier

@@ -4,7 +4,6 @@ import (
4 4
 	"bytes"
5 5
 	"encoding/json"
6 6
 	"fmt"
7
-	"net"
8 7
 	"net/url"
9 8
 
10 9
 	"github.com/9seconds/mtg/v2/mtglib"
@@ -38,6 +37,7 @@ type Config struct {
38 37
 	PublicIPv4                  TypeIP          `json:"publicIpv4"`
39 38
 	PublicIPv6                  TypeIP          `json:"publicIpv6"`
40 39
 	DomainFronting              struct {
40
+		Host          TypeHost `json:"host"`
41 41
 		IP            TypeIP   `json:"ip"`
42 42
 		Port          TypePort `json:"port"`
43 43
 		ProxyProtocol TypeBool `json:"proxyProtocol"`
@@ -117,14 +117,8 @@ func (c *Config) GetDomainFrontingPort(defaultValue uint) uint {
117 117
 	return c.DomainFrontingPort.Get(defaultValue)
118 118
 }
119 119
 
120
-func (c *Config) GetDomainFrontingIP(defaultValue net.IP) string {
121
-	if ip := c.DomainFronting.IP.Get(nil); ip != nil {
122
-		return ip.String()
123
-	}
124
-	if ip := c.DomainFrontingIP.Get(defaultValue); ip != nil {
125
-		return ip.String()
126
-	}
127
-	return ""
120
+func (c *Config) GetDomainFrontingHost() string {
121
+	return c.DomainFronting.Host.Get("")
128 122
 }
129 123
 
130 124
 func (c *Config) GetDomainFrontingProxyProtocol(defaultValue bool) bool {

+ 41
- 0
internal/config/config_test.go Voir le fichier

@@ -74,6 +74,47 @@ func (suite *ConfigTestSuite) TestString() {
74 74
 	suite.NotEmpty(conf.String())
75 75
 }
76 76
 
77
+func (suite *ConfigTestSuite) TestDomainFrontingIPIgnoredWhenHostSet() {
78
+	conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
79
+	suite.NoError(err)
80
+
81
+	suite.NoError(conf.DomainFronting.Host.Set("fronting-backend"))
82
+	suite.NoError(conf.DomainFronting.IP.Set("10.0.0.10"))
83
+	suite.NoError(conf.Validate())
84
+	suite.Equal("fronting-backend", conf.GetDomainFrontingHost())
85
+}
86
+
87
+func (suite *ConfigTestSuite) TestDomainFrontingHostFromTOML() {
88
+	conf, err := config.Parse(suite.ReadConfig("domain_fronting_host.toml"))
89
+	suite.NoError(err)
90
+	suite.NoError(conf.Validate())
91
+	suite.Equal("fronting-backend", conf.GetDomainFrontingHost())
92
+}
93
+
94
+func (suite *ConfigTestSuite) TestDomainFrontingHostAcceptsLiteralIP() {
95
+	conf, err := config.Parse(suite.ReadConfig("domain_fronting_host_ip.toml"))
96
+	suite.NoError(err)
97
+	suite.NoError(conf.Validate())
98
+	suite.Equal("10.0.0.1", conf.GetDomainFrontingHost())
99
+}
100
+
101
+func (suite *ConfigTestSuite) TestDomainFrontingIPIgnoredFromTOML() {
102
+	conf, err := config.Parse(suite.ReadConfig("domain_fronting_ip.toml"))
103
+	suite.NoError(err)
104
+	suite.NoError(conf.Validate())
105
+	// Deprecated [domain-fronting].ip is parsed but never used to derive
106
+	// the dial target — the user must migrate to [domain-fronting].host.
107
+	suite.NotNil(conf.DomainFronting.IP.Get(nil))
108
+	suite.Equal("", conf.GetDomainFrontingHost())
109
+}
110
+
111
+func (suite *ConfigTestSuite) TestDomainFrontingNotSet() {
112
+	conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
113
+	suite.NoError(err)
114
+	suite.NoError(conf.Validate())
115
+	suite.Equal("", conf.GetDomainFrontingHost())
116
+}
117
+
77 118
 func TestConfig(t *testing.T) {
78 119
 	t.Parallel()
79 120
 	suite.Run(t, &ConfigTestSuite{})

+ 1
- 0
internal/config/parse.go Voir le fichier

@@ -24,6 +24,7 @@ type tomlConfig struct {
24 24
 	PublicIPv4                  string `toml:"public-ipv4" json:"publicIpv4,omitempty"`
25 25
 	PublicIPv6                  string `toml:"public-ipv6" json:"publicIpv6,omitempty"`
26 26
 	DomainFronting              struct {
27
+		Host          string `toml:"host" json:"host,omitempty"`
27 28
 		IP            string `toml:"ip" json:"ip,omitempty"`
28 29
 		Port          uint   `toml:"port" json:"port,omitempty"`
29 30
 		ProxyProtocol bool   `toml:"proxy-protocol" json:"proxyProtocol,omitempty"`

+ 5
- 0
internal/config/testdata/domain_fronting_host.toml Voir le fichier

@@ -0,0 +1,5 @@
1
+secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
2
+bind-to = "0.0.0.0:3128"
3
+
4
+[domain-fronting]
5
+host = "fronting-backend"

+ 5
- 0
internal/config/testdata/domain_fronting_host_ip.toml Voir le fichier

@@ -0,0 +1,5 @@
1
+secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
2
+bind-to = "0.0.0.0:3128"
3
+
4
+[domain-fronting]
5
+host = "10.0.0.1"

+ 5
- 0
internal/config/testdata/domain_fronting_ip.toml Voir le fichier

@@ -0,0 +1,5 @@
1
+secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
2
+bind-to = "0.0.0.0:3128"
3
+
4
+[domain-fronting]
5
+ip = "10.0.0.10"

+ 61
- 0
internal/config/type_host.go Voir le fichier

@@ -0,0 +1,61 @@
1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"net"
6
+	"strings"
7
+)
8
+
9
+// TypeHost is a non-empty string that is either a literal IP address
10
+// (IPv4 or IPv6) or a hostname suitable for DNS resolution. It does not
11
+// include a port — the port belongs in a separate field.
12
+type TypeHost struct {
13
+	Value string
14
+}
15
+
16
+func (t *TypeHost) Set(value string) error {
17
+	if value == "" {
18
+		return fmt.Errorf("host cannot be empty")
19
+	}
20
+
21
+	if net.ParseIP(value) != nil {
22
+		t.Value = value
23
+
24
+		return nil
25
+	}
26
+
27
+	if strings.ContainsAny(value, " \t\n/?#") {
28
+		return fmt.Errorf("incorrect host %q", value)
29
+	}
30
+
31
+	// At this point value is not a parsed IP (IPv6 literals returned
32
+	// above), so any remaining colon indicates a host:port form, which
33
+	// belongs in a separate field.
34
+	if strings.Contains(value, ":") {
35
+		return fmt.Errorf("host must not contain a port: %q", value)
36
+	}
37
+
38
+	t.Value = value
39
+
40
+	return nil
41
+}
42
+
43
+func (t TypeHost) Get(defaultValue string) string {
44
+	if t.Value == "" {
45
+		return defaultValue
46
+	}
47
+
48
+	return t.Value
49
+}
50
+
51
+func (t *TypeHost) UnmarshalText(data []byte) error {
52
+	return t.Set(string(data))
53
+}
54
+
55
+func (t TypeHost) MarshalText() ([]byte, error) {
56
+	return []byte(t.Value), nil
57
+}
58
+
59
+func (t TypeHost) String() string {
60
+	return t.Value
61
+}

+ 77
- 0
internal/config/type_host_test.go Voir le fichier

@@ -0,0 +1,77 @@
1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"testing"
6
+
7
+	"github.com/9seconds/mtg/v2/internal/config"
8
+	"github.com/stretchr/testify/assert"
9
+	"github.com/stretchr/testify/suite"
10
+)
11
+
12
+type typeHostTestStruct struct {
13
+	Value config.TypeHost `json:"value"`
14
+}
15
+
16
+type TypeHostTestSuite struct {
17
+	suite.Suite
18
+}
19
+
20
+func (suite *TypeHostTestSuite) TestUnmarshalFail() {
21
+	testData := []string{
22
+		"",
23
+		"web:8443",
24
+		"http://example.com",
25
+		"example.com/path",
26
+		"two words",
27
+	}
28
+
29
+	for _, v := range testData {
30
+		data, err := json.Marshal(map[string]string{
31
+			"value": v,
32
+		})
33
+		suite.NoError(err)
34
+
35
+		suite.T().Run(v, func(t *testing.T) {
36
+			assert.Error(t, json.Unmarshal(data, &typeHostTestStruct{}))
37
+		})
38
+	}
39
+}
40
+
41
+func (suite *TypeHostTestSuite) TestUnmarshalOk() {
42
+	testData := []string{
43
+		"example.com",
44
+		"web",
45
+		"sub.example.com",
46
+		"127.0.0.1",
47
+		"2001:db8::1",
48
+	}
49
+
50
+	for _, v := range testData {
51
+		value := v
52
+
53
+		data, err := json.Marshal(map[string]string{
54
+			"value": value,
55
+		})
56
+		suite.NoError(err)
57
+
58
+		suite.T().Run(value, func(t *testing.T) {
59
+			testStruct := &typeHostTestStruct{}
60
+			assert.NoError(t, json.Unmarshal(data, testStruct))
61
+			assert.Equal(t, value, testStruct.Value.Get(""))
62
+		})
63
+	}
64
+}
65
+
66
+func (suite *TypeHostTestSuite) TestGet() {
67
+	value := config.TypeHost{}
68
+	suite.Equal("default", value.Get("default"))
69
+
70
+	suite.NoError(value.Set("example.com"))
71
+	suite.Equal("example.com", value.Get("default"))
72
+}
73
+
74
+func TestTypeHost(t *testing.T) {
75
+	t.Parallel()
76
+	suite.Run(t, &TypeHostTestSuite{})
77
+}

+ 10
- 5
mtglib/proxy.go Voir le fichier

@@ -30,7 +30,7 @@ type Proxy struct {
30 30
 	idleTimeout                 time.Duration
31 31
 	handshakeTimeout            time.Duration
32 32
 	domainFrontingPort          int
33
-	domainFrontingIP            string
33
+	domainFrontingHost          string
34 34
 	domainFrontingProxyProtocol bool
35 35
 	workerPool                  *ants.PoolWithFunc
36 36
 	telegram                    *dc.Telegram
@@ -48,11 +48,12 @@ type Proxy struct {
48 48
 }
49 49
 
50 50
 // DomainFrontingAddress returns a host:port pair for a fronting domain.
51
-// If DomainFrontingIP is set, it is used instead of resolving the hostname.
51
+// If a fronting host (literal IP or hostname) is configured, it is used
52
+// instead of resolving the secret's hostname.
52 53
 func (p *Proxy) DomainFrontingAddress() string {
53 54
 	host := p.secret.Host
54
-	if p.domainFrontingIP != "" {
55
-		host = p.domainFrontingIP
55
+	if p.domainFrontingHost != "" {
56
+		host = p.domainFrontingHost
56 57
 	}
57 58
 
58 59
 	return net.JoinHostPort(host, strconv.Itoa(p.domainFrontingPort))
@@ -343,6 +344,10 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
343 344
 	logger := opts.getLogger("proxy")
344 345
 	updatersLogger := logger.Named("telegram-updaters")
345 346
 
347
+	if opts.DomainFrontingIP != "" {
348
+		logger.Warning("mtglib.ProxyOpts.DomainFrontingIP is deprecated and ignored; use DomainFrontingHost instead")
349
+	}
350
+
346 351
 	proxy := &Proxy{
347 352
 		ctx:                      ctx,
348 353
 		ctxCancel:                cancel,
@@ -354,7 +359,7 @@ func NewProxy(opts ProxyOpts) (*Proxy, error) {
354 359
 		eventStream:              opts.EventStream,
355 360
 		logger:                   logger,
356 361
 		domainFrontingPort:       opts.getDomainFrontingPort(),
357
-		domainFrontingIP:         opts.DomainFrontingIP,
362
+		domainFrontingHost:       opts.DomainFrontingHost,
358 363
 		tolerateTimeSkewness:     opts.getTolerateTimeSkewness(),
359 364
 		idleTimeout:              opts.getIdleTimeout(),
360 365
 		handshakeTimeout:         opts.getHandshakeTimeout(),

+ 15
- 4
mtglib/proxy_opts.go Voir le fichier

@@ -105,13 +105,24 @@ type ProxyOpts struct {
105 105
 	// This is an optional setting.
106 106
 	DomainFrontingPort uint
107 107
 
108
-	// DomainFrontingIP is an IP address to use when connecting to the fronting
109
-	// domain instead of resolving the hostname from the secret via DNS.
108
+	// DomainFrontingHost is the address to use when connecting to the
109
+	// fronting domain instead of resolving the hostname from the secret via
110
+	// DNS. It can be a literal IP or a hostname; hostnames are resolved at
111
+	// dial time via the native dialer (which honours dual-stack and Happy
112
+	// Eyeballs).
110 113
 	//
111
-	// This is useful when DNS resolution of the fronting host is blocked.
112
-	// The hostname from the secret is still used for SNI in the TLS handshake.
114
+	// This is useful when DNS resolution of the secret's hostname is blocked
115
+	// or loops back to this server. The hostname from the secret is still
116
+	// used for SNI in the TLS handshake.
113 117
 	//
114 118
 	// This is an optional setting.
119
+	DomainFrontingHost string
120
+
121
+	// DomainFrontingIP previously held the dial target for the fronting
122
+	// domain. The setting is no longer honoured: setting it logs a warning
123
+	// at proxy startup and the value is dropped.
124
+	//
125
+	// Deprecated: use DomainFrontingHost. Setting this field has no effect.
115 126
 	DomainFrontingIP string
116 127
 
117 128
 	// DomainFrontingProxyProtocol is used if communication between upstream

Chargement…
Annuler
Enregistrer