diff --git a/example.config.toml b/example.config.toml index c0ca32c6f..abc0bffea 100644 --- a/example.config.toml +++ b/example.config.toml @@ -48,6 +48,13 @@ concurrency = 8192 # Only ipv4 connectivity is used prefer-ip = "prefer-ipv6" +# Public IP addresses of this server. Used by 'mtg access' to generate +# proxy links and by 'mtg doctor' to validate SNI-DNS match. +# If not set, mtg tries to detect them automatically via ifconfig.co. +# Set these if ifconfig.co is unreachable from your server. +# public-ipv4 = "1.2.3.4" +# public-ipv6 = "2001:db8::1" + # If this setting is set, then mtg will try to get proxy updates from Telegram # Usually this is completely fine to have it disabled, because mtg has a list # of some core proxies hardcoded. diff --git a/internal/cli/access.go b/internal/cli/access.go index ec266e90a..c93c97b19 100644 --- a/internal/cli/access.go +++ b/internal/cli/access.go @@ -58,6 +58,9 @@ func (a *Access) Run(cli *CLI, version string) error { wg.Go(func() { ip := a.PublicIPv4 + if ip == nil { + ip = conf.PublicIPv4.Get(nil) + } if ip == nil { ip = getIP(ntw, "tcp4") } @@ -70,6 +73,9 @@ func (a *Access) Run(cli *CLI, version string) error { }) wg.Go(func() { ip := a.PublicIPv6 + if ip == nil { + ip = conf.PublicIPv6.Get(nil) + } if ip == nil { ip = getIP(ntw, "tcp6") } diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 21ea0e12e..b00c3e08a 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -332,13 +332,20 @@ func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) boo return false } - ourIP4 := getIP(ntw, "tcp4") - ourIP6 := getIP(ntw, "tcp6") + ourIP4 := d.conf.PublicIPv4.Get(nil) + if ourIP4 == nil { + ourIP4 = getIP(ntw, "tcp4") + } + + ourIP6 := d.conf.PublicIPv6.Get(nil) + if ourIP6 == nil { + ourIP6 = getIP(ntw, "tcp6") + } if ourIP4 == nil && ourIP6 == nil { tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck "description": "cannot detect public IP address", - "error": errors.New("ifconfig.co is unreachable for both IPv4 and IPv6"), + "error": errors.New("cannot detect automatically and public-ipv4/public-ipv6 are not set in config"), }) return false } diff --git a/internal/config/config.go b/internal/config/config.go index 6bb0a6cb5..4ca566c8c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -35,6 +35,8 @@ type Config struct { DomainFrontingProxyProtocol TypeBool `json:"domainFrontingProxyProtocol"` TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"` Concurrency TypeConcurrency `json:"concurrency"` + PublicIPv4 TypeIP `json:"publicIpv4"` + PublicIPv6 TypeIP `json:"publicIpv6"` DomainFronting struct { IP TypeIP `json:"ip"` Port TypePort `json:"port"` diff --git a/internal/config/config_test.go b/internal/config/config_test.go index e6b19e060..c291c29f8 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -42,6 +42,32 @@ func (suite *ConfigTestSuite) TestParseMinimalConfig() { suite.Equal("0.0.0.0:3128", conf.BindTo.String()) } +func (suite *ConfigTestSuite) TestParsePublicIP() { + conf, err := config.Parse(suite.ReadConfig("public_ip.toml")) + suite.NoError(err) + suite.Equal("203.0.113.1", conf.PublicIPv4.Get(nil).String()) + suite.Equal("2001:db8::1", conf.PublicIPv6.Get(nil).String()) +} + +func (suite *ConfigTestSuite) TestParsePublicIPv4Only() { + conf, err := config.Parse(suite.ReadConfig("public_ip_v4_only.toml")) + suite.NoError(err) + suite.Equal("203.0.113.1", conf.PublicIPv4.Get(nil).String()) + suite.Nil(conf.PublicIPv6.Get(nil)) +} + +func (suite *ConfigTestSuite) TestParsePublicIPInvalid() { + _, err := config.Parse(suite.ReadConfig("public_ip_invalid.toml")) + suite.Error(err) +} + +func (suite *ConfigTestSuite) TestParsePublicIPNotSet() { + conf, err := config.Parse(suite.ReadConfig("minimal.toml")) + suite.NoError(err) + suite.Nil(conf.PublicIPv4.Get(nil)) + suite.Nil(conf.PublicIPv6.Get(nil)) +} + func (suite *ConfigTestSuite) TestString() { conf, err := config.Parse(suite.ReadConfig("minimal.toml")) suite.NoError(err) diff --git a/internal/config/parse.go b/internal/config/parse.go index 6cf824d64..fd607087e 100644 --- a/internal/config/parse.go +++ b/internal/config/parse.go @@ -21,6 +21,8 @@ type tomlConfig struct { DomainFrontingProxyProtocol bool `toml:"domain-fronting-proxy-protocol" json:"domainFrontingProxyProtocol,omitempty"` TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"` Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` + PublicIPv4 string `toml:"public-ipv4" json:"publicIpv4,omitempty"` + PublicIPv6 string `toml:"public-ipv6" json:"publicIpv6,omitempty"` DomainFronting struct { IP string `toml:"ip" json:"ip,omitempty"` Port uint `toml:"port" json:"port,omitempty"` diff --git a/internal/config/testdata/public_ip.toml b/internal/config/testdata/public_ip.toml new file mode 100644 index 000000000..6e4673c15 --- /dev/null +++ b/internal/config/testdata/public_ip.toml @@ -0,0 +1,4 @@ +secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t" +bind-to = "0.0.0.0:3128" +public-ipv4 = "203.0.113.1" +public-ipv6 = "2001:db8::1" diff --git a/internal/config/testdata/public_ip_invalid.toml b/internal/config/testdata/public_ip_invalid.toml new file mode 100644 index 000000000..0c9777630 --- /dev/null +++ b/internal/config/testdata/public_ip_invalid.toml @@ -0,0 +1,3 @@ +secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t" +bind-to = "0.0.0.0:3128" +public-ipv4 = "not-an-ip" diff --git a/internal/config/testdata/public_ip_v4_only.toml b/internal/config/testdata/public_ip_v4_only.toml new file mode 100644 index 000000000..f0c63c800 --- /dev/null +++ b/internal/config/testdata/public_ip_v4_only.toml @@ -0,0 +1,3 @@ +secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t" +bind-to = "0.0.0.0:3128" +public-ipv4 = "203.0.113.1"