From 897e6bf505294c779bb4933910cace5b4b10ac62 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Fri, 27 Feb 2026 15:33:49 +0100 Subject: [PATCH] Propagate DNS setting to configuration --- example.config.toml | 41 ++++++---- internal/config/config.go | 12 +++ internal/config/parse.go | 1 + internal/config/type_dns_uri.go | 69 ++++++++++++++++ internal/config/type_dns_uri_test.go | 117 +++++++++++++++++++++++++++ network/v2/dns.go | 8 ++ 6 files changed, 234 insertions(+), 14 deletions(-) create mode 100644 internal/config/type_dns_uri.go create mode 100644 internal/config/type_dns_uri_test.go diff --git a/example.config.toml b/example.config.toml index 0e40c3f..4cd84d0 100644 --- a/example.config.toml +++ b/example.config.toml @@ -134,8 +134,33 @@ allow-fallback-on-unknown-dc = false # it has to access. # # By default we use Cloudflare. +# +# DEPRECATED option: +# If dns option is specified, it will be used instead doh-ip = "1.1.1.1" +# Starting from mtg v2.1.12 we have changed a configuration for DNS. Now it +# supports DNS-over-HTTPS, DNS-over-TLS, custom UDP resolver and system +# resolver. +# +# Here is how to define DNS-over-HTTPS: +# - https://1.1.1.1 +# - https://1.1.1.1/dns-query +# - https://cloudflare-dns.com/dns-query +# - https://cloudflare-dns.com +# +# Here is how to define DNS-over-TLS: +# - tls://1.1.1.1 +# - tls://cloudflare-dns.com +# +# Here is how to define a custom UDP resolver (we support only IPs here) +# - 1.1.1.1 +# - udp://1.1.1.1 +# +# If you set it to empty string, default resolver will be used. +# But please comment out doh-ip +dns = "https://1.1.1.1" + # mtg can work via proxies (for now, we support only socks5). Proxy # configuration is done via list. So, you can specify many proxies # there. @@ -149,25 +174,13 @@ doh-ip = "1.1.1.1" # # Proxy configuration is done via ordinary URI schema: # -# socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s +# socks5://user:password@host:port # # Only socks5 proxy is used. user/password is optional. As you can # see, you can specify some parameters in GET query. These parameters # configure circuit breaker. -# -# open_threshold means a number of errors which should happen so we stop -# use a proxy. -# -# half_open_timeout means a time period (in Golang duration notation) -# after which we can retry with this proxy -# -# reset_failures_timeout means a time period when we flush out errors -# when circuit breaker in closed state. -# -# Please see https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker -# on details about circuit breakers. proxies = [ - # "socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s" + # "socks5://user:password@host:port" ] # network timeouts define different settings for timeouts. tcp timeout diff --git a/internal/config/config.go b/internal/config/config.go index 15a76ab..e3d8bd2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net" + "net/url" "github.com/9seconds/mtg/v2/mtglib" ) @@ -56,6 +57,7 @@ type Config struct { Idle TypeDuration `json:"idle"` } `json:"timeout"` DOHIP TypeIP `json:"dohIp"` + DNS TypeDNSURI `json:"dns"` Proxies []TypeProxyURL `json:"proxies"` } `json:"network"` Stats struct { @@ -76,6 +78,16 @@ type Config struct { } `json:"stats"` } +func (c *Config) GetDNS() *url.URL { + var dohURL *url.URL + + if dohIP := c.Network.DOHIP.Get(nil); dohIP != nil { + dohURL, _ = url.Parse("https://" + dohIP.String()) + } + + return c.Network.DNS.Get(dohURL) +} + func (c *Config) GetDomainFrontingPort(defaultValue uint) uint { if port := c.DomainFronting.Port.Get(0); port != 0 { return port diff --git a/internal/config/parse.go b/internal/config/parse.go index bb5ccce..1186769 100644 --- a/internal/config/parse.go +++ b/internal/config/parse.go @@ -52,6 +52,7 @@ type tomlConfig struct { Idle string `toml:"idle" json:"idle,omitempty"` } `toml:"timeout" json:"timeout,omitempty"` DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"` + DNS string `toml:"dns" json:"dns,omitempty"` Proxies []string `toml:"proxies" json:"proxies,omitempty"` } `toml:"network" json:"network,omitempty"` Stats struct { diff --git a/internal/config/type_dns_uri.go b/internal/config/type_dns_uri.go new file mode 100644 index 0000000..eeb09d2 --- /dev/null +++ b/internal/config/type_dns_uri.go @@ -0,0 +1,69 @@ +package config + +import ( + "fmt" + "net" + "net/url" +) + +type TypeDNSURI struct { + Value *url.URL +} + +func (t *TypeDNSURI) Set(value string) error { + parsed, err := url.Parse(value) + if err != nil { + return fmt.Errorf("value is not URI: %w", err) + } + + if parsed.Host == "" { + parsed.Host = parsed.Path + parsed.Path = "" + parsed.Scheme = "udp" + } + + switch parsed.Scheme { + case "https", "tls": + case "udp": + if ip := net.ParseIP(parsed.Hostname()); ip == nil { + return fmt.Errorf("simple DNS must IP address: %s", parsed.Hostname()) + } + default: + return fmt.Errorf("unsupported DNS type %s", parsed.Scheme) + } + + if parsed.Scheme != "https" && parsed.Path != "" { + return fmt.Errorf("path is supported only for DoH: %s", parsed) + } + + if parsed.User != nil { + return fmt.Errorf("used info is not supported: %s", parsed.User.String()) + } + + t.Value = parsed + + return nil +} + +func (t *TypeDNSURI) Get(defaultValue *url.URL) *url.URL { + if t.Value != nil { + return t.Value + } + + return defaultValue +} + +func (t *TypeDNSURI) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeDNSURI) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeDNSURI) String() string { + if t.Value == nil { + return "" + } + return t.Value.String() +} diff --git a/internal/config/type_dns_uri_test.go b/internal/config/type_dns_uri_test.go new file mode 100644 index 0000000..939ca90 --- /dev/null +++ b/internal/config/type_dns_uri_test.go @@ -0,0 +1,117 @@ +package config_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeDNSURITestStruct struct { + Value config.TypeDNSURI `json:"value"` +} + +type TypeDNSURITestSuite struct { + suite.Suite +} + +func (suite *TypeDNSURITestSuite) TestUnmarshalFail() { + testData := []string{ + "xx", + "ppar", + "", + "dns://hahaha", + "udp://xcxxcv", + "udp://1.1.1.1/xcv", + "1.1.1.1/xxx", + "tls://dns/xx", + "tls://1.1.1.1/xx", + "https://user:password@1.1.1.1", + "tls://user:password@1.1.1.1", + "udp://user:password@1.1.1.1", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeDNSURITestStruct{})) + }) + } +} + +func (suite *TypeDNSURITestSuite) TestUnmarshalOk() { + testData := []string{ + "1.1.1.1", + "tls://1.1.1.1", + "tls://dns.google", + "https://1.1.1.1", + "https://1.1.1.1/dns-query", + "https://dns.google", + "https://dns.google/dns-query", + "udp://1.1.1.1", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + testStruct := &typeDNSURITestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + if v == "1.1.1.1" { + v = "udp://" + v + } + assert.Equal(t, v, testStruct.Value.String()) + }) + } +} + +func (suite *TypeDNSURITestSuite) TestMarshalOk() { + testData := []string{ + "tls://1.1.1.1", + "tls://dns.google", + "https://1.1.1.1", + "https://1.1.1.1/dns-query", + } + + for _, v := range testData { + suite.T().Run(v, func(t *testing.T) { + testStruct := &typePreferIPTestStruct{ + Value: config.TypePreferIP{ + Value: v, + }, + } + + encodedJSON, err := json.Marshal(testStruct) + assert.NoError(t, err) + + expectedJSON, err := json.Marshal(map[string]string{ + "value": v, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) + }) + } +} + +func (suite *TypeDNSURITestSuite) TestGet() { + value := config.TypeDNSURI{} + suite.Nil(value.Get(nil)) + + suite.NoError(value.Set("tls://1.1.1.1")) + suite.NotNil(value.Get(nil)) +} + +func TestDNSURI(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeDNSURITestSuite{}) +} diff --git a/network/v2/dns.go b/network/v2/dns.go index 0728609..ddb0273 100644 --- a/network/v2/dns.go +++ b/network/v2/dns.go @@ -21,6 +21,14 @@ func GetDNS(u *url.URL) (*net.Resolver, error) { return dns.NewCachingResolver(nil, dnsCacheOptions...), nil } + if u.Scheme == "" { + u.Scheme = "udp" + } + if u.Scheme == "udp" && u.Host == "" { + u.Host = u.Path + u.Path = "" + } + switch u.Scheme { case "tls": return dns.NewDoTResolver(u.Host, dns.DoTCache(dnsCacheOptions...))