From 0abe90f2e5962dc4feb3982b1251d5f25750a1c5 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 1 Apr 2022 23:46:40 +0300 Subject: [PATCH 1/2] DoH spike --- resolver/resolver-plain.go | 70 +++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/resolver/resolver-plain.go b/resolver/resolver-plain.go index 2ddcff90..9b1854c5 100644 --- a/resolver/resolver-plain.go +++ b/resolver/resolver-plain.go @@ -2,8 +2,13 @@ package resolver import ( "context" + "encoding/base64" "errors" + "fmt" + "io/ioutil" "net" + "net/http" + "strings" "time" "github.com/miekg/dns" @@ -40,7 +45,7 @@ func (pr *PlainResolver) Query(ctx context.Context, q *Query) (*RRCache, error) // create query dnsQuery := new(dns.Msg) dnsQuery.SetQuestion(q.FQDN, uint16(q.QType)) - + var reply *dns.Msg // get timeout from context and config var timeout time.Duration if deadline, ok := ctx.Deadline(); !ok { @@ -52,32 +57,51 @@ func (pr *PlainResolver) Query(ctx context.Context, q *Query) (*RRCache, error) timeout = defaultRequestTimeout } - // create client - dnsClient := &dns.Client{ - Timeout: timeout, - Dialer: &net.Dialer{ - Timeout: timeout, - LocalAddr: getLocalAddr("udp"), - }, - } + if strings.HasPrefix(pr.resolver.ServerAddress, "https:") { + buf, err := dnsQuery.Pack() - // query server - reply, ttl, err := dnsClient.Exchange(dnsQuery, pr.resolver.ServerAddress) - log.Tracer(ctx).Tracef("resolver: query took %s", ttl) - // error handling - if err != nil { - // Hint network environment at failed connection if err is not a timeout. - var nErr net.Error - if errors.As(err, &nErr) && !nErr.Timeout() { - netenv.ReportFailedConnection() + if err != nil { + return nil, err } - return nil, err - } + b64dns := base64.RawStdEncoding.EncodeToString(buf) + url := fmt.Sprintf("%s/dns-query?dns=%s", pr.resolver.ServerAddress, b64dns) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + reply := new(dns.Msg) + reply.Unpack(body) + } else { + // create client + dnsClient := &dns.Client{ + Timeout: timeout, + Dialer: &net.Dialer{ + Timeout: timeout, + LocalAddr: getLocalAddr("udp"), + }, + } - // check if blocked - if pr.resolver.IsBlockedUpstream(reply) { - return nil, &BlockedUpstreamError{pr.resolver.Info.DescriptiveName()} + // query server + reply, ttl, err := dnsClient.Exchange(dnsQuery, pr.resolver.ServerAddress) + log.Tracer(ctx).Tracef("resolver: query took %s", ttl) + // error handling + if err != nil { + // Hint network environment at failed connection if err is not a timeout. + var nErr net.Error + if errors.As(err, &nErr) && !nErr.Timeout() { + netenv.ReportFailedConnection() + } + + return nil, err + } + + // check if blocked + if pr.resolver.IsBlockedUpstream(reply) { + return nil, &BlockedUpstreamError{pr.resolver.Info.DescriptiveName()} + } } // hint network environment at successful connection From ecbbb3a33e1d84a096cac0e7c67886de72cb21e2 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 13 Apr 2022 17:35:23 +0300 Subject: [PATCH 2/2] DoH first working version --- resolver/config.go | 26 ++++++++- resolver/resolver-https.go | 116 +++++++++++++++++++++++++++++++++++++ resolver/resolver.go | 1 + resolver/resolvers.go | 25 ++++++-- 4 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 resolver/resolver-https.go diff --git a/resolver/config.go b/resolver/config.go index b274f119..e0ddb852 100644 --- a/resolver/config.go +++ b/resolver/config.go @@ -111,7 +111,7 @@ The format is: "protocol://ip:port?parameter=value¶meter=value" ExpertiseLevel: config.ExpertiseLevelExpert, ReleaseLevel: config.ReleaseLevelStable, DefaultValue: defaultNameServers, - ValidationRegex: fmt.Sprintf("^(%s|%s|%s)://.*", ServerTypeDoT, ServerTypeDNS, ServerTypeTCP), + ValidationRegex: fmt.Sprintf("^(%s|%s|%s|%s)://.*", ServerTypeDoT, ServerTypeDoH, ServerTypeDNS, ServerTypeTCP), ValidationFunc: validateNameservers, Annotations: config.Annotations{ config.DisplayHintAnnotation: config.DisplayHintOrdered, @@ -126,6 +126,14 @@ The format is: "protocol://ip:port?parameter=value¶meter=value" "dot://149.112.112.112:853?verify=dns.quad9.net&name=Quad9&blockedif=empty", }, }, + { + Name: "Quad9 DoH", + Action: config.QuickReplace, + Value: []string{ + "doh://149.112.112.112:443?verify=dns.quad9.net&name=Quad9&blockedif=empty", + "doh://9.9.9.9:443?verify=dns.quad9.net&name=Quad9&blockedif=empty", + }, + }, { Name: "AdGuard", Action: config.QuickReplace, @@ -134,6 +142,14 @@ The format is: "protocol://ip:port?parameter=value¶meter=value" "dot://94.140.15.15:853?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip", }, }, + { + Name: "AdGuard DoH", + Action: config.QuickReplace, + Value: []string{ + "doh://94.140.14.14:443?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip", + "doh://94.140.15.15:443?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip", + }, + }, { Name: "Foundation for Applied Privacy", Action: config.QuickReplace, @@ -150,6 +166,14 @@ The format is: "protocol://ip:port?parameter=value¶meter=value" "dot://1.0.0.2:853?verify=cloudflare-dns.com&name=Cloudflare&blockedif=zeroip", }, }, + { + Name: "Cloudflare (with Malware Filter) DoH", + Action: config.QuickReplace, + Value: []string{ + "doh://1.1.1.2:443?verify=cloudflare-dns.com&name=Cloudflare&blockedif=zeroip", + "doh://1.0.0.2:443?verify=cloudflare-dns.com&name=Cloudflare&blockedif=zeroip", + }, + }, }, "self:detail:internalSpecialUseDomains": internalSpecialUseDomains, "self:detail:connectivityDomains": netenv.ConnectivityDomains, diff --git a/resolver/resolver-https.go b/resolver/resolver-https.go new file mode 100644 index 00000000..6069723f --- /dev/null +++ b/resolver/resolver-https.go @@ -0,0 +1,116 @@ +package resolver + +import ( + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "github.com/miekg/dns" +) + +// TCPResolver is a resolver using just a single tcp connection with pipelining. +type HttpsResolver struct { + BasicResolverConn +} + +// tcpQuery holds the query information for a tcpResolverConn. +type HttpsQuery struct { + Query *Query + Response chan *dns.Msg +} + +// MakeCacheRecord creates an RRCache record from a reply. +func (tq *HttpsQuery) MakeCacheRecord(reply *dns.Msg, resolverInfo *ResolverInfo) *RRCache { + return &RRCache{ + Domain: tq.Query.FQDN, + Question: tq.Query.QType, + RCode: reply.Rcode, + Answer: reply.Answer, + Ns: reply.Ns, + Extra: reply.Extra, + Resolver: resolverInfo.Copy(), + } +} + +// NewTCPResolver returns a new TPCResolver. +func NewHttpsResolver(resolver *Resolver) *HttpsResolver { + newResolver := &HttpsResolver{ + BasicResolverConn: BasicResolverConn{ + resolver: resolver, + }, + } + newResolver.BasicResolverConn.init() + return newResolver +} + +// Query executes the given query against the resolver. +func (hr *HttpsResolver) Query(ctx context.Context, q *Query) (*RRCache, error) { + // Get resolver connection. + dnsQuery := new(dns.Msg) + dnsQuery.SetQuestion(q.FQDN, uint16(q.QType)) + + buf, err := dnsQuery.Pack() + + if err != nil { + return nil, err + } + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + ServerName: hr.resolver.VerifyDomain, + // TODO: use portbase rng + }, + } + + b64dns := base64.RawStdEncoding.EncodeToString(buf) + + url := &url.URL{ + Scheme: "https", + Host: hr.resolver.ServerAddress, + Path: fmt.Sprintf("%s/dns-query", hr.resolver.Path), // "dns-query" path is specified in rfc-8484 (https://www.rfc-editor.org/rfc/rfc8484.html) + ForceQuery: true, + RawQuery: fmt.Sprintf("dns=%s", b64dns), + } + + request := &http.Request{ + Method: "GET", + URL: url, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Body: nil, + Host: hr.resolver.ServerAddress, + } + + client := &http.Client{Transport: tr} + + resp, err := client.Do(request) + + if err != nil { + return nil, err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + reply := new(dns.Msg) + reply.Unpack(body) + + newRecord := &RRCache{ + Domain: q.FQDN, + Question: q.QType, + RCode: reply.Rcode, + Answer: reply.Answer, + Ns: reply.Ns, + Extra: reply.Extra, + Resolver: hr.resolver.Info.Copy(), + } + + // TODO: check if reply.Answer is valid + return newRecord, nil +} diff --git a/resolver/resolver.go b/resolver/resolver.go index 56d4b6cc..5e15a0e6 100644 --- a/resolver/resolver.go +++ b/resolver/resolver.go @@ -64,6 +64,7 @@ type Resolver struct { VerifyDomain string Search []string SearchOnly bool + Path string // logic interface Conn ResolverConn `json:"-"` diff --git a/resolver/resolvers.go b/resolver/resolvers.go index 0f7eaa16..42fc3c17 100644 --- a/resolver/resolvers.go +++ b/resolver/resolvers.go @@ -31,6 +31,7 @@ const ( parameterBlockedIf = "blockedif" parameterSearch = "search" parameterSearchOnly = "search-only" + parameterPath = "path" ) var ( @@ -78,6 +79,8 @@ func resolverConnFactory(resolver *Resolver) ResolverConn { return NewTCPResolver(resolver) case ServerTypeDoT: return NewTCPResolver(resolver).UseTLS() + case ServerTypeDoH: + return NewHttpsResolver(resolver) case ServerTypeDNS: return NewPlainResolver(resolver) default: @@ -92,7 +95,7 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) { } switch u.Scheme { - case ServerTypeDNS, ServerTypeDoT, ServerTypeTCP: + case ServerTypeDNS, ServerTypeDoT, ServerTypeDoH, ServerTypeTCP: default: return nil, false, fmt.Errorf("DNS resolver scheme %q invalid", u.Scheme) } @@ -136,7 +139,8 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) { parameterVerify, parameterBlockedIf, parameterSearch, - parameterSearchOnly: + parameterSearchOnly, + parameterPath: // Known key, continue. default: // Unknown key, abort. @@ -146,13 +150,23 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) { // Check domain verification config. verifyDomain := query.Get(parameterVerify) - if verifyDomain != "" && u.Scheme != ServerTypeDoT { - return nil, false, fmt.Errorf("domain verification only supported in DOT") + if verifyDomain != "" && !(u.Scheme == ServerTypeDoT || u.Scheme == ServerTypeDoH) { + return nil, false, fmt.Errorf("domain verification only supported in DoT and DoH") } - if verifyDomain == "" && u.Scheme == ServerTypeDoT { + if verifyDomain == "" && (u.Scheme == ServerTypeDoT || u.Scheme == ServerTypeDoH) { return nil, false, fmt.Errorf("DOT must have a verify query parameter set") } + // Check path for https (doh) request + path := query.Get(parameterPath) + if path != "" && u.Scheme != "doh" { + return nil, false, fmt.Errorf("path parameter is only supported in DoH") + } + + if path != "" && !strings.HasPrefix(path, "/") { + path = "/" + path + } + // Check block detection type. blockType := query.Get(parameterBlockedIf) if blockType == "" { @@ -177,6 +191,7 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) { }, ServerAddress: net.JoinHostPort(ip.String(), strconv.Itoa(int(port))), VerifyDomain: verifyDomain, + Path: path, UpstreamBlockDetection: blockType, }