From 6d2eae4f5804d0548e3412a43bbe396e9ae0b866 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sat, 24 Apr 2021 17:27:43 +0200 Subject: [PATCH] Add support for DNS stamps and for DNSCrypt servers This allows connections to DoH servers using their stamp, and adds support for DNSCrypt servers by the way. --- README.md | 4 +- cmd/doggo/cli/help.go | 1 + completions/doggo.fish | 2 +- go.mod | 3 +- go.sum | 20 +++++++-- internal/app/nameservers.go | 18 +++++++++ pkg/models/models.go | 11 ++--- pkg/resolvers/dnscrypt.go | 81 +++++++++++++++++++++++++++++++++++++ pkg/resolvers/resolver.go | 11 +++++ 9 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 pkg/resolvers/dnscrypt.go diff --git a/README.md b/README.md index 572c818..10d15ec 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ --- -**doggo** is a modern command-line DNS client (like _dig_) written in Golang. It outputs information in a neat concise manner and supports protocols like DoH, DoT as well. +**doggo** is a modern command-line DNS client (like _dig_) written in Golang. It outputs information in a neat concise manner and supports protocols like DoH, DoT and DNSCrypt as well. It's totally inspired from [dog](https://github.com/ogham/dog/) which is written in Rust. I wanted to add some features to it but since I don't know Rust, I found it as a nice opportunity to experiment with writing a DNS Client from scratch in `Go` myself. Hence the name `dog` +`go` => **doggo**. @@ -195,6 +195,8 @@ URL scheme of the server is used to identify which resolver to use for lookups. @tcp:// eg: @1.1.1.1 initiates a TCP resolver for 1.1.1.1:53. @https:// eg: @https://cloudflare-dns.com/dns-query initiates a DOH resolver for Cloudflare DoH server. @tls:// eg: @1.1.1.1 initiates a DoT resolver for 1.1.1.1:853. + @sdns:// eg: @sdns://AgcAAAAAAAAABzEuMC4wLjEAEmRucy5jbG91ZGZsYXJlLmNvbQovZG5zLXF1ZXJ5 + initiates a DNSCrypt or DoH resolver using its DNS stamp ``` ### Query Options diff --git a/cmd/doggo/cli/help.go b/cmd/doggo/cli/help.go index a409e1c..c27ccf8 100644 --- a/cmd/doggo/cli/help.go +++ b/cmd/doggo/cli/help.go @@ -36,6 +36,7 @@ var appHelpTextTemplate = `{{ "NAME" | color "" "heading" }}: {{"@tcp://" | color "yellow" ""}} eg: @tcp://1.1.1.1 initiates a {{"TCP" | color "cyan" ""}} resolver for 1.1.1.1:53. {{"@https://" | color "yellow" ""}} eg: @https://cloudflare-dns.com/dns-query initiates a {{"DOH" | color "cyan" ""}} resolver for Cloudflare DoH server. {{"@tls://" | color "yellow" ""}} eg: @tls://1.1.1.1 initiates a {{"DoT" | color "cyan" ""}} resolver for 1.1.1.1:853. + {{"@sdns://" | color "yellow" ""}} initiates a {{"DNSCrypt" | color "cyan" ""}} or {{"DoH" | color "cyan" ""}} resolver using its DNS stamp. {{ "Query Options" | color "" "heading" }}: {{"-q, --query=HOSTNAME" | color "yellow" ""}} Hostname to query the DNS records for (eg {{"mrkaran.dev" | color "cyan" ""}}). diff --git a/completions/doggo.fish b/completions/doggo.fish index d0fa28a..af99acd 100644 --- a/completions/doggo.fish +++ b/completions/doggo.fish @@ -12,7 +12,7 @@ complete -c doggo -s 'n' -l 'nameserver' -d "Address of a specific nameserver to complete -c doggo -s 'c' -l 'class' -d "Network class of the DNS record being queried" -x -a "IN CH HS" # Transport options -complete -c doggo -x -a "@udp:// @tcp:// @https:// @tls://" -d "Select the protocol for resolving queries" +complete -c doggo -x -a "@udp:// @tcp:// @https:// @tls:// @sdns://" -d "Select the protocol for resolving queries" # Resolver options complete -c doggo -l 'ndots' -d "Specify ndots parameter. Takes value from /etc/resolv.conf if using the system namesever or 1 otherwise" diff --git a/go.mod b/go.mod index 103944d..cfe2f65 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,10 @@ module github.com/mr-karan/doggo go 1.16 require ( + github.com/ameshkov/dnscrypt v1.1.0 + github.com/ameshkov/dnsstamps v1.0.3 github.com/fatih/color v1.10.0 github.com/go-chi/chi v1.5.3 - github.com/go-chi/render v1.0.1 github.com/knadh/koanf v0.14.0 github.com/mattn/go-runewidth v0.0.9 // indirect github.com/miekg/dns v1.1.35 diff --git a/go.sum b/go.sum index 8f99064..d609f68 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,14 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= +github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= +github.com/ameshkov/dnscrypt v1.1.0 h1:2vAt5dD6ZmqlAxEAfzRcLBnkvdf8NI46Kn9InSwQbSI= +github.com/ameshkov/dnscrypt v1.1.0/go.mod h1:ikduAxNLCTEfd1AaCgpIA5TgroIVQ8JY3Vb095fiFJg= +github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= +github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= +github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -9,8 +19,6 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-chi/chi v1.5.3 h1:+DVDS9/D3MTbEu3WrrH3oz9oP6PlSPSNj8LLw3X17yU= github.com/go-chi/chi v1.5.3/go.mod h1:Q8xfe6s3fjZyMr8ZTv5jL+vxhVaFyCq2s+RvSfzTD0E= -github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= -github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= @@ -24,6 +32,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs= github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= @@ -45,13 +54,15 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -61,6 +72,7 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/app/nameservers.go b/internal/app/nameservers.go index eaa106c..5f8daae 100644 --- a/internal/app/nameservers.go +++ b/internal/app/nameservers.go @@ -5,6 +5,7 @@ import ( "net" "net/url" + "github.com/ameshkov/dnsstamps" "github.com/mr-karan/doggo/pkg/config" "github.com/mr-karan/doggo/pkg/models" ) @@ -60,6 +61,23 @@ func initNameserver(n string) (models.Nameserver, error) { if err != nil { return ns, err } + if u.Scheme == "sdns" { + stamp, err := dnsstamps.NewServerStampFromString(n) + if err != nil { + return ns, err + } + switch stamp.Proto { + case dnsstamps.StampProtoTypeDoH: + ns.Type = models.DOHResolver + address := url.URL{Scheme: "https", Host: stamp.ProviderName, Path: stamp.Path} + ns.Address = address.String() + case dnsstamps.StampProtoTypeDNSCrypt: + ns.Type = models.DNSCryptResolver + ns.Address = n + default: + return ns, fmt.Errorf("unsupported protocol: %v", stamp.Proto.String()) + } + } if u.Scheme == "https" { ns.Type = models.DOHResolver ns.Address = u.String() diff --git a/pkg/models/models.go b/pkg/models/models.go index a361b79..a3c0852 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -8,11 +8,12 @@ const ( // DefaultUDPPort specifies the default port for a DNS server connecting over UDP DefaultUDPPort = "53" // DefaultTCPPort specifies the default port for a DNS server connecting over TCP - DefaultTCPPort = "53" - UDPResolver = "udp" - DOHResolver = "doh" - TCPResolver = "tcp" - DOTResolver = "dot" + DefaultTCPPort = "53" + UDPResolver = "udp" + DOHResolver = "doh" + TCPResolver = "tcp" + DOTResolver = "dot" + DNSCryptResolver = "dnscrypt" ) // QueryFlags is used store the query params diff --git a/pkg/resolvers/dnscrypt.go b/pkg/resolvers/dnscrypt.go new file mode 100644 index 0000000..c0eb282 --- /dev/null +++ b/pkg/resolvers/dnscrypt.go @@ -0,0 +1,81 @@ +package resolvers + +import ( + "github.com/ameshkov/dnscrypt" + "github.com/miekg/dns" + "github.com/sirupsen/logrus" +) + +// DNSCryptResolver represents the config options for setting up a Resolver. +type DNSCryptResolver struct { + client *dnscrypt.Client + serverInfo *dnscrypt.ServerInfo + server string + resolverOptions Options +} + +// DNSCryptResolverOpts holds options for setting up a DNSCrypt resolver. +type DNSCryptResolverOpts struct { + IPv4Only bool + IPv6Only bool + UseTLS bool + UseTCP bool +} + +// NewDNSCryptResolver accepts a list of nameservers and configures a DNS resolver. +func NewDNSCryptResolver(server string, dnscryptOpts DNSCryptResolverOpts, resolverOpts Options) (Resolver, error) { + net := "udp" + if dnscryptOpts.UseTCP { + net = "tcp" + } + client := &dnscrypt.Client{Proto: net, AdjustPayloadSize: true} + serverInfo, _, err := client.Dial(server) + if err != nil { + return nil, err + } + return &DNSCryptResolver{ + client: client, + serverInfo: serverInfo, + server: server, + resolverOptions: resolverOpts, + }, nil +} + +// Lookup takes a dns.Question and sends them to DNS Server. +// It parses the Response from the server in a custom output format. +func (r *DNSCryptResolver) Lookup(question dns.Question) (Response, error) { + var ( + rsp Response + messages = prepareMessages(question, r.resolverOptions.Ndots, r.resolverOptions.SearchList) + ) + for _, msg := range messages { + r.resolverOptions.Logger.WithFields(logrus.Fields{ + "domain": msg.Question[0].Name, + "ndots": r.resolverOptions.Ndots, + "nameserver": r.serverInfo.ProviderName, + }).Debug("Attempting to resolve") + in, rtt, err := r.client.Exchange(&msg, r.serverInfo) + if err != nil { + return rsp, err + } + // pack questions in output. + for _, q := range msg.Question { + ques := Question{ + Name: q.Name, + Class: dns.ClassToString[q.Qclass], + Type: dns.TypeToString[q.Qtype], + } + rsp.Questions = append(rsp.Questions, ques) + } + // get the authorities and answers. + output := parseMessage(in, rtt, r.server) + rsp.Authorities = output.Authorities + rsp.Answers = output.Answers + + if len(output.Answers) > 0 { + // stop iterating the searchlist. + break + } + } + return rsp, nil +} diff --git a/pkg/resolvers/resolver.go b/pkg/resolvers/resolver.go index 3af8db8..7796b67 100644 --- a/pkg/resolvers/resolver.go +++ b/pkg/resolvers/resolver.go @@ -127,6 +127,17 @@ func LoadResolvers(opts Options) ([]Resolver, error) { } rslvrs = append(rslvrs, rslvr) } + if ns.Type == models.DNSCryptResolver { + opts.Logger.Debug("initiating DNSCrypt resolver") + rslvr, err := NewDNSCryptResolver(ns.Address, + DNSCryptResolverOpts{ + UseTCP: false, + }, resolverOpts) + if err != nil { + return rslvrs, err + } + rslvrs = append(rslvrs, rslvr) + } } return rslvrs, nil }