Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for DOH (json web api + RFC8484) #36

Merged
merged 2 commits into from Aug 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
130 changes: 130 additions & 0 deletions doh/doh_client.go
@@ -0,0 +1,130 @@
package doh

import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"io/ioutil"
"net/http"

"github.com/miekg/dns"
"github.com/projectdiscovery/retryablehttp-go"
)

type Client struct {
DefaultResolver Resolver
httpClient *retryablehttp.Client
}

func NewWithOptions(options Options) *Client {
return &Client{DefaultResolver: options.DefaultResolver, httpClient: options.httpClient}
}

func New() *Client {
return NewWithOptions(Options{DefaultResolver: Cloudflare, httpClient: retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)})
}

func (c *Client) Query(name string, question QuestionType) (*Response, error) {
return c.QueryWithResolver(c.DefaultResolver, name, question)
}

func (c *Client) QueryWithResolver(r Resolver, name string, question QuestionType) (*Response, error) {
return c.QueryWithJsonAPI(r, name, question)
}

func (c *Client) QueryWithJsonAPI(r Resolver, name string, question QuestionType) (*Response, error) {
req, err := retryablehttp.NewRequest(http.MethodGet, r.URL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/dns-json")
q := req.URL.Query()
q.Add("name", name)
q.Add("type", question.ToString())
req.URL.RawQuery = q.Encode()

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.Body == nil {
return nil, errors.New("empty response body")
}

var response Response

err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return nil, err
}

return &response, nil
}

func (c *Client) QueryWithDOH(method Method, r Resolver, name string, question uint16) (*dns.Msg, error) {
msg := &dns.Msg{}
msg.Id = 0
msg.Question = make([]dns.Question, 1)
msg.Question[0] = dns.Question{
Name: dns.Fqdn(name),
Qtype: question,
Qclass: dns.ClassINET,
}
return c.QueryWithDOHMsg(method, r, msg)
}

func (c *Client) QueryWithDOHMsg(method Method, r Resolver, msg *dns.Msg) (*dns.Msg, error) {
packedMsg, err := msg.Pack()
if err != nil {
return nil, err
}

var body []byte
var dnsParam string
switch method {
case MethodPost:
dnsParam = ""
body = packedMsg
case MethodGet:
dnsParam = base64.RawURLEncoding.EncodeToString(packedMsg)
body = nil
default:
return nil, errors.New("unsupported method")
}
req, err := retryablehttp.NewRequest(string(method), r.URL, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/dns-message")
if dnsParam != "" {
q := req.URL.Query()
q.Add("dns", dnsParam)
req.URL.RawQuery = q.Encode()
} else if len(body) > 0 {
req.Header.Set("Content-Type", "application/dns-message")
}

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.Body == nil {
return nil, errors.New("empty response body")
}

respBodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

respMsg := &dns.Msg{}
if err := respMsg.Unpack(respBodyBytes); err != nil {
return nil, err
}
return respMsg, nil
}
32 changes: 32 additions & 0 deletions doh/doh_client_test.go
@@ -0,0 +1,32 @@
package doh

import (
"testing"

"github.com/miekg/dns"
"github.com/stretchr/testify/require"
)

func TestConsistentResolve(t *testing.T) {
client := New()
var lastAnswer string
for i := 0; i < 10; i++ {
d, err := client.Query("example.com", A)
require.Nil(t, err, "could not resolve dns")
if lastAnswer == "" {
lastAnswer = d.Answer[0].Data
} else {
require.Equal(t, lastAnswer, d.Answer[0].Data, "got another data from previous")
}
}
}

func TestResolvers(t *testing.T) {
client := New()
d, err := client.QueryWithDOH(MethodGet, OpenDNS, "www.example.com", dns.TypeA)
require.Nil(t, err, "could not resolve dns")
require.NotNil(t, d, "could not retrieve data")
d, err = client.QueryWithDOH(MethodPost, OpenDNS, "www.example.com", dns.TypeA)
require.Nil(t, err, "could not resolve dns")
require.NotNil(t, d, "could not retrieve data")
}
72 changes: 72 additions & 0 deletions doh/options.go
@@ -0,0 +1,72 @@
package doh

import (
"fmt"
"net/http"

retryablehttp "github.com/projectdiscovery/retryablehttp-go"
)

type Options struct {
DefaultResolver Resolver
httpClient *retryablehttp.Client
}

type Resolver struct {
Name string
URL string
}

var (
Cloudflare = Resolver{Name: "Cloudflare", URL: "https://cloudflare-dns.com/dns-query"}
Google = Resolver{Name: "Google", URL: "https://dns.google.com/resolve"}
Quad9 = Resolver{Name: "Cloudflare", URL: "https://dns.quad9.net:5053/dns-query"}
PowerDNS = Resolver{Name: "PowerDNS", URL: "https://doh.powerdns.org/dns-query"}
OpenDNS = Resolver{Name: "OpenDNS", URL: "https://doh.opendns.com/dns-query"}
)

type QuestionType string

func (q QuestionType) ToString() string {
return fmt.Sprint(q)
}

const (
A QuestionType = "A"
AAAA QuestionType = "AAAA"
MX QuestionType = "MX"
NS QuestionType = "NS"
SOA QuestionType = "SOA"
PTR QuestionType = "PTR"
CNAME QuestionType = "CNAME"
)

type Response struct {
Status int `json:"Status"`
TC bool `json:"TC"`
RD bool `json:"RD"`
RA bool `json:"RA"`
AD bool `json:"AD"`
CD bool `json:"CD"`
Question []Question `json:"Question"`
Answer []Answer `json:"Answer"`
Comment string
}

type Question struct {
Name string `json:"name"`
Type int `json:"type"`
}
type Answer struct {
Name string `json:"name"`
Type int `json:"type"`
TTL int `json:"TTL"`
Data string `json:"data"`
}

type Method string

const (
MethodGet Method = http.MethodGet
MethodPost Method = http.MethodPost
)
2 changes: 2 additions & 0 deletions go.mod
Expand Up @@ -3,6 +3,8 @@ module github.com/projectdiscovery/retryabledns
go 1.14

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/miekg/dns v1.1.29
github.com/projectdiscovery/retryablehttp-go v1.0.2
github.com/stretchr/testify v1.7.0
)
16 changes: 13 additions & 3 deletions go.sum
@@ -1,9 +1,12 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
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=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/projectdiscovery/retryablehttp-go v1.0.2 h1:LV1/KAQU+yeWhNVlvveaYFsjBYRwXlNEq0PvrezMV0U=
github.com/projectdiscovery/retryablehttp-go v1.0.2/go.mod h1:dx//aY9V247qHdsRf0vdWHTBZuBQ2vm6Dq5dagxrDYI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand All @@ -13,15 +16,22 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
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-20210521195947-fe42d452be8f h1:Si4U+UcgJzya9kpiEUJKQvjr512OLli+gL4poHrz93U=
golang.org/x/net v0.0.0-20210521195947-fe42d452be8f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down