From 282a8ce78eab2c51de14b4a181ba4e0f78e6c15f Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 17 Sep 2024 20:42:27 +0700 Subject: [PATCH] all: add DNS Stamps support See: https://dnscrypt.info/stamps-specifications --- cmd/cli/prog.go | 4 ++ config.go | 61 ++++++++++++++++++++++++++++- config_internal_test.go | 86 +++++++++++++++++++++++++++++++++++++++++ config_test.go | 15 +++++++ go.mod | 1 + go.sum | 2 + resolver.go | 3 ++ 7 files changed, 170 insertions(+), 2 deletions(-) diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index 711e966..bfe32e0 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -308,7 +308,11 @@ func (p *prog) setupUpstream(cfg *ctrld.Config) { isControlDUpstream := false for n := range cfg.Upstream { uc := cfg.Upstream[n] + sdns := uc.Type == ctrld.ResolverTypeSDNS uc.Init() + if sdns { + mainLog.Load().Debug().Msgf("initialized DNS Stamps with endpoint: %s, type: %s", uc.Endpoint, uc.Type) + } isControlDUpstream = isControlDUpstream || uc.IsControlD() if uc.BootstrapIP == "" { uc.SetupBootstrapIP() diff --git a/config.go b/config.go index ab22045..86ca4b7 100644 --- a/config.go +++ b/config.go @@ -7,6 +7,7 @@ import ( "crypto/x509" "encoding/hex" "errors" + "fmt" "io" "math/rand" "net" @@ -22,6 +23,7 @@ import ( "sync/atomic" "time" + "github.com/ameshkov/dnsstamps" "github.com/go-playground/validator/v10" "github.com/miekg/dns" "github.com/spf13/viper" @@ -229,7 +231,7 @@ type NetworkConfig struct { // UpstreamConfig specifies configuration for upstreams that ctrld will forward requests to. type UpstreamConfig struct { Name string `mapstructure:"name" toml:"name,omitempty"` - Type string `mapstructure:"type" toml:"type,omitempty" validate:"oneof=doh doh3 dot doq os legacy"` + Type string `mapstructure:"type" toml:"type,omitempty" validate:"oneof=doh doh3 dot doq os legacy sdns"` Endpoint string `mapstructure:"endpoint" toml:"endpoint,omitempty"` BootstrapIP string `mapstructure:"bootstrap_ip" toml:"bootstrap_ip,omitempty"` Domain string `mapstructure:"-" toml:"-"` @@ -303,10 +305,13 @@ type Rule map[string][]string // Init initialized necessary values for an UpstreamConfig. func (uc *UpstreamConfig) Init() { + if err := uc.initDnsStamps(); err != nil { + ProxyLogger.Load().Fatal().Err(err).Msg("invalid DNS Stamps") + } uc.initDoHScheme() uc.uid = upstreamUID() if u, err := url.Parse(uc.Endpoint); err == nil { - uc.Domain = u.Host + uc.Domain = u.Hostname() switch uc.Type { case ResolverTypeDOH, ResolverTypeDOH3: uc.u = u @@ -694,6 +699,47 @@ func (uc *UpstreamConfig) initDoHScheme() { } } +// initDnsStamps initializes upstream config based on encoded DNS Stamps Endpoint. +func (uc *UpstreamConfig) initDnsStamps() error { + if uc.Type != ResolverTypeSDNS { + return nil + } + sdns, err := dnsstamps.NewServerStampFromString(uc.Endpoint) + if err != nil { + return err + } + ip, port, _ := net.SplitHostPort(sdns.ServerAddrStr) + providerName, port2, _ := net.SplitHostPort(sdns.ProviderName) + if port2 != "" { + port = port2 + } + if providerName == "" { + providerName = sdns.ProviderName + } + switch sdns.Proto { + case dnsstamps.StampProtoTypeDoH: + uc.Type = ResolverTypeDOH + host := sdns.ProviderName + if port != "" && port != defaultPortFor(uc.Type) { + host = net.JoinHostPort(providerName, port) + } + uc.Endpoint = "https://" + host + sdns.Path + case dnsstamps.StampProtoTypeTLS: + uc.Type = ResolverTypeDOT + uc.Endpoint = net.JoinHostPort(providerName, port) + case dnsstamps.StampProtoTypeDoQ: + uc.Type = ResolverTypeDOQ + uc.Endpoint = net.JoinHostPort(providerName, port) + case dnsstamps.StampProtoTypePlain: + uc.Type = ResolverTypeLegacy + uc.Endpoint = sdns.ServerAddrStr + default: + return fmt.Errorf("unsupported stamp protocol %q", sdns.Proto) + } + uc.BootstrapIP = ip + return nil +} + // Init initialized necessary values for an ListenerConfig. func (lc *ListenerConfig) Init() { if lc.Policy != nil { @@ -746,6 +792,17 @@ func upstreamConfigStructLevelValidation(sl validator.StructLevel) { return } + // initDoHScheme/initDnsStamps may change upstreams information, + // so restoring changed values after validation to keep original one. + defer func(ep, typ string) { + uc.Endpoint = ep + uc.Type = typ + }(uc.Endpoint, uc.Type) + + if err := uc.initDnsStamps(); err != nil { + sl.ReportError(uc.Endpoint, "endpoint", "Endpoint", "http_url", "") + return + } uc.initDoHScheme() // DoH/DoH3 requires endpoint is an HTTP url. if uc.Type == ResolverTypeDOH || uc.Type == ResolverTypeDOH3 { diff --git a/config_internal_test.go b/config_internal_test.go index 2dc05c3..41edd32 100644 --- a/config_internal_test.go +++ b/config_internal_test.go @@ -26,6 +26,7 @@ func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) { func TestUpstreamConfig_Init(t *testing.T) { u1, _ := url.Parse("https://example.com") u2, _ := url.Parse("https://example.com?k=v") + u3, _ := url.Parse("https://freedns.controld.com/p1") tests := []struct { name string uc *UpstreamConfig @@ -199,6 +200,91 @@ func TestUpstreamConfig_Init(t *testing.T) { u: u1, }, }, + { + "sdns -> doh", + &UpstreamConfig{ + Name: "sdns", + Type: "sdns", + Endpoint: "sdns://AgMAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29tAy9wMQ", + BootstrapIP: "", + Domain: "", + Timeout: 0, + IPStack: IpStackBoth, + }, + &UpstreamConfig{ + Name: "sdns", + Type: "doh", + Endpoint: "https://freedns.controld.com/p1", + BootstrapIP: "76.76.2.11", + Domain: "freedns.controld.com", + Timeout: 0, + IPStack: IpStackBoth, + u: u3, + }, + }, + { + "sdns -> dot", + &UpstreamConfig{ + Name: "sdns", + Type: "sdns", + Endpoint: "sdns://AwcAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29t", + BootstrapIP: "", + Domain: "", + Timeout: 0, + IPStack: IpStackBoth, + }, + &UpstreamConfig{ + Name: "sdns", + Type: "dot", + Endpoint: "freedns.controld.com:843", + BootstrapIP: "76.76.2.11", + Domain: "freedns.controld.com", + Timeout: 0, + IPStack: IpStackBoth, + }, + }, + { + "sdns -> doq", + &UpstreamConfig{ + Name: "sdns", + Type: "sdns", + Endpoint: "sdns://BAcAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29t", + BootstrapIP: "", + Domain: "", + Timeout: 0, + IPStack: IpStackBoth, + }, + &UpstreamConfig{ + Name: "sdns", + Type: "doq", + Endpoint: "freedns.controld.com:784", + BootstrapIP: "76.76.2.11", + Domain: "freedns.controld.com", + Timeout: 0, + IPStack: IpStackBoth, + }, + }, + { + "sdns -> legacy", + &UpstreamConfig{ + Name: "sdns", + Type: "sdns", + Endpoint: "sdns://AAcAAAAAAAAACjc2Ljc2LjIuMTE", + BootstrapIP: "", + Domain: "", + Timeout: 0, + IPStack: IpStackBoth, + }, + &UpstreamConfig{ + Name: "sdns", + Type: "legacy", + Endpoint: "76.76.2.11:53", + BootstrapIP: "76.76.2.11", + Domain: "76.76.2.11", + Timeout: 0, + IPStack: IpStackBoth, + }, + }, } for _, tc := range tests { diff --git a/config_test.go b/config_test.go index 03a1a3f..c1ffeb4 100644 --- a/config_test.go +++ b/config_test.go @@ -127,6 +127,21 @@ func TestConfigValidation(t *testing.T) { } } +func TestConfigValidationDoNotChangeEndpoint(t *testing.T) { + cfg := configWithInvalidDoHEndpoint(t) + endpointMap := map[string]struct{}{} + for _, uc := range cfg.Upstream { + endpointMap[uc.Endpoint] = struct{}{} + } + validate := validator.New() + _ = ctrld.ValidateConfig(validate, cfg) + for _, uc := range cfg.Upstream { + if _, ok := endpointMap[uc.Endpoint]; !ok { + t.Fatalf("expected endpoint '%s' to exist", uc.Endpoint) + } + } +} + func TestConfigDiscoverOverride(t *testing.T) { v := viper.NewWithOptions(viper.KeyDelimiter("::")) ctrld.InitConfig(v, "test_config_discover_override") diff --git a/go.mod b/go.mod index 525e70a..1dc51e0 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.23.1 require ( github.com/Masterminds/semver v1.5.0 + github.com/ameshkov/dnsstamps v1.0.3 github.com/coreos/go-systemd/v22 v22.5.0 github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf github.com/frankban/quicktest v1.14.6 diff --git a/go.sum b/go.sum index fb2b650..5c560e9 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= +github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= diff --git a/resolver.go b/resolver.go index 8df4a29..6f25ba3 100644 --- a/resolver.go +++ b/resolver.go @@ -30,6 +30,9 @@ const ( ResolverTypeLegacy = "legacy" // ResolverTypePrivate is like ResolverTypeOS, but use for local resolver only. ResolverTypePrivate = "private" + // ResolverTypeSDNS specifies resolver with information encoded using DNS Stamps. + // See: https://dnscrypt.info/stamps-specifications/ + ResolverTypeSDNS = "sdns" ) const (