diff --git a/doh/doh_client.go b/doh/doh_client.go new file mode 100644 index 0000000..081a93a --- /dev/null +++ b/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 +} diff --git a/doh/doh_client_test.go b/doh/doh_client_test.go new file mode 100644 index 0000000..5bac92d --- /dev/null +++ b/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") +} diff --git a/doh/options.go b/doh/options.go new file mode 100644 index 0000000..b740d3f --- /dev/null +++ b/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 +) diff --git a/go.mod b/go.mod index ad8bce5..770eebe 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 8e13e73..a857c2e 100644 --- a/go.sum +++ b/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= @@ -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=