├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── issue-report.md ├── dependabot.yml ├── release.yml └── workflows │ ├── autorelease-tag.yml │ ├── build-test.yml │ ├── codeql-analysis.yml │ ├── dep-auto-merge.yml │ └── lint-test.yml ├── LICENSE.md ├── README.md ├── client.go ├── client_queue.go ├── client_test.go ├── connpool.go ├── doh ├── doh_client.go ├── doh_client_test.go ├── options.go └── util.go ├── go.mod ├── go.sum ├── hostsfile ├── hostfile_test.go ├── hostsfile.go └── tests │ ├── linux_host │ ├── linux_host_special_chars │ ├── linux_host_tabs_spaces_comments │ ├── macos_host │ ├── macos_host_special_chars │ ├── macos_host_tabs_spaces_comments │ ├── win_host │ ├── win_host_special_chars │ └── win_host_tabs_spaces_comments ├── options.go ├── options_test.go ├── resolver.go ├── root.go └── validate.go /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Ask an question / advise on using retryabledns 5 | url: https://github.com/projectdiscovery/retryabledns/discussions/categories/q-a 6 | about: Ask a question or request support for using retryabledns 7 | 8 | - name: Share idea / feature to discuss for retryabledns 9 | url: https://github.com/projectdiscovery/retryabledns/discussions/categories/ideas 10 | about: Share idea / feature to discuss for retryabledns 11 | 12 | - name: Connect with PD Team (Discord) 13 | url: https://discord.gg/projectdiscovery 14 | about: Connect with PD Team for direct communication -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request feature to implement in this project 4 | labels: 'Type: Enhancement' 5 | --- 6 | 7 | 13 | 14 | ### Please describe your feature request: 15 | 16 | 17 | ### Describe the use case of this feature: 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue report 3 | about: Create a report to help us to improve the project 4 | labels: 'Type: Bug' 5 | 6 | --- 7 | 8 | 13 | 14 | 15 | 16 | ### retryabledns version: 17 | 18 | 19 | 20 | 21 | ### Current Behavior: 22 | 23 | 24 | ### Expected Behavior: 25 | 26 | 27 | ### Steps To Reproduce: 28 | 33 | 34 | 35 | ### Anything else: 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Maintain dependencies for go modules 10 | - package-ecosystem: "gomod" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | target-branch: "main" 15 | commit-message: 16 | prefix: "chore" 17 | include: "scope" 18 | labels: 19 | - "Type: Maintenance" 20 | allow: 21 | - dependency-name: "github.com/projectdiscovery/*" 22 | 23 | # # Maintain dependencies for docker 24 | # - package-ecosystem: "docker" 25 | # directory: "/" 26 | # schedule: 27 | # interval: "weekly" 28 | # target-branch: "dev" 29 | # commit-message: 30 | # prefix: "chore" 31 | # include: "scope" 32 | # labels: 33 | # - "Type: Maintenance" 34 | # 35 | # # Maintain dependencies for GitHub Actions 36 | # - package-ecosystem: "github-actions" 37 | # directory: "/" 38 | # schedule: 39 | # interval: "weekly" 40 | # target-branch: "dev" 41 | # commit-message: 42 | # prefix: "chore" 43 | # include: "scope" 44 | # labels: 45 | # - "Type: Maintenance" -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | categories: 6 | - title: 🎉 New Features 7 | labels: 8 | - "Type: Enhancement" 9 | - title: 🐞 Bugs Fixes 10 | labels: 11 | - "Type: Bug" 12 | - title: 🔨 Maintenance 13 | labels: 14 | - "Type: Maintenance" 15 | - title: Other Changes 16 | labels: 17 | - "*" -------------------------------------------------------------------------------- /.github/workflows/autorelease-tag.yml: -------------------------------------------------------------------------------- 1 | name: 🔖 Auto release gh action 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Get Commit Count 18 | id: get_commit 19 | run: git rev-list `git rev-list --tags --no-walk --max-count=1`..HEAD --count | xargs -I {} echo COMMIT_COUNT={} >> $GITHUB_OUTPUT 20 | 21 | - name: Create release and tag 22 | if: ${{ steps.get_commit.outputs.COMMIT_COUNT > 0 }} 23 | id: tag_version 24 | uses: mathieudutour/github-tag-action@v6.1 25 | with: 26 | github_token: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Create a GitHub release 29 | if: ${{ steps.get_commit.outputs.COMMIT_COUNT > 0 }} 30 | uses: actions/create-release@v1 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | tag_name: ${{ steps.tag_version.outputs.new_tag }} 35 | release_name: Release ${{ steps.tag_version.outputs.new_tag }} 36 | body: ${{ steps.tag_version.outputs.changelog }} -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: 🔨 Build Test 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | 8 | jobs: 9 | build: 10 | name: Test Builds 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Set up Go 14 | uses: actions/setup-go@v4 15 | with: 16 | go-version: 1.21.x 17 | 18 | - name: Check out code 19 | uses: actions/checkout@v4 20 | 21 | - name: Test 22 | run: go test ./... 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 🚨 CodeQL Analysis 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | 7 | jobs: 8 | analyze: 9 | name: Analyze 10 | runs-on: ubuntu-latest 11 | permissions: 12 | actions: read 13 | contents: read 14 | security-events: write 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | language: [ 'go' ] 20 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | # Initializes the CodeQL tools for scanning. 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@v2 29 | with: 30 | languages: ${{ matrix.language }} 31 | 32 | - name: Autobuild 33 | uses: github/codeql-action/autobuild@v2 34 | 35 | - name: Perform CodeQL Analysis 36 | uses: github/codeql-action/analyze@v2 -------------------------------------------------------------------------------- /.github/workflows/dep-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: 🤖 dep auto merge 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | permissions: 10 | pull-requests: write 11 | issues: write 12 | repository-projects: write 13 | 14 | jobs: 15 | automerge: 16 | runs-on: ubuntu-latest 17 | if: github.actor == 'dependabot[bot]' 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | token: ${{ secrets.DEPENDABOT_PAT }} 22 | 23 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 24 | with: 25 | github-token: ${{ secrets.DEPENDABOT_PAT }} 26 | target: all -------------------------------------------------------------------------------- /.github/workflows/lint-test.yml: -------------------------------------------------------------------------------- 1 | name: 🙏🏻 Lint Test 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | lint: 9 | name: Lint Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Run golangci-lint 15 | uses: golangci/golangci-lint-action@v3 16 | with: 17 | version: latest 18 | args: --timeout 5m 19 | working-directory: . -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ProjectDiscovery, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Retryable dns resolver 2 | 3 | Based on `miekg/dns` and freely inspired by `bogdanovich/dns_resolver`. 4 | 5 | ## Features 6 | 7 | - Supports both system default DNS resolvers and user-provided ones 8 | - Retries DNS requests in case of I/O errors, timeouts, or network failures 9 | - Allows arbitrary query types 10 | - Resolution with random resolvers 11 | - Compatible with various DNS resolver protocols (TCP, UDP, DoH, and DoT) 12 | 13 | ### Using *go get* 14 | 15 | ```console 16 | $ go get github.com/projectdiscovery/retryabledns 17 | ``` 18 | 19 | After this command *retryabledns* library source will be in your $GOPATH 20 | 21 | ## `/etc/hosts` file processing 22 | 23 | By default, the library processes the `/etc/hosts` file up to a maximum amount of lines for efficiency (4096). If your setup has a larger hosts file and you want to process more lines, you can easily configure this limit by adjusting the `hostsfile.MaxLines` variable. 24 | 25 | For example: 26 | 27 | ``` go 28 | hostsfile.MaxLines = 10000 // Now the library will process up to 10000 lines from the hosts file 29 | ``` 30 | 31 | ## Example 32 | 33 | Usage Example: 34 | 35 | ``` go 36 | package main 37 | 38 | import ( 39 | "log" 40 | 41 | "github.com/projectdiscovery/retryabledns" 42 | "github.com/miekg/dns" 43 | ) 44 | 45 | func main() { 46 | // It requires a list of resolvers. 47 | // Valid protocols are "udp", "tcp", "doh", "dot". Default are "udp". 48 | resolvers := []string{"8.8.8.8:53", "8.8.4.4:53", "tcp:1.1.1.1"} 49 | retries := 2 50 | hostname := "hackerone.com" 51 | 52 | dnsClient, err := retryabledns.New(resolvers, retries) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | 57 | ips, err := dnsClient.Resolve(hostname) 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | 62 | log.Println(ips) 63 | 64 | // Query Types: dns.TypeA, dns.TypeNS, dns.TypeCNAME, dns.TypeSOA, dns.TypePTR, dns.TypeMX, dns.TypeANY 65 | // dns.TypeTXT, dns.TypeAAAA, dns.TypeSRV (from github.com/miekg/dns) 66 | // retryabledns.ErrRetriesExceeded will be returned if a result isn't returned in max retries 67 | dnsResponses, err := dnsClient.Query(hostname, dns.TypeA) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | 72 | log.Println(dnsResponses) 73 | } 74 | ``` 75 | 76 | Credits: 77 | 78 | - `https://github.com/lixiangzhong/dnsutil` 79 | - `https://github.com/rs/dnstrace` 80 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package retryabledns 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/gob" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "math/rand" 11 | "net" 12 | "net/url" 13 | "strings" 14 | "sync" 15 | "sync/atomic" 16 | "time" 17 | 18 | "github.com/miekg/dns" 19 | "github.com/projectdiscovery/retryabledns/doh" 20 | "github.com/projectdiscovery/retryabledns/hostsfile" 21 | iputil "github.com/projectdiscovery/utils/ip" 22 | mapsutil "github.com/projectdiscovery/utils/maps" 23 | sliceutil "github.com/projectdiscovery/utils/slice" 24 | "golang.org/x/net/proxy" 25 | ) 26 | 27 | var ( 28 | // DefaultMaxPerCNAMEFollows is the default number of times a CNAME can be followed within a trace 29 | DefaultMaxPerCNAMEFollows = 32 30 | 31 | // ErrRetriesExceeded is the error returned when the max retries are exceeded 32 | ErrRetriesExceeded = errors.New("could not resolve, max retries exceeded") 33 | ) 34 | 35 | var internalRangeCheckerInstance *internalRangeChecker 36 | 37 | func init() { 38 | var err error 39 | internalRangeCheckerInstance, err = newInternalRangeChecker() 40 | if err != nil { 41 | fmt.Printf("could not initialize range checker: %s\n", err) 42 | } 43 | } 44 | 45 | // Client is a DNS resolver client to resolve hostnames. 46 | type Client struct { 47 | resolvers []Resolver 48 | options Options 49 | serversIndex uint32 50 | TCPFallback bool 51 | udpClient *dns.Client 52 | udpConnPool mapsutil.SyncLockMap[string, *ConnPool] 53 | tcpClient *dns.Client 54 | dohClient *doh.Client 55 | dotClient *dns.Client 56 | udpProxy proxy.Dialer 57 | tcpProxy proxy.Dialer 58 | dotProxy proxy.Dialer 59 | knownHosts map[string][]string 60 | } 61 | 62 | // New creates a new dns client 63 | func New(baseResolvers []string, maxRetries int) (*Client, error) { 64 | return NewWithOptions(Options{BaseResolvers: baseResolvers, MaxRetries: maxRetries}) 65 | } 66 | 67 | // New creates a new dns client with options 68 | func NewWithOptions(options Options) (*Client, error) { 69 | if err := options.Validate(); err != nil { 70 | return nil, err 71 | } 72 | parsedBaseResolvers := parseResolvers(sliceutil.Dedupe(options.BaseResolvers)) 73 | var knownHosts map[string][]string 74 | if options.Hostsfile { 75 | knownHosts, _ = hostsfile.ParseDefault() 76 | } 77 | 78 | if options.MaxPerCNAMEFollows == 0 { 79 | options.MaxPerCNAMEFollows = DefaultMaxPerCNAMEFollows 80 | } 81 | 82 | httpClient := doh.NewHttpClient( 83 | doh.WithTimeout(options.Timeout), 84 | doh.WithInsecureSkipVerify(), 85 | doh.WithProxy(options.Proxy), // no-op if empty 86 | ) 87 | 88 | // If proxy is specified, force TCP for all resolvers 89 | if options.Proxy != "" { 90 | for i, resolver := range parsedBaseResolvers { 91 | if networkResolver, ok := resolver.(*NetworkResolver); ok && networkResolver.Protocol == UDP { 92 | // Convert UDP resolvers to TCP when proxy is specified 93 | parsedBaseResolvers[i] = &NetworkResolver{ 94 | Protocol: TCP, 95 | Host: networkResolver.Host, 96 | Port: networkResolver.Port, 97 | } 98 | } 99 | } 100 | } 101 | 102 | udpDialer := &net.Dialer{LocalAddr: options.GetLocalAddr(UDP)} 103 | tcpDialer := &net.Dialer{LocalAddr: options.GetLocalAddr(TCP)} 104 | dotDialer := &net.Dialer{LocalAddr: options.GetLocalAddr(TCP)} 105 | 106 | udpClient := &dns.Client{ 107 | Net: "", 108 | Timeout: options.Timeout, 109 | Dialer: udpDialer, 110 | } 111 | tcpClient := &dns.Client{ 112 | Net: TCP.String(), 113 | Timeout: options.Timeout, 114 | Dialer: tcpDialer, 115 | } 116 | dohClient := doh.NewWithOptions( 117 | doh.Options{ 118 | HttpClient: httpClient, 119 | }, 120 | ) 121 | dotClient := &dns.Client{ 122 | Net: "tcp-tls", 123 | Timeout: options.Timeout, 124 | Dialer: dotDialer, 125 | } 126 | 127 | client := Client{ 128 | options: options, 129 | resolvers: parsedBaseResolvers, 130 | udpClient: udpClient, 131 | tcpClient: tcpClient, 132 | dohClient: dohClient, 133 | dotClient: dotClient, 134 | knownHosts: knownHosts, 135 | } 136 | 137 | if options.Proxy != "" { 138 | proxyURL, err := url.Parse(options.Proxy) 139 | if err != nil { 140 | return nil, fmt.Errorf("invalid proxy URL: %v", err) 141 | } 142 | proxyDialer, err := proxy.FromURL(proxyURL, udpDialer) 143 | if err != nil { 144 | return nil, fmt.Errorf("error creating proxy dialer: %v", err) 145 | } 146 | tcpProxyDialer, err := proxy.FromURL(proxyURL, tcpDialer) 147 | if err != nil { 148 | return nil, fmt.Errorf("error creating proxy dialer: %v", err) 149 | } 150 | dotProxyDialer, err := proxy.FromURL(proxyURL, dotDialer) 151 | if err != nil { 152 | return nil, fmt.Errorf("error creating proxy dialer: %v", err) 153 | } 154 | 155 | client.udpProxy = proxyDialer 156 | client.tcpProxy = tcpProxyDialer 157 | client.dotProxy = dotProxyDialer 158 | } 159 | 160 | if options.ConnectionPoolThreads > 1 { 161 | client.udpConnPool = mapsutil.SyncLockMap[string, *ConnPool]{ 162 | Map: make(mapsutil.Map[string, *ConnPool]), 163 | } 164 | for _, resolver := range client.resolvers { 165 | resolverHost, resolverPort, err := net.SplitHostPort(resolver.String()) 166 | if err != nil { 167 | return nil, err 168 | } 169 | networkResolver := NetworkResolver{ 170 | Protocol: UDP, 171 | Port: resolverPort, 172 | Host: resolverHost, 173 | } 174 | udpConnPool, err := NewConnPool(networkResolver, options.ConnectionPoolThreads) 175 | if err != nil { 176 | return nil, err 177 | } 178 | _ = client.udpConnPool.Set(resolver.String(), udpConnPool) 179 | } 180 | } 181 | return &client, nil 182 | } 183 | 184 | // ResolveWithSyscall attempts to resolve the host through system calls 185 | func (c *Client) ResolveWithSyscall(host string) (*DNSData, error) { 186 | ips, err := net.LookupIP(host) 187 | if err != nil { 188 | return nil, err 189 | } 190 | var d DNSData 191 | d.Host = host 192 | for _, ip := range ips { 193 | if ipv4 := ip.To4(); ipv4 != nil { 194 | d.A = append(d.A, ip.String()) 195 | } else if ipv6 := ip.To16(); ipv6 != nil { 196 | d.AAAA = append(d.AAAA, ip.String()) 197 | } 198 | } 199 | 200 | return &d, nil 201 | } 202 | 203 | // Resolve is the underlying resolve function that actually resolves a host 204 | // and gets the ip records for that host. 205 | func (c *Client) Resolve(host string) (*DNSData, error) { 206 | return c.QueryMultiple(host, []uint16{dns.TypeA, dns.TypeAAAA}) 207 | } 208 | 209 | // Do sends a provided dns request and return the raw native response 210 | func (c *Client) Do(msg *dns.Msg) (*dns.Msg, error) { 211 | var resp *dns.Msg 212 | var err error 213 | for i := 0; i < c.options.MaxRetries; i++ { 214 | index := atomic.AddUint32(&c.serversIndex, 1) 215 | resolver := c.resolvers[index%uint32(len(c.resolvers))] 216 | 217 | switch r := resolver.(type) { 218 | case *NetworkResolver: 219 | switch r.Protocol { 220 | case TCP: 221 | if c.tcpProxy != nil { 222 | var tcpConn *dns.Conn 223 | tcpConn, err = c.dialWithProxy(c.tcpProxy, "tcp", resolver.String()) 224 | if err != nil { 225 | break 226 | } 227 | defer tcpConn.Close() 228 | resp, _, err = c.tcpClient.ExchangeWithConn(msg, tcpConn) 229 | } else { 230 | resp, _, err = c.tcpClient.Exchange(msg, resolver.String()) 231 | } 232 | case UDP: 233 | if c.options.ConnectionPoolThreads > 1 { 234 | if udpConnPool, ok := c.udpConnPool.Get(resolver.String()); ok { 235 | resp, _, err = udpConnPool.Exchange(context.TODO(), c.udpClient, msg) 236 | } 237 | } else if c.udpProxy != nil { 238 | var udpConn *dns.Conn 239 | udpConn, err = c.dialWithProxy(c.udpProxy, "udp", resolver.String()) 240 | if err != nil { 241 | break 242 | } 243 | defer udpConn.Close() 244 | resp, _, err = c.udpClient.ExchangeWithConn(msg, udpConn) 245 | } else { 246 | resp, _, err = c.udpClient.Exchange(msg, resolver.String()) 247 | } 248 | case DOT: 249 | resp, _, err = c.dotClient.Exchange(msg, resolver.String()) 250 | } 251 | case *DohResolver: 252 | method := doh.MethodPost 253 | if r.Protocol == GET { 254 | method = doh.MethodGet 255 | } 256 | resp, err = c.dohClient.QueryWithDOHMsg(method, doh.Resolver{URL: r.URL}, msg) 257 | } 258 | 259 | if err != nil || resp == nil { 260 | continue 261 | } 262 | 263 | if resp.Rcode != dns.RcodeSuccess { 264 | continue 265 | } 266 | 267 | // In case we get a non empty answer stop retrying 268 | return resp, nil 269 | } 270 | return resp, ErrRetriesExceeded 271 | } 272 | 273 | func (c *Client) dialWithProxy(dialer proxy.Dialer, network, addr string) (*dns.Conn, error) { 274 | conn, err := dialer.Dial(network, addr) 275 | if err != nil { 276 | return nil, err 277 | } 278 | return &dns.Conn{Conn: conn}, nil 279 | } 280 | 281 | // Query sends a provided dns request and return enriched response 282 | func (c *Client) Query(host string, requestType uint16) (*DNSData, error) { 283 | return c.QueryMultiple(host, []uint16{requestType}) 284 | } 285 | 286 | // A helper function 287 | func (c *Client) A(host string) (*DNSData, error) { 288 | return c.QueryMultiple(host, []uint16{dns.TypeA}) 289 | } 290 | 291 | // AAAA helper function 292 | func (c *Client) AAAA(host string) (*DNSData, error) { 293 | return c.QueryMultiple(host, []uint16{dns.TypeAAAA}) 294 | } 295 | 296 | // MX helper function 297 | func (c *Client) MX(host string) (*DNSData, error) { 298 | return c.QueryMultiple(host, []uint16{dns.TypeMX}) 299 | } 300 | 301 | // CNAME helper function 302 | func (c *Client) CNAME(host string) (*DNSData, error) { 303 | return c.QueryMultiple(host, []uint16{dns.TypeCNAME}) 304 | } 305 | 306 | // SOA helper function 307 | func (c *Client) SOA(host string) (*DNSData, error) { 308 | return c.QueryMultiple(host, []uint16{dns.TypeSOA}) 309 | } 310 | 311 | // TXT helper function 312 | func (c *Client) TXT(host string) (*DNSData, error) { 313 | return c.QueryMultiple(host, []uint16{dns.TypeTXT}) 314 | } 315 | 316 | // SRV helper function 317 | func (c *Client) SRV(host string) (*DNSData, error) { 318 | return c.QueryMultiple(host, []uint16{dns.TypeSRV}) 319 | } 320 | 321 | // PTR helper function 322 | func (c *Client) PTR(host string) (*DNSData, error) { 323 | return c.QueryMultiple(host, []uint16{dns.TypePTR}) 324 | } 325 | 326 | // ANY helper function 327 | func (c *Client) ANY(host string) (*DNSData, error) { 328 | return c.QueryMultiple(host, []uint16{dns.TypeANY}) 329 | } 330 | 331 | // NS helper function 332 | func (c *Client) NS(host string) (*DNSData, error) { 333 | return c.QueryMultiple(host, []uint16{dns.TypeNS}) 334 | } 335 | 336 | func (c *Client) AXFR(host string) (*AXFRData, error) { 337 | return c.axfr(host) 338 | } 339 | 340 | // QueryMultiple sends a provided dns request and return the data with a specific resolver 341 | func (c *Client) QueryMultipleWithResolver(host string, requestTypes []uint16, resolver Resolver) (*DNSData, error) { 342 | return c.queryMultiple(host, requestTypes, resolver) 343 | } 344 | 345 | // CAA helper function 346 | func (c *Client) CAA(host string) (*DNSData, error) { 347 | return c.QueryMultiple(host, []uint16{dns.TypeCAA}) 348 | } 349 | 350 | // QueryMultiple sends a provided dns request and return the data 351 | func (c *Client) QueryMultiple(host string, requestTypes []uint16) (*DNSData, error) { 352 | return c.queryMultiple(host, requestTypes, nil) 353 | } 354 | 355 | // QueryMultiple sends a provided dns request and return the data 356 | func (c *Client) queryMultiple(host string, requestTypes []uint16, resolver Resolver) (*DNSData, error) { 357 | var ( 358 | hasResolver bool = resolver != nil 359 | dnsdata DNSData 360 | err error 361 | ) 362 | 363 | // integrate data with known hosts in case 364 | if c.options.Hostsfile { 365 | if ips, ok := c.knownHosts[host]; ok { 366 | for _, ip := range ips { 367 | if iputil.IsIPv4(ip) { 368 | dnsdata.A = append(dnsdata.A, ip) 369 | } else if iputil.IsIPv6(ip) { 370 | dnsdata.AAAA = append(dnsdata.AAAA, ip) 371 | } 372 | } 373 | } 374 | if len(dnsdata.AAAA)+len(dnsdata.A) > 0 { 375 | dnsdata.HostsFile = true 376 | } 377 | } 378 | 379 | msg := &dns.Msg{} 380 | msg.Id = dns.Id() 381 | msg.SetEdns0(4096, false) 382 | 383 | for _, requestType := range requestTypes { 384 | name := dns.Fqdn(host) 385 | msg.Question = make([]dns.Question, 1) 386 | 387 | switch requestType { 388 | case dns.TypeAXFR: 389 | msg.SetAxfr(name) 390 | case dns.TypePTR: // In case of PTR adjust the domain name 391 | var err error 392 | if net.ParseIP(host) != nil { 393 | name, err = dns.ReverseAddr(host) 394 | if err != nil { 395 | return nil, err 396 | } 397 | } 398 | fallthrough 399 | default: 400 | // Enable Extension Mechanisms for DNS for all messages 401 | msg.RecursionDesired = true 402 | question := dns.Question{ 403 | Name: name, 404 | Qtype: requestType, 405 | Qclass: dns.ClassINET, 406 | } 407 | msg.Question[0] = question 408 | } 409 | 410 | var ( 411 | resp *dns.Msg 412 | trResp chan *dns.Envelope 413 | i int 414 | ) 415 | for i = 0; i < c.options.MaxRetries; i++ { 416 | index := atomic.AddUint32(&c.serversIndex, 1) 417 | if !hasResolver { 418 | resolver = c.resolvers[index%uint32(len(c.resolvers))] 419 | } 420 | switch r := resolver.(type) { 421 | case *NetworkResolver: 422 | if requestType == dns.TypeAXFR { 423 | var dnsconn *dns.Conn 424 | switch r.Protocol { 425 | case TCP: 426 | dnsconn, err = c.tcpClient.Dial(resolver.String()) 427 | case UDP: 428 | dnsconn, err = c.udpClient.Dial(resolver.String()) 429 | case DOT: 430 | dnsconn, err = c.dotClient.Dial(resolver.String()) 431 | default: 432 | dnsconn, err = c.tcpClient.Dial(resolver.String()) 433 | } 434 | if err != nil { 435 | break 436 | } 437 | defer dnsconn.Close() 438 | dnsTransfer := &dns.Transfer{Conn: dnsconn} 439 | trResp, err = dnsTransfer.In(msg, resolver.String()) 440 | } else { 441 | switch r.Protocol { 442 | case TCP: 443 | if c.tcpProxy != nil { 444 | var tcpConn *dns.Conn 445 | tcpConn, err = c.dialWithProxy(c.tcpProxy, "tcp", resolver.String()) 446 | if err != nil { 447 | break 448 | } 449 | defer tcpConn.Close() 450 | resp, _, err = c.tcpClient.ExchangeWithConn(msg, tcpConn) 451 | } else { 452 | resp, _, err = c.tcpClient.Exchange(msg, resolver.String()) 453 | } 454 | case UDP: 455 | if c.options.ConnectionPoolThreads > 1 { 456 | if udpConnPool, ok := c.udpConnPool.Get(resolver.String()); ok { 457 | resp, _, err = udpConnPool.Exchange(context.TODO(), c.udpClient, msg) 458 | } 459 | } else { 460 | resp, _, err = c.udpClient.Exchange(msg, resolver.String()) 461 | } 462 | case DOT: 463 | resp, _, err = c.dotClient.Exchange(msg, resolver.String()) 464 | } 465 | } 466 | case *DohResolver: 467 | method := doh.MethodPost 468 | if r.Protocol == GET { 469 | method = doh.MethodGet 470 | } 471 | resp, err = c.dohClient.QueryWithDOHMsg(method, doh.Resolver{URL: r.URL}, msg) 472 | } 473 | 474 | if err != nil || (trResp == nil && resp == nil) { 475 | continue 476 | } 477 | 478 | // https://github.com/projectdiscovery/retryabledns/issues/25 479 | if resp != nil && resp.Truncated && c.TCPFallback { 480 | resp, _, err = c.tcpClient.Exchange(msg, resolver.String()) 481 | if err != nil || resp == nil { 482 | continue 483 | } 484 | } 485 | 486 | switch requestType { 487 | case dns.TypeAXFR: 488 | err = dnsdata.ParseFromEnvelopeChan(trResp) 489 | default: 490 | err = dnsdata.ParseFromMsg(resp) 491 | } 492 | 493 | // Note: this will refer only to the last valid response 494 | // the whole series of responses can be found in the dnsdata.Raw field 495 | dnsdata.RawResp = resp 496 | 497 | // populate anyway basic info 498 | dnsdata.Host = host 499 | switch { 500 | case resp != nil: 501 | dnsdata.StatusCode = dns.RcodeToString[resp.Rcode] 502 | dnsdata.StatusCodeRaw = resp.Rcode 503 | dnsdata.Raw += resp.String() 504 | case trResp != nil: 505 | // pass 506 | } 507 | dnsdata.Timestamp = time.Now() 508 | dnsdata.Resolver = append(dnsdata.Resolver, resolver.String()) 509 | 510 | if err != nil || !dnsdata.contains() { 511 | continue 512 | } 513 | dnsdata.dedupe() 514 | 515 | // stop on success 516 | if resp != nil && resp.Rcode == dns.RcodeSuccess { 517 | break 518 | } 519 | if trResp != nil { 520 | break 521 | } 522 | } 523 | // Finished retry loop at limit, bail out 524 | if i == c.options.MaxRetries && err != nil { 525 | err = errors.Join(ErrRetriesExceeded, err) 526 | break 527 | } 528 | } 529 | 530 | return &dnsdata, err 531 | } 532 | 533 | // QueryParallel sends a provided dns request to multiple resolvers in parallel 534 | func (c *Client) QueryParallel(host string, requestType uint16, resolvers []string) ([]*DNSData, error) { 535 | msg := dns.Msg{} 536 | msg.SetQuestion(dns.CanonicalName(host), requestType) 537 | 538 | var dnsdatas []*DNSData 539 | 540 | var wg sync.WaitGroup 541 | for _, resolver := range resolvers { 542 | var dnsdata DNSData 543 | dnsdatas = append(dnsdatas, &dnsdata) 544 | wg.Add(1) 545 | go func(resolver string, dnsdata *DNSData) { 546 | defer wg.Done() 547 | resp, err := dns.Exchange(msg.Copy(), resolver) 548 | if err != nil { 549 | return 550 | } 551 | err = dnsdata.ParseFromMsg(resp) 552 | if err != nil { 553 | return 554 | } 555 | dnsdata.Host = host 556 | dnsdata.StatusCode = dns.RcodeToString[resp.Rcode] 557 | dnsdata.StatusCodeRaw = resp.Rcode 558 | dnsdata.Timestamp = time.Now() 559 | dnsdata.Resolver = append(dnsdata.Resolver, resolver) 560 | dnsdata.RawResp = resp 561 | dnsdata.Raw = resp.String() 562 | dnsdata.dedupe() 563 | }(resolver, &dnsdata) 564 | } 565 | 566 | wg.Wait() 567 | 568 | return dnsdatas, nil 569 | } 570 | 571 | // Trace the requested domain with the provided query type 572 | func (c *Client) Trace(host string, requestType uint16, maxrecursion int) (*TraceData, error) { 573 | var tracedata TraceData 574 | host = dns.CanonicalName(host) 575 | msg := dns.Msg{} 576 | msg.SetQuestion(host, requestType) 577 | servers := RootDNSServersIPv4 578 | seenNS := make(map[string]struct{}) 579 | seenCName := make(map[string]int) 580 | for i := 1; i < maxrecursion; i++ { 581 | msg.SetQuestion(host, requestType) 582 | dnsdatas, err := c.QueryParallel(host, requestType, servers) 583 | if err != nil { 584 | return nil, err 585 | } 586 | 587 | for _, server := range servers { 588 | seenNS[server] = struct{}{} 589 | } 590 | 591 | if len(dnsdatas) == 0 { 592 | return &tracedata, nil 593 | } 594 | 595 | for _, dnsdata := range dnsdatas { 596 | if dnsdata != nil && len(dnsdata.Resolver) > 0 { 597 | tracedata.DNSData = append(tracedata.DNSData, dnsdata) 598 | } 599 | } 600 | 601 | var newNSResolvers []string 602 | var nextCname string 603 | for _, d := range dnsdatas { 604 | // Add ns records as new resolvers 605 | for _, ns := range d.NS { 606 | ips, err := net.LookupIP(ns) 607 | if err != nil { 608 | continue 609 | } 610 | for _, ip := range ips { 611 | if ip.To4() != nil { 612 | newNSResolvers = append(newNSResolvers, net.JoinHostPort(ip.String(), "53")) 613 | } 614 | } 615 | } 616 | // Follow CNAME - should happen at the final step of the trace 617 | for _, cname := range d.CNAME { 618 | if nextCname == "" { 619 | nextCname = cname 620 | break 621 | } 622 | } 623 | } 624 | newNSResolvers = sliceutil.Dedupe(newNSResolvers) 625 | 626 | // if we have no new resolvers => return 627 | if len(newNSResolvers) == 0 { 628 | break 629 | } 630 | 631 | // Pick a random server 632 | randomServer := newNSResolvers[rand.Intn(len(newNSResolvers))] 633 | // If we pick the same resolver and we are not following any new cname => return 634 | if _, ok := seenNS[randomServer]; ok && nextCname == "" { 635 | break 636 | } 637 | 638 | servers = []string{randomServer} 639 | 640 | // follow cname if any 641 | if nextCname != "" { 642 | seenCName[nextCname]++ 643 | if seenCName[nextCname] > c.options.MaxPerCNAMEFollows { 644 | break 645 | } 646 | host = nextCname 647 | } 648 | } 649 | 650 | return &tracedata, nil 651 | } 652 | 653 | func (c *Client) axfr(host string) (*AXFRData, error) { 654 | // obtain ns servers 655 | dnsData, err := c.NS(host) 656 | if err != nil { 657 | return nil, err 658 | } 659 | // resolve ns servers to ips 660 | var resolvers []Resolver 661 | 662 | for _, ns := range dnsData.NS { 663 | nsData, err := c.A(ns) 664 | if err != nil { 665 | continue 666 | } 667 | for _, a := range nsData.A { 668 | resolvers = append(resolvers, &NetworkResolver{Protocol: TCP, Host: a, Port: "53"}) 669 | } 670 | } 671 | 672 | resolvers = append(resolvers, c.resolvers...) 673 | 674 | var data []*DNSData 675 | // perform zone transfer for each ns 676 | for _, resolver := range resolvers { 677 | nsData, err := c.QueryMultipleWithResolver(host, []uint16{dns.TypeAXFR}, resolver) 678 | if err != nil { 679 | continue 680 | } 681 | data = append(data, nsData) 682 | } 683 | 684 | return &AXFRData{Host: host, DNSData: data}, nil 685 | } 686 | 687 | func (c *Client) Close() { 688 | _ = c.udpConnPool.Iterate(func(_ string, connPool *ConnPool) error { 689 | connPool.Close() 690 | return nil 691 | }) 692 | } 693 | 694 | // DNSData is the data for a DNS request response 695 | type DNSData struct { 696 | Host string `json:"host,omitempty"` 697 | TTL uint32 `json:"ttl,omitempty"` 698 | Resolver []string `json:"resolver,omitempty"` 699 | A []string `json:"a,omitempty"` 700 | AAAA []string `json:"aaaa,omitempty"` 701 | CNAME []string `json:"cname,omitempty"` 702 | MX []string `json:"mx,omitempty"` 703 | PTR []string `json:"ptr,omitempty"` 704 | SOA []SOA `json:"soa,omitempty"` 705 | NS []string `json:"ns,omitempty"` 706 | TXT []string `json:"txt,omitempty"` 707 | SRV []string `json:"srv,omitempty"` 708 | CAA []string `json:"caa,omitempty"` 709 | AllRecords []string `json:"all,omitempty"` 710 | Raw string `json:"raw,omitempty"` 711 | HasInternalIPs bool `json:"has_internal_ips,omitempty"` 712 | InternalIPs []string `json:"internal_ips,omitempty"` 713 | StatusCode string `json:"status_code,omitempty"` 714 | StatusCodeRaw int `json:"status_code_raw,omitempty"` 715 | TraceData *TraceData `json:"trace,omitempty"` 716 | AXFRData *AXFRData `json:"axfr,omitempty"` 717 | RawResp *dns.Msg `json:"raw_resp,omitempty"` 718 | Timestamp time.Time `json:"timestamp,omitempty"` 719 | HostsFile bool `json:"hosts_file,omitempty"` 720 | } 721 | 722 | type SOA struct { 723 | Name string `json:"name,omitempty"` 724 | NS string `json:"ns,omitempty"` 725 | Mbox string `json:"mailbox,omitempty"` 726 | Serial uint32 `json:"serial,omitempty"` 727 | Refresh uint32 `json:"refresh,omitempty"` 728 | Retry uint32 `json:"retry,omitempty"` 729 | Expire uint32 `json:"expire,omitempty"` 730 | Minttl uint32 `json:"minttl,omitempty"` 731 | } 732 | 733 | // CheckInternalIPs when set to true returns if DNS response IPs 734 | // belong to internal IP ranges. 735 | var CheckInternalIPs = false 736 | 737 | func (d *DNSData) ParseFromRR(rrs []dns.RR) error { 738 | for _, record := range rrs { 739 | if d.TTL == 0 && record.Header().Ttl > 0 { 740 | d.TTL = record.Header().Ttl 741 | } 742 | switch recordType := record.(type) { 743 | case *dns.A: 744 | if CheckInternalIPs && internalRangeCheckerInstance != nil && internalRangeCheckerInstance.ContainsIPv4(recordType.A) { 745 | d.HasInternalIPs = true 746 | d.InternalIPs = append(d.InternalIPs, trimChars(recordType.A.String())) 747 | } 748 | d.A = append(d.A, trimChars(recordType.A.String())) 749 | case *dns.NS: 750 | d.NS = append(d.NS, trimChars(recordType.Ns)) 751 | case *dns.CNAME: 752 | d.CNAME = append(d.CNAME, trimChars(recordType.Target)) 753 | case *dns.SOA: 754 | d.SOA = append(d.SOA, SOA{ 755 | Name: trimChars(recordType.Hdr.Name), 756 | NS: trimChars(recordType.Ns), 757 | Mbox: trimChars(recordType.Mbox), 758 | Serial: recordType.Serial, 759 | Refresh: recordType.Refresh, 760 | Retry: recordType.Retry, 761 | Expire: recordType.Expire, 762 | Minttl: recordType.Minttl, 763 | }, 764 | ) 765 | case *dns.PTR: 766 | d.PTR = append(d.PTR, trimChars(recordType.Ptr)) 767 | case *dns.MX: 768 | d.MX = append(d.MX, trimChars(recordType.Mx)) 769 | case *dns.CAA: 770 | d.CAA = append(d.CAA, trimChars(recordType.Value)) 771 | case *dns.TXT: 772 | // Per RFC 7208, a single TXT record can be broken up into multiple parts and "MUST be treated as if those strings are concatenated 773 | // together without adding spaces"; see: https://www.rfc-editor.org/rfc/rfc7208 774 | d.TXT = append(d.TXT, strings.Join(recordType.Txt, "")) 775 | case *dns.SRV: 776 | d.SRV = append(d.SRV, trimChars(recordType.Target)) 777 | case *dns.AAAA: 778 | if CheckInternalIPs && internalRangeCheckerInstance.ContainsIPv6(recordType.AAAA) { 779 | d.HasInternalIPs = true 780 | d.InternalIPs = append(d.InternalIPs, trimChars(recordType.AAAA.String())) 781 | } 782 | d.AAAA = append(d.AAAA, trimChars(recordType.AAAA.String())) 783 | } 784 | d.AllRecords = append(d.AllRecords, record.String()) 785 | } 786 | return nil 787 | } 788 | 789 | // ParseFromMsg and enrich data 790 | func (d *DNSData) ParseFromMsg(msg *dns.Msg) error { 791 | allRecords := append(msg.Answer, msg.Extra...) 792 | allRecords = append(allRecords, msg.Ns...) 793 | return d.ParseFromRR(allRecords) 794 | } 795 | 796 | func (d *DNSData) ParseFromEnvelopeChan(envChan chan *dns.Envelope) error { 797 | var allRecords []dns.RR 798 | for env := range envChan { 799 | if env.Error != nil { 800 | return env.Error 801 | } 802 | allRecords = append(allRecords, env.RR...) 803 | } 804 | return d.ParseFromRR(allRecords) 805 | } 806 | 807 | func (d *DNSData) contains() bool { 808 | return len(d.A) > 0 || len(d.AAAA) > 0 || len(d.CNAME) > 0 || len(d.MX) > 0 || len(d.NS) > 0 || len(d.PTR) > 0 || len(d.TXT) > 0 || len(d.SRV) > 0 || len(d.SOA) > 0 || len(d.CAA) > 0 809 | } 810 | 811 | // JSON returns the object as json string 812 | func (d *DNSData) JSON() (string, error) { 813 | b, err := json.Marshal(&d) 814 | return string(b), err 815 | } 816 | 817 | func trimChars(s string) string { 818 | return strings.TrimRight(s, ".") 819 | } 820 | 821 | func (d *DNSData) dedupe() { 822 | d.Resolver = sliceutil.Dedupe(d.Resolver) 823 | d.A = sliceutil.Dedupe(d.A) 824 | d.AAAA = sliceutil.Dedupe(d.AAAA) 825 | d.CNAME = sliceutil.Dedupe(d.CNAME) 826 | d.MX = sliceutil.Dedupe(d.MX) 827 | d.PTR = sliceutil.Dedupe(d.PTR) 828 | d.NS = sliceutil.Dedupe(d.NS) 829 | d.TXT = sliceutil.Dedupe(d.TXT) 830 | d.SRV = sliceutil.Dedupe(d.SRV) 831 | d.CAA = sliceutil.Dedupe(d.CAA) 832 | d.AllRecords = sliceutil.Dedupe(d.AllRecords) 833 | } 834 | 835 | // Marshal encodes the dnsdata to a binary representation 836 | func (d *DNSData) Marshal() ([]byte, error) { 837 | var b bytes.Buffer 838 | enc := gob.NewEncoder(&b) 839 | err := enc.Encode(d) 840 | if err != nil { 841 | return nil, err 842 | } 843 | return b.Bytes(), nil 844 | } 845 | 846 | // Unmarshal decodes the dnsdata from a binary representation 847 | func (d *DNSData) Unmarshal(b []byte) error { 848 | dec := gob.NewDecoder(bytes.NewBuffer(b)) 849 | return dec.Decode(&d) 850 | } 851 | 852 | // TraceData contains the trace information for a dns query 853 | type TraceData struct { 854 | Host string `json:"host,omitempty"` 855 | DNSData []*DNSData `json:"chain,omitempty"` 856 | } 857 | 858 | type AXFRData struct { 859 | Host string `json:"host,omitempty"` 860 | DNSData []*DNSData `json:"chain,omitempty"` 861 | } 862 | 863 | // GetSOARecords returns the NS and Mbox of all SOA records as a string slice 864 | func (d *DNSData) GetSOARecords() []string { 865 | var soaRecords []string 866 | for _, soa := range d.SOA { 867 | soaRecords = append(soaRecords, soa.NS, soa.Mbox) 868 | } 869 | return soaRecords 870 | } 871 | -------------------------------------------------------------------------------- /client_queue.go: -------------------------------------------------------------------------------- 1 | package retryabledns 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | type waitingClient struct { 10 | returnCh chan *dns.Conn 11 | doneCh <-chan struct{} 12 | arrivalTime time.Time 13 | index int // The index of the item in the heap. 14 | } 15 | 16 | // A clientQueue implements heap.Interface and holds waitingClients. 17 | type clientQueue []*waitingClient 18 | 19 | func (pq clientQueue) Len() int { return len(pq) } 20 | 21 | func (pq clientQueue) Less(i, j int) bool { 22 | return pq[i].arrivalTime.Before(pq[j].arrivalTime) 23 | } 24 | 25 | func (pq clientQueue) Swap(i, j int) { 26 | pq[i], pq[j] = pq[j], pq[i] 27 | pq[i].index = i 28 | pq[j].index = j 29 | } 30 | 31 | func (pq *clientQueue) Push(x any) { 32 | n := len(*pq) 33 | item := x.(*waitingClient) 34 | item.index = n 35 | *pq = append(*pq, item) 36 | } 37 | 38 | func (pq *clientQueue) Pop() any { 39 | old := *pq 40 | n := len(old) 41 | item := old[n-1] 42 | old[n-1] = nil // avoid memory leak 43 | item.index = -1 // for safety 44 | *pq = old[0 : n-1] 45 | return item 46 | } 47 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package retryabledns 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/miekg/dns" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestDialerLocalAddr(t *testing.T) { 12 | /** Works without LocalAddrIP **/ 13 | options := Options{ 14 | BaseResolvers: []string{"1.1.1.1:53", "udp:8.8.8.8"}, 15 | MaxRetries: 3, 16 | } 17 | err := options.Validate() 18 | require.Nil(t, err) 19 | client, _ := NewWithOptions(options) 20 | d, err := client.QueryMultiple("example.com", []uint16{dns.TypeA}) 21 | require.Nil(t, err) 22 | // From current dig result 23 | require.True(t, len(d.A) > 0) 24 | 25 | /** Errors with invalid LocalAddrIP **/ 26 | options = Options{ 27 | BaseResolvers: []string{"1.1.1.1:53", "udp:8.8.8.8"}, 28 | MaxRetries: 3, 29 | } 30 | options.SetLocalAddrIP("1.2.3.4") 31 | err = options.Validate() 32 | require.Nil(t, err) 33 | client, _ = NewWithOptions(options) 34 | _, err = client.QueryMultiple("example.com", []uint16{dns.TypeA}) 35 | require.NotNil(t, err) 36 | 37 | /** Does not error with valid Local IP **/ 38 | // options = Options{ 39 | // BaseResolvers: []string{"1.1.1.1:53", "udp:8.8.8.8"}, 40 | // MaxRetries: 3, 41 | // } 42 | // err = options.SetLocalAddrIPFromNetInterface("en0") 43 | // require.Nil(t, err) 44 | // err = options.Validate() 45 | // require.Nil(t, err) 46 | // client, _ = NewWithOptions(options) 47 | // _, err = client.QueryMultiple("example.com", []uint16{dns.TypeA}) 48 | // require.Nil(t, err) 49 | // // From current dig result 50 | // require.True(t, len(d.A) > 0) 51 | } 52 | 53 | func TestConsistentResolve(t *testing.T) { 54 | client, _ := New([]string{"8.8.8.8:53", "1.1.1.1:53"}, 5) 55 | 56 | var last string 57 | for i := 0; i < 10; i++ { 58 | d, err := client.Resolve("scanme.sh") 59 | require.Nil(t, err, "could not resolve dns") 60 | 61 | if last != "" { 62 | require.Equal(t, last, d.A[0], "got another data from previous") 63 | } else { 64 | last = d.A[0] 65 | } 66 | } 67 | } 68 | 69 | func TestUDP(t *testing.T) { 70 | client, _ := New([]string{"1.1.1.1:53", "udp:8.8.8.8"}, 5) 71 | 72 | d, err := client.QueryMultiple("scanme.sh", []uint16{dns.TypeA}) 73 | require.Nil(t, err) 74 | 75 | // From current dig result 76 | require.True(t, len(d.A) > 0) 77 | } 78 | 79 | func TestTCP(t *testing.T) { 80 | client, _ := New([]string{"tcp:1.1.1.1:53", "tcp:8.8.8.8"}, 5) 81 | 82 | d, err := client.QueryMultiple("scanme.sh", []uint16{dns.TypeA}) 83 | require.Nil(t, err) 84 | 85 | // From current dig result 86 | require.True(t, len(d.A) > 0) 87 | } 88 | 89 | func TestDOH(t *testing.T) { 90 | client, _ := New([]string{"doh:https://doh.opendns.com/dns-query:post", "doh:https://doh.opendns.com/dns-query:get"}, 5) 91 | 92 | d, err := client.QueryMultiple("scanme.sh", []uint16{dns.TypeA}) 93 | require.Nil(t, err) 94 | 95 | // From current dig result 96 | require.True(t, len(d.A) > 0) 97 | } 98 | 99 | func TestDOT(t *testing.T) { 100 | client, _ := New([]string{"dot:dns.google:853", "dot:1dot1dot1dot1.cloudflare-dns.com"}, 5) 101 | 102 | d, err := client.QueryMultiple("scanme.sh", []uint16{dns.TypeA}) 103 | require.Nil(t, err) 104 | 105 | // From current dig result 106 | require.True(t, len(d.A) > 0) 107 | } 108 | 109 | func TestQueryMultiple(t *testing.T) { 110 | client, _ := New([]string{"8.8.8.8:53", "1.1.1.1:53"}, 5) 111 | 112 | // Test various query types 113 | d, err := client.QueryMultiple("scanme.sh", []uint16{ 114 | dns.TypeA, 115 | dns.TypeAAAA, 116 | dns.TypeSOA, 117 | }) 118 | require.Nil(t, err) 119 | 120 | // From current dig result 121 | require.True(t, len(d.A) > 0) 122 | require.True(t, len(d.AAAA) > 0) 123 | require.True(t, len(d.SOA) > 0) 124 | require.NotZero(t, d.TTL) 125 | } 126 | 127 | func TestRetries(t *testing.T) { 128 | client, _ := New([]string{"127.0.0.1"}, 5) 129 | 130 | // Test that error is returned on max retries, should conn refused 5 times then err 131 | _, err := client.QueryMultiple("scanme.sh", []uint16{dns.TypeA}) 132 | require.ErrorIs(t, err, ErrRetriesExceeded) 133 | 134 | msg := &dns.Msg{} 135 | msg.Id = dns.Id() 136 | msg.SetEdns0(4096, false) 137 | msg.Question = make([]dns.Question, 1) 138 | msg.RecursionDesired = true 139 | question := dns.Question{ 140 | Name: "scanme.sh", 141 | Qtype: dns.TypeA, 142 | Qclass: dns.ClassINET, 143 | } 144 | msg.Question[0] = question 145 | 146 | // Test with raw Do() interface as well 147 | _, err = client.Do(msg) 148 | require.True(t, err == ErrRetriesExceeded) 149 | } 150 | 151 | func TestNoRecords(t *testing.T) { 152 | client, err := New([]string{"8.8.8.8:53", "1.1.1.1:53"}, 5) 153 | require.NoError(t, err) 154 | 155 | // Test various query types 156 | res, err := client.QueryMultiple("donotexist.scanme.sh", []uint16{ 157 | dns.TypeA, 158 | dns.TypeAAAA, 159 | }) 160 | require.NoError(t, err) 161 | require.NotNil(t, res) 162 | 163 | assert.Empty(t, res.A) 164 | assert.Empty(t, res.AAAA) 165 | } 166 | 167 | func TestTrace(t *testing.T) { 168 | client, _ := New([]string{"8.8.8.8:53", "1.1.1.1:53"}, 5) 169 | 170 | _, err := client.Trace("www.projectdiscovery.io", dns.TypeA, 100) 171 | require.Nil(t, err, "could not resolve dns") 172 | } 173 | -------------------------------------------------------------------------------- /connpool.go: -------------------------------------------------------------------------------- 1 | package retryabledns 2 | 3 | import ( 4 | "container/heap" 5 | "context" 6 | "fmt" 7 | "net" 8 | "time" 9 | 10 | "github.com/miekg/dns" 11 | ) 12 | 13 | type ConnPool struct { 14 | items map[*dns.Conn]bool 15 | newArrival chan *waitingClient 16 | finished chan *dns.Conn 17 | clients clientQueue 18 | cancel context.CancelFunc 19 | resolver NetworkResolver 20 | } 21 | 22 | func NewConnPool(resolver NetworkResolver, poolSize int) (*ConnPool, error) { 23 | ctx, cancel := context.WithCancel(context.Background()) 24 | pool := &ConnPool{ 25 | items: make(map[*dns.Conn]bool, poolSize), 26 | newArrival: make(chan *waitingClient), 27 | finished: make(chan *dns.Conn), 28 | cancel: cancel, 29 | resolver: resolver, 30 | } 31 | heap.Init(&pool.clients) 32 | for i := 0; i < poolSize; i++ { 33 | conn, err := dns.Dial(resolver.Protocol.String(), resolver.String()) 34 | if err != nil { 35 | return nil, fmt.Errorf("unable to create conn to %s: %w", resolver.String(), err) 36 | } 37 | pool.items[conn] = false 38 | } 39 | go pool.coordinate(ctx) 40 | return pool, nil 41 | } 42 | 43 | func (cp *ConnPool) LocalAddrs() []*net.UDPAddr { 44 | retval := make([]*net.UDPAddr, len(cp.items)) 45 | i := 0 46 | for conn := range cp.items { 47 | retval[i] = conn.LocalAddr().(*net.UDPAddr) 48 | i++ 49 | } 50 | return retval 51 | } 52 | 53 | func (cp *ConnPool) Resolver() NetworkResolver { 54 | return cp.resolver 55 | } 56 | 57 | func (cp *ConnPool) Exchange(ctx context.Context, client *dns.Client, msg *dns.Msg) (r *dns.Msg, rtt time.Duration, err error) { 58 | conn, err := cp.getConnection(ctx) 59 | if err != nil { 60 | return nil, time.Duration(0), err 61 | } 62 | defer cp.releaseConnection(conn) 63 | return client.ExchangeWithConn(msg, conn) 64 | } 65 | 66 | func (cp *ConnPool) Close() { 67 | cp.cancel() 68 | for conn := range cp.items { 69 | conn.Close() 70 | } 71 | } 72 | 73 | func (cp *ConnPool) coordinate(ctx context.Context) { 74 | for { 75 | select { 76 | case <-ctx.Done(): 77 | return 78 | case client := <-cp.newArrival: 79 | heap.Push(&cp.clients, client) 80 | case conn := <-cp.finished: 81 | cp.items[conn] = false 82 | } 83 | for conn, inUse := range cp.items { 84 | if !inUse && len(cp.clients) > 0 { 85 | cp.items[conn] = true 86 | client := heap.Pop(&cp.clients).(*waitingClient) 87 | select { 88 | case client.returnCh <- conn: 89 | case <-client.doneCh: 90 | cp.items[conn] = false 91 | case <-ctx.Done(): 92 | return 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | func (cp *ConnPool) getConnection(ctx context.Context) (*dns.Conn, error) { 100 | client := &waitingClient{ 101 | arrivalTime: time.Now(), 102 | returnCh: make(chan *dns.Conn), 103 | doneCh: ctx.Done(), 104 | } 105 | select { 106 | case cp.newArrival <- client: 107 | case <-ctx.Done(): 108 | return nil, ctx.Err() 109 | } 110 | select { 111 | case conn := <-client.returnCh: 112 | return conn, nil 113 | case <-ctx.Done(): 114 | return nil, ctx.Err() 115 | } 116 | } 117 | 118 | func (cp *ConnPool) releaseConnection(conn *dns.Conn) { 119 | cp.finished <- conn 120 | } 121 | -------------------------------------------------------------------------------- /doh/doh_client.go: -------------------------------------------------------------------------------- 1 | package doh 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "io" 9 | "net/http" 10 | 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | type Client struct { 15 | DefaultResolver Resolver 16 | httpClient *http.Client 17 | } 18 | 19 | func NewWithOptions(options Options) *Client { 20 | return &Client{DefaultResolver: options.DefaultResolver, httpClient: options.HttpClient} 21 | } 22 | 23 | func New() *Client { 24 | httpClient := NewHttpClient( 25 | WithTimeout(DefaultTimeout), 26 | WithInsecureSkipVerify(), 27 | ) 28 | return NewWithOptions(Options{DefaultResolver: Cloudflare, HttpClient: httpClient}) 29 | } 30 | 31 | func (c *Client) Query(name string, question QuestionType) (*Response, error) { 32 | return c.QueryWithResolver(c.DefaultResolver, name, question) 33 | } 34 | 35 | func (c *Client) QueryWithResolver(r Resolver, name string, question QuestionType) (*Response, error) { 36 | return c.QueryWithJsonAPI(r, name, question) 37 | } 38 | 39 | func (c *Client) QueryWithJsonAPI(r Resolver, name string, question QuestionType) (*Response, error) { 40 | req, err := http.NewRequest(http.MethodGet, r.URL, nil) 41 | if err != nil { 42 | return nil, err 43 | } 44 | req.Header.Set("Accept", "application/dns-json") 45 | q := req.URL.Query() 46 | q.Add("name", name) 47 | q.Add("type", question.ToString()) 48 | req.URL.RawQuery = q.Encode() 49 | 50 | resp, err := c.httpClient.Do(req) 51 | if err != nil { 52 | return nil, err 53 | } 54 | defer resp.Body.Close() 55 | 56 | if resp.Body == nil { 57 | return nil, errors.New("empty response body") 58 | } 59 | 60 | var response Response 61 | 62 | err = json.NewDecoder(resp.Body).Decode(&response) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return &response, nil 68 | } 69 | 70 | func (c *Client) QueryWithDOH(method Method, r Resolver, name string, question uint16) (*dns.Msg, error) { 71 | msg := &dns.Msg{} 72 | msg.Id = 0 73 | msg.Question = make([]dns.Question, 1) 74 | msg.Question[0] = dns.Question{ 75 | Name: dns.Fqdn(name), 76 | Qtype: question, 77 | Qclass: dns.ClassINET, 78 | } 79 | return c.QueryWithDOHMsg(method, r, msg) 80 | } 81 | 82 | func (c *Client) QueryWithDOHMsg(method Method, r Resolver, msg *dns.Msg) (*dns.Msg, error) { 83 | packedMsg, err := msg.Pack() 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | var body []byte 89 | var dnsParam string 90 | switch method { 91 | case MethodPost: 92 | dnsParam = "" 93 | body = packedMsg 94 | case MethodGet: 95 | dnsParam = base64.RawURLEncoding.EncodeToString(packedMsg) 96 | body = nil 97 | default: 98 | return nil, errors.New("unsupported method") 99 | } 100 | req, err := http.NewRequest(string(method), r.URL, bytes.NewReader(body)) 101 | if err != nil { 102 | return nil, err 103 | } 104 | req.Header.Set("Accept", "application/dns-message") 105 | if dnsParam != "" { 106 | q := req.URL.Query() 107 | q.Add("dns", dnsParam) 108 | req.URL.RawQuery = q.Encode() 109 | } else if len(body) > 0 { 110 | req.Header.Set("Content-Type", "application/dns-message") 111 | } 112 | 113 | resp, err := c.httpClient.Do(req) 114 | if err != nil { 115 | return nil, err 116 | } 117 | defer resp.Body.Close() 118 | 119 | if resp.Body == nil { 120 | return nil, errors.New("empty response body") 121 | } 122 | 123 | respBodyBytes, err := io.ReadAll(resp.Body) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | respMsg := &dns.Msg{} 129 | if err := respMsg.Unpack(respBodyBytes); err != nil { 130 | return nil, err 131 | } 132 | return respMsg, nil 133 | } 134 | -------------------------------------------------------------------------------- /doh/doh_client_test.go: -------------------------------------------------------------------------------- 1 | package doh 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/miekg/dns" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestConsistentResolve(t *testing.T) { 11 | client := New() 12 | var lastAnswer string 13 | for i := 0; i < 10; i++ { 14 | d, err := client.Query("scanme.sh", A) 15 | require.Nil(t, err, "could not resolve dns") 16 | if lastAnswer == "" { 17 | lastAnswer = d.Answer[0].Data 18 | } else { 19 | require.Equal(t, lastAnswer, d.Answer[0].Data, "got another data from previous") 20 | } 21 | } 22 | } 23 | 24 | func TestResolvers(t *testing.T) { 25 | client := New() 26 | d, err := client.QueryWithDOH(MethodGet, OpenDNS, "www.example.com", dns.TypeA) 27 | require.Nil(t, err, "could not resolve dns") 28 | require.NotNil(t, d, "could not retrieve data") 29 | d, err = client.QueryWithDOH(MethodPost, OpenDNS, "www.example.com", dns.TypeA) 30 | require.Nil(t, err, "could not resolve dns") 31 | require.NotNil(t, d, "could not retrieve data") 32 | } 33 | -------------------------------------------------------------------------------- /doh/options.go: -------------------------------------------------------------------------------- 1 | package doh 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | var DefaultTimeout = 5 * time.Second 10 | 11 | type Options struct { 12 | DefaultResolver Resolver 13 | HttpClient *http.Client 14 | } 15 | 16 | type Resolver struct { 17 | Name string 18 | URL string 19 | } 20 | 21 | var ( 22 | Cloudflare = Resolver{Name: "Cloudflare", URL: "https://cloudflare-dns.com/dns-query"} 23 | Google = Resolver{Name: "Google", URL: "https://dns.google.com/resolve"} 24 | Quad9 = Resolver{Name: "Cloudflare", URL: "https://dns.quad9.net:5053/dns-query"} 25 | PowerDNS = Resolver{Name: "PowerDNS", URL: "https://doh.powerdns.org/dns-query"} 26 | OpenDNS = Resolver{Name: "OpenDNS", URL: "https://doh.opendns.com/dns-query"} 27 | ) 28 | 29 | type QuestionType string 30 | 31 | func (q QuestionType) ToString() string { 32 | return fmt.Sprint(q) 33 | } 34 | 35 | const ( 36 | A QuestionType = "A" 37 | AAAA QuestionType = "AAAA" 38 | MX QuestionType = "MX" 39 | NS QuestionType = "NS" 40 | SOA QuestionType = "SOA" 41 | PTR QuestionType = "PTR" 42 | ANY QuestionType = "ANY" 43 | CNAME QuestionType = "CNAME" 44 | ) 45 | 46 | type Response struct { 47 | Status int `json:"Status"` 48 | TC bool `json:"TC"` 49 | RD bool `json:"RD"` 50 | RA bool `json:"RA"` 51 | AD bool `json:"AD"` 52 | CD bool `json:"CD"` 53 | Question []Question `json:"Question"` 54 | Answer []Answer `json:"Answer"` 55 | Comment string 56 | } 57 | 58 | type Question struct { 59 | Name string `json:"name"` 60 | Type int `json:"type"` 61 | } 62 | type Answer struct { 63 | Name string `json:"name"` 64 | Type int `json:"type"` 65 | TTL int `json:"TTL"` 66 | Data string `json:"data"` 67 | } 68 | 69 | type Method string 70 | 71 | const ( 72 | MethodGet Method = http.MethodGet 73 | MethodPost Method = http.MethodPost 74 | ) 75 | -------------------------------------------------------------------------------- /doh/util.go: -------------------------------------------------------------------------------- 1 | package doh 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | // ClientOption defines a function type for configuring an http.Client 11 | type ClientOption func(*http.Client) 12 | 13 | // WithTimeout sets the timeout for the http.Client 14 | func WithTimeout(timeout time.Duration) ClientOption { 15 | return func(c *http.Client) { 16 | c.Timeout = timeout 17 | } 18 | } 19 | 20 | // WithInsecureSkipVerify sets the InsecureSkipVerify option for the TLS config 21 | func WithInsecureSkipVerify() ClientOption { 22 | return func(c *http.Client) { 23 | transport, ok := c.Transport.(*http.Transport) 24 | if !ok { 25 | transport = &http.Transport{} 26 | c.Transport = transport 27 | } 28 | if transport.TLSClientConfig == nil { 29 | transport.TLSClientConfig = &tls.Config{} 30 | } 31 | transport.TLSClientConfig.InsecureSkipVerify = true 32 | } 33 | } 34 | 35 | // WithProxy sets a proxy for the http.Client 36 | func WithProxy(proxyURL string) ClientOption { 37 | return func(c *http.Client) { 38 | if proxyURL == "" { 39 | return 40 | } 41 | proxyURL, err := url.Parse(proxyURL) 42 | if err != nil { 43 | return 44 | } 45 | 46 | transport, ok := c.Transport.(*http.Transport) 47 | if !ok { 48 | transport = &http.Transport{} 49 | c.Transport = transport 50 | } 51 | 52 | transport.Proxy = http.ProxyURL(proxyURL) 53 | } 54 | } 55 | 56 | // NewHttpClient creates a new http.Client with the given options 57 | func NewHttpClient(opts ...ClientOption) *http.Client { 58 | client := &http.Client{} 59 | for _, opt := range opts { 60 | opt(client) 61 | } 62 | return client 63 | } 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/projectdiscovery/retryabledns 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/miekg/dns v1.1.56 7 | github.com/stretchr/testify v1.9.0 8 | ) 9 | 10 | require ( 11 | github.com/mattn/go-isatty v0.0.20 // indirect 12 | github.com/projectdiscovery/blackrock v0.0.1 // indirect 13 | github.com/tidwall/gjson v1.14.3 // indirect 14 | github.com/tidwall/match v1.1.1 // indirect 15 | github.com/tidwall/pretty v1.2.0 // indirect 16 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect 17 | golang.org/x/sync v0.10.0 // indirect 18 | ) 19 | 20 | require ( 21 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 22 | github.com/aymerick/douceur v0.2.0 // indirect 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/gorilla/css v1.0.1 // indirect 25 | github.com/microcosm-cc/bluemonday v1.0.27 // indirect 26 | github.com/pkg/errors v0.9.1 // indirect 27 | github.com/pmezard/go-difflib v1.0.0 // indirect 28 | github.com/projectdiscovery/utils v0.4.19 29 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect 30 | go.uber.org/multierr v1.11.0 // indirect 31 | golang.org/x/mod v0.17.0 // indirect 32 | golang.org/x/net v0.33.0 33 | golang.org/x/sys v0.28.0 // indirect 34 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= 2 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 3 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 4 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 8 | github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 9 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 10 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 11 | github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 12 | github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 13 | github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= 14 | github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= 15 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 16 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ= 20 | github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss= 21 | github.com/projectdiscovery/utils v0.4.19 h1:rWOOTWUMQK9gvgH01rrw0qFi0hrh712hM1pCUzapCqA= 22 | github.com/projectdiscovery/utils v0.4.19/go.mod h1:y5gnpQn802iEWqf0djTRNskJlS62P5eqe1VS1+ah0tk= 23 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= 24 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= 25 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 26 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 27 | github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= 28 | github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 29 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 30 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 31 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 32 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 33 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 34 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 35 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= 36 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 37 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 38 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 39 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 40 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 41 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 42 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 43 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 45 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 46 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 47 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 48 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 49 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 50 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 51 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 52 | -------------------------------------------------------------------------------- /hostsfile/hostfile_test.go: -------------------------------------------------------------------------------- 1 | package hostsfile 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | // Linux 10 | func TestLinuxParsePassed(t *testing.T) { 11 | elem, err := Parse("./tests/linux_host") 12 | require.NotNil(t, elem, "host file empty") 13 | require.Nil(t, err, "an error was throwed, err var is not nil") 14 | } 15 | 16 | func TestLinuxParseTabSpacesComments(t *testing.T) { 17 | elem, err := Parse("./tests/linux_host_tabs_spaces_comments") 18 | require.NotNil(t, elem, "host file empty") 19 | require.Nil(t, err, "an error was throwed, err var is not nil") 20 | } 21 | 22 | func TestLinuxParseSpecialChars(t *testing.T) { 23 | elem, err := Parse("./tests/linux_host_special_chars") 24 | require.NotNil(t, elem, "host file empty") 25 | require.Nil(t, err, "an error was throwed, err var is not nil") 26 | } 27 | 28 | // Mac 29 | func TestMacosParsePassed(t *testing.T) { 30 | elem, err := Parse("./tests/macos_host") 31 | require.NotNil(t, elem, "host file empty") 32 | require.Nil(t, err, "an error was throwed, err var is not nil") 33 | } 34 | 35 | func TestMacosParseTabSpacesComments(t *testing.T) { 36 | elem, err := Parse("./tests/macos_host_tabs_spaces_comments") 37 | require.NotNil(t, elem, "host file empty") 38 | require.Nil(t, err, "an error was throwed, err var is not nil") 39 | } 40 | 41 | func TestMacosParseSpecialChars(t *testing.T) { 42 | elem, err := Parse("./tests/macos_host_special_chars") 43 | require.NotNil(t, elem, "host file empty") 44 | require.Nil(t, err, "an error was throwed, err var is not nil") 45 | } 46 | 47 | // Windows 48 | func TestWinParsePassed(t *testing.T) { 49 | elem, err := Parse("./tests/win_host") 50 | require.NotNil(t, elem, "host file empty") 51 | require.Nil(t, err, "an error was throwed, err var is not nil") 52 | } 53 | 54 | func TestWindowsParseTabSpacesComments(t *testing.T) { 55 | elem, err := Parse("./tests/win_host_tabs_spaces_comments") 56 | require.NotNil(t, elem, "host file empty") 57 | require.Nil(t, err, "an error was throwed, err var is not nil") 58 | } 59 | 60 | func TestWinParseSpecialChars(t *testing.T) { 61 | elem, err := Parse("./tests/win_host_special_chars") 62 | require.NotNil(t, elem, "host file empty") 63 | require.Nil(t, err, "an error was throwed, err var is not nil") 64 | } 65 | -------------------------------------------------------------------------------- /hostsfile/hostsfile.go: -------------------------------------------------------------------------------- 1 | package hostsfile 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "os" 8 | "runtime" 9 | "strings" 10 | 11 | fileutil "github.com/projectdiscovery/utils/file" 12 | ) 13 | 14 | const ( 15 | localhostName = "localhost" 16 | ) 17 | 18 | var ( 19 | // MaxLines defines the maximum number of lines the Parse function will process from the hosts file. 20 | MaxLines = 4096 21 | ) 22 | 23 | func Path() string { 24 | if isWindows() { 25 | return fmt.Sprintf(`%s\System32\Drivers\etc\hosts`, os.Getenv("SystemRoot")) 26 | } 27 | return "/etc/hosts" 28 | } 29 | 30 | func ParseDefault() (map[string][]string, error) { 31 | return Parse(Path()) 32 | } 33 | 34 | func Parse(p string) (map[string][]string, error) { 35 | if !fileutil.FileExists(p) { 36 | return nil, errors.New("hosts file doesn't exist") 37 | } 38 | 39 | hostsFileCh, err := fileutil.ReadFile(p) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | items := make(map[string][]string) 45 | lineCount := 0 46 | 47 | for line := range hostsFileCh { 48 | lineCount++ 49 | if lineCount > MaxLines { 50 | break 51 | } 52 | 53 | line = strings.TrimSpace(line) 54 | // skip comments and empty lines 55 | if line == "" || strings.HasPrefix(line, "#") { 56 | continue 57 | } 58 | 59 | // discard comment part 60 | if idx := strings.Index(line, "#"); idx > 0 { 61 | line = line[:idx] 62 | } 63 | tokens := strings.Fields(line) 64 | if len(tokens) > 1 { 65 | ip := tokens[0] 66 | for _, hostname := range tokens[1:] { 67 | items[hostname] = append(items[hostname], ip) 68 | } 69 | } 70 | } 71 | 72 | // windows 11 resolves localhost with system dns resolver 73 | if _, ok := items[localhostName]; !ok && isWindows() { 74 | localhostIPs, err := net.LookupHost(localhostName) 75 | if err != nil { 76 | return nil, err 77 | } 78 | items[localhostName] = localhostIPs 79 | } 80 | 81 | return items, nil 82 | } 83 | 84 | func isWindows() bool { 85 | return runtime.GOOS == "windows" 86 | } 87 | -------------------------------------------------------------------------------- /hostsfile/tests/linux_host: -------------------------------------------------------------------------------- 1 | ## 2 | # Host Database 3 | # 4 | # localhost is used to configure the loopback interface 5 | # when the system is booting. Do not change this entry. 6 | ## 7 | 127.0.0.1 localhost 8 | 255.255.255.255 broadcasthost 9 | ::1 localhost -------------------------------------------------------------------------------- /hostsfile/tests/linux_host_special_chars: -------------------------------------------------------------------------------- 1 | ## 2 | # Host Database 3 | # 4 | # localhost is used to configure the loopback interface 5 | # when the system is booting. Do not change this entry. 6 | ## 7 | 127.0.0.1 localhost 8 | 255.255.255.255 broadcasthost 9 | \t\n\n\n\t 10 | 11 | &&&& 12 | $$ 13 | # test comment for unit test 14 | >><> 15 | ::1 localhost -------------------------------------------------------------------------------- /hostsfile/tests/linux_host_tabs_spaces_comments: -------------------------------------------------------------------------------- 1 | ## 2 | # Host Database 3 | # 4 | # localhost is used to configure the loopback interface 5 | # when the system is booting. Do not change this entry. 6 | ## 7 | 127.0.0.1 localhost 8 | 255.255.255.255 broadcasthost 9 | \t\n\n\n\t 10 | 11 | 12 | # test comment for unit test 13 | 14 | ::1 localhost -------------------------------------------------------------------------------- /hostsfile/tests/macos_host: -------------------------------------------------------------------------------- 1 | ## 2 | # Host Database 3 | # 4 | # localhost is used to configure the loopback interface 5 | # when the system is booting. Do not change this entry. 6 | ## 7 | 127.0.0.1 localhost 8 | 255.255.255.255 broadcasthost 9 | ::1 localhost 10 | -------------------------------------------------------------------------------- /hostsfile/tests/macos_host_special_chars: -------------------------------------------------------------------------------- 1 | ## 2 | # Host Database 3 | # 4 | # localhost is used to configure the loopback interface 5 | # when the system is booting. Do not change this entry. 6 | ## 7 | 127.0.0.1 localhost 8 | 255.255.255.255 broadcasthost 9 | \t\n\n\n\t 10 | 11 | &&&& 12 | $$ 13 | # test comment for unit test 14 | >><> 15 | ::1 localhost -------------------------------------------------------------------------------- /hostsfile/tests/macos_host_tabs_spaces_comments: -------------------------------------------------------------------------------- 1 | ## 2 | # Host Database 3 | # 4 | # localhost is used to configure the loopback interface 5 | # when the system is booting. Do not change this entry. 6 | ## 7 | 127.0.0.1 localhost 8 | 255.255.255.255 broadcasthost 9 | 10 | # aaa 11 | \n\n\n\n\n 12 | # test comment for unit test 13 | 14 | ::1 localhost -------------------------------------------------------------------------------- /hostsfile/tests/win_host: -------------------------------------------------------------------------------- 1 | # Copyright (c) 1993-2009 Microsoft Corp. 2 | # 3 | # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. 4 | # 5 | # This file contains the mappings of IP addresses to host names. Each 6 | # entry should be kept on an individual line. The IP address should 7 | # be placed in the first column followed by the corresponding host name. 8 | # The IP address and the host name should be separated by at least one 9 | # space. 10 | # 11 | # Additionally, comments (such as these) may be inserted on individual 12 | # lines or following the machine name denoted by a '#' symbol. 13 | # 14 | # For example: 15 | # 16 | # 102.54.94.97 rhino.acme.com # source server 17 | # 38.25.63.10 x.acme.com # x client host 18 | 19 | # localhost name resolution is handled within DNS itself. 20 | 127.0.0.1 localhost 21 | ::1 localhost -------------------------------------------------------------------------------- /hostsfile/tests/win_host_special_chars: -------------------------------------------------------------------------------- 1 | # Copyright (c) 1993-2009 Microsoft Corp. 2 | # 3 | # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. 4 | # 5 | # This file contains the mappings of IP addresses to host names. Each 6 | # entry should be kept on an individual line. The IP address should 7 | # be placed in the first column followed by the corresponding host name. 8 | # The IP address and the host name should be separated by at least one 9 | # space. 10 | $$$%& 11 | # 12 | # Additionally, comments (such as these) may be inserted on individual 13 | # lines or following the machine name denoted by a '#' symbol. 14 | # 15 | # For example: 16 | # 17 | # 102.54.94.97 rhino.acme.com # source server 18 | # 38.25.63.10 x.acme.com # x client host 19 | 20 | # localhost name resolution is handled within DNS itself. 21 | 22 | 127.0.0.1 localhost 23 | 24 | &%&% 25 | 26 | <<>> 27 | # comment test 28 | \n 29 | \n 30 | \t\n\n 31 | 32 | ::1 localhost -------------------------------------------------------------------------------- /hostsfile/tests/win_host_tabs_spaces_comments: -------------------------------------------------------------------------------- 1 | # Copyright (c) 1993-2009 Microsoft Corp. 2 | # 3 | # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. 4 | # 5 | # This file contains the mappings of IP addresses to host names. Each 6 | # entry should be kept on an individual line. The IP address should 7 | # be placed in the first column followed by the corresponding host name. 8 | # The IP address and the host name should be separated by at least one 9 | # space. 10 | # 11 | # Additionally, comments (such as these) may be inserted on individual 12 | # lines or following the machine name denoted by a '#' symbol. 13 | # 14 | # For example: 15 | # 16 | # 102.54.94.97 rhino.acme.com # source server 17 | # 38.25.63.10 x.acme.com # x client host 18 | 19 | # localhost name resolution is handled within DNS itself. 20 | 21 | 127.0.0.1 localhost 22 | 23 | # comment test 24 | \n 25 | \n 26 | \t\n\n 27 | 28 | ::1 localhost -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package retryabledns 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "time" 8 | ) 9 | 10 | var ( 11 | ErrMaxRetriesZero = errors.New("retries must be at least 1") 12 | ErrResolversEmpty = errors.New("resolvers list must not be empty") 13 | 14 | BaseResolvers = []string{ 15 | "1.1.1.1:53", 16 | "1.0.0.1:53", 17 | "8.8.8.8:53", 18 | "8.8.4.4:53", 19 | } 20 | DefaultOptions = Options{ 21 | BaseResolvers: BaseResolvers, 22 | MaxRetries: 1, 23 | Timeout: 3 * time.Second, 24 | } 25 | ) 26 | 27 | type Options struct { 28 | BaseResolvers []string 29 | MaxRetries int 30 | Timeout time.Duration 31 | Hostsfile bool 32 | LocalAddrIP net.IP 33 | LocalAddrPort uint16 34 | ConnectionPoolThreads int 35 | MaxPerCNAMEFollows int 36 | Proxy string 37 | } 38 | 39 | // Returns a net.Addr of a UDP or TCP type depending on whats required 40 | func (options *Options) GetLocalAddr(proto Protocol) net.Addr { 41 | if options.LocalAddrIP == nil { 42 | return nil 43 | } 44 | ipPort := net.JoinHostPort(options.LocalAddrIP.String(), fmt.Sprint(options.LocalAddrPort)) 45 | var ipAddr net.Addr 46 | switch proto { 47 | case UDP: 48 | ipAddr, _ = net.ResolveUDPAddr("udp", ipPort) 49 | default: 50 | ipAddr, _ = net.ResolveTCPAddr("tcp", ipPort) 51 | } 52 | return ipAddr 53 | } 54 | 55 | // Sets the ip from a string, if invalid sets as nil 56 | func (options *Options) SetLocalAddrIP(ip string) { 57 | // invalid ips are no-ops 58 | options.LocalAddrIP = net.ParseIP(ip) 59 | } 60 | 61 | // Sets the first available IP from a network interface name e.g. eth0 62 | func (options *Options) SetLocalAddrIPFromNetInterface(ifaceName string) error { 63 | iface, err := net.InterfaceByName(ifaceName) 64 | if err != nil { 65 | return err 66 | } 67 | addrs, err := iface.Addrs() 68 | if err != nil { 69 | return err 70 | } 71 | for _, addr := range addrs { 72 | ipnetAddr, ok := addr.(*net.IPNet) 73 | if !ok { 74 | continue 75 | } 76 | options.LocalAddrIP = ipnetAddr.IP 77 | return nil 78 | } 79 | return errors.New("no ip address found for interface") 80 | } 81 | 82 | func (options *Options) Validate() error { 83 | if options.MaxRetries == 0 { 84 | return ErrMaxRetriesZero 85 | } 86 | 87 | if len(options.BaseResolvers) == 0 { 88 | return ErrResolversEmpty 89 | } 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package retryabledns 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | stringsutil "github.com/projectdiscovery/utils/strings" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestSetLocalAddrIPFromNetInterface(t *testing.T) { 12 | options := Options{ 13 | MaxRetries: 0, 14 | } 15 | // search loopback interface 16 | interfaces, err := net.Interfaces() 17 | require.Nil(t, err) 18 | for _, iface := range interfaces { 19 | if iface.Flags&net.FlagLoopback != 0 { 20 | err := options.SetLocalAddrIPFromNetInterface(iface.Name) 21 | require.Nil(t, err) 22 | require.NotNil(t, options.LocalAddrIP) 23 | require.True(t, stringsutil.EqualFoldAny(options.LocalAddrIP.String(), "127.0.0.1", "::1")) 24 | } 25 | } 26 | 27 | // Should error with invalid interface name 28 | err = options.SetLocalAddrIPFromNetInterface("lo1234") 29 | require.NotNil(t, err) 30 | } 31 | 32 | func TestValidateOptions(t *testing.T) { 33 | t.Run("empty options", func(t *testing.T) { 34 | options := Options{} 35 | err := options.Validate() 36 | require.NotNil(t, err) 37 | }) 38 | 39 | t.Run("max retries errors with zero", func(t *testing.T) { 40 | options := Options{ 41 | MaxRetries: 0, 42 | } 43 | err := options.Validate() 44 | require.ErrorIs(t, err, ErrMaxRetriesZero) 45 | }) 46 | 47 | t.Run("base resolvers errors if empty", func(t *testing.T) { 48 | options := Options{ 49 | MaxRetries: 1, 50 | BaseResolvers: []string{}, 51 | } 52 | err := options.Validate() 53 | require.ErrorIs(t, err, ErrResolversEmpty) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /resolver.go: -------------------------------------------------------------------------------- 1 | package retryabledns 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | 7 | stringsutil "github.com/projectdiscovery/utils/strings" 8 | ) 9 | 10 | type Protocol string 11 | 12 | const ( 13 | UDP Protocol = "udp" 14 | TCP Protocol = "tcp" 15 | DOH Protocol = "doh" 16 | DOT Protocol = "dot" 17 | ) 18 | 19 | func (p Protocol) String() string { 20 | return string(p) 21 | } 22 | 23 | func (p Protocol) StringWithSemicolon() string { 24 | return p.String() + ":" 25 | } 26 | 27 | type DohProtocol string 28 | 29 | const ( 30 | JsonAPI DohProtocol = "jsonapi" 31 | GET DohProtocol = "get" 32 | POST DohProtocol = "post" 33 | ) 34 | 35 | func (p DohProtocol) String() string { 36 | return string(p) 37 | } 38 | 39 | func (p DohProtocol) StringWithSemicolon() string { 40 | return ":" + p.String() 41 | } 42 | 43 | type Resolver interface { 44 | String() string 45 | } 46 | 47 | type NetworkResolver struct { 48 | Protocol Protocol 49 | Host string 50 | Port string 51 | } 52 | 53 | func (r NetworkResolver) String() string { 54 | return net.JoinHostPort(r.Host, r.Port) 55 | } 56 | 57 | type DohResolver struct { 58 | Protocol DohProtocol 59 | URL string 60 | } 61 | 62 | func (r DohResolver) Method() string { 63 | if r.Protocol == POST { 64 | return POST.String() 65 | } 66 | 67 | return GET.String() 68 | } 69 | 70 | func (r DohResolver) String() string { 71 | return r.URL 72 | } 73 | 74 | func parseResolver(r string) (resolver Resolver) { 75 | rNetworkTokens := trimProtocol(r) 76 | protocol := UDP 77 | 78 | if len(r) >= 4 && r[3] == 58 { // 58 is ":" 79 | switch r[0:3] { 80 | case "udp": 81 | case "tcp": 82 | protocol = TCP 83 | case "dot": 84 | protocol = DOT 85 | case "doh": 86 | protocol = DOH 87 | isJsonApi, isGet := hasDohProtocol(r, JsonAPI.StringWithSemicolon()), hasDohProtocol(r, GET.StringWithSemicolon()) 88 | URL := trimDohProtocol(rNetworkTokens) 89 | dohResolver := &DohResolver{URL: URL, Protocol: POST} 90 | if isJsonApi { 91 | dohResolver.Protocol = JsonAPI 92 | } else if isGet { 93 | dohResolver.Protocol = GET 94 | } 95 | resolver = dohResolver 96 | default: 97 | // unsupported protocol? 98 | } 99 | } 100 | 101 | if protocol != DOH { 102 | networkResolver := &NetworkResolver{Protocol: protocol} 103 | parseHostPort(networkResolver, rNetworkTokens) 104 | resolver = networkResolver 105 | } 106 | 107 | return 108 | } 109 | 110 | func parseHostPort(networkResolver *NetworkResolver, r string) { 111 | if host, port, err := net.SplitHostPort(r); err == nil { 112 | networkResolver.Host = host 113 | networkResolver.Port = port 114 | } else { 115 | networkResolver.Host = r 116 | if networkResolver.Protocol == DOT { 117 | networkResolver.Port = "853" 118 | } else { 119 | networkResolver.Port = "53" 120 | } 121 | } 122 | } 123 | 124 | func hasDohProtocol(resolver, protocol string) bool { 125 | return strings.HasSuffix(resolver, protocol) 126 | } 127 | 128 | func trimProtocol(resolver string) string { 129 | return stringsutil.TrimPrefixAny(resolver, TCP.StringWithSemicolon(), UDP.StringWithSemicolon(), DOH.StringWithSemicolon(), DOT.StringWithSemicolon()) 130 | } 131 | 132 | func trimDohProtocol(resolver string) string { 133 | return stringsutil.TrimSuffixAny(resolver, GET.StringWithSemicolon(), POST.StringWithSemicolon(), JsonAPI.StringWithSemicolon()) 134 | } 135 | 136 | func parseResolvers(resolvers []string) []Resolver { 137 | var parsedResolvers []Resolver 138 | for _, resolver := range resolvers { 139 | parsedResolvers = append(parsedResolvers, parseResolver(resolver)) 140 | } 141 | return parsedResolvers 142 | } 143 | -------------------------------------------------------------------------------- /root.go: -------------------------------------------------------------------------------- 1 | package retryabledns 2 | 3 | type RootDNS struct { 4 | Host string 5 | IPv4 string 6 | IPv6 string 7 | Operator string 8 | } 9 | 10 | // https://www.iana.org/domains/root/servers 11 | 12 | var RootDNSServers = []RootDNS{ 13 | {"a.root-servers.net", "198.41.0.4", "2001:503:ba3e::2:30", "Verisign, Inc"}, 14 | {"b.root-servers.net", "199.9.14.201", "2001:500:200::b", "University of Southern California, Information Sciences Institute"}, 15 | {"c.root-servers.net", "192.33.4.12", "2001:500:2::c", "Cogent Communications"}, 16 | {"d.root-servers.net", "199.7.91.13", "2001:500:2d::d", "University of Maryland"}, 17 | {"e.root-servers.net", "192.203.230.10", "2001:500:a8::e", "NASA (Ames Research Center)"}, 18 | {"f.root-servers.net", "192.5.5.241", "2001:500:2f::f", "Internet Systems Consortium, Inc."}, 19 | {"g.root-servers.net", "192.112.36.4", "2001:500:12::d0d", "US Department of Defense (NIC)"}, 20 | {"h.root-servers.net", "198.97.190.53", "2001:500:1::53", "US Army (Research Lab)"}, 21 | {"i.root-servers.net", "192.36.148.17", "2001:7fe::53", "Netnod"}, 22 | {"j.root-servers.net", "192.58.128.30", "2001:503:c27::2:30", "Verisign, Inc"}, 23 | {"k.root-servers.net", "193.0.14.129", "2001:7fd::1", "RIPE NCC"}, 24 | {"l.root-servers.net", "199.7.83.42", "2001:500:9f::42", "ICANN"}, 25 | {"m.root-servers.net", "202.12.27.33", "2001:dc3::35", "WIDE Project"}, 26 | } 27 | 28 | var RootDNSServersIPv4 = []string{ 29 | "198.41.0.4:53", "199.9.14.201:53", "192.33.4.12:53", "199.7.91.13:53", 30 | "192.203.230.10:53", "192.5.5.241:53", "192.112.36.4:53", "198.97.190.53:53", 31 | "192.36.148.17:53", "192.58.128.30:53", "193.0.14.129:53", "199.7.83.42:53", 32 | "202.12.27.33:53", 33 | } 34 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package retryabledns 2 | 3 | import "net" 4 | 5 | // ipv4InternalRanges contains the IP ranges internal in IPv4 range. 6 | var ipv4InternalRanges = []string{ 7 | "0.0.0.0/8", // Current network (only valid as source address) 8 | "10.0.0.0/8", // Private network 9 | "100.64.0.0/10", // Shared Address Space 10 | "127.0.0.0/8", // Loopback 11 | "169.254.0.0/16", // Link-local (Also many cloud providers Metadata endpoint) 12 | "172.16.0.0/12", // Private network 13 | "192.0.0.0/24", // IETF Protocol Assignments 14 | "192.0.2.0/24", // TEST-NET-1, documentation and examples 15 | "192.88.99.0/24", // IPv6 to IPv4 relay (includes 2002::/16) 16 | "192.168.0.0/16", // Private network 17 | "198.18.0.0/15", // Network benchmark tests 18 | "198.51.100.0/24", // TEST-NET-2, documentation and examples 19 | "203.0.113.0/24", // TEST-NET-3, documentation and examples 20 | "224.0.0.0/4", // IP multicast (former Class D network) 21 | "240.0.0.0/4", // Reserved (former Class E network) 22 | } 23 | 24 | // ipv6InternalRanges contains the IP ranges internal in IPv6 range. 25 | var ipv6InternalRanges = []string{ 26 | "::1/128", // Loopback 27 | "64:ff9b::/96", // IPv4/IPv6 translation (RFC 6052) 28 | "100::/64", // Discard prefix (RFC 6666) 29 | "2001::/32", // Teredo tunneling 30 | "2001:10::/28", // Deprecated (previously ORCHID) 31 | "2001:20::/28", // ORCHIDv2 32 | "2001:db8::/32", // Addresses used in documentation and example source code 33 | "2002::/16", // 6to4 34 | "fc00::/7", // Unique local address 35 | "fe80::/10", // Link-local address 36 | "ff00::/8", // Multicast 37 | } 38 | 39 | // internalRangeChecker contains a list of internal IP ranges. 40 | type internalRangeChecker struct { 41 | ipv4 []*net.IPNet 42 | ipv6 []*net.IPNet 43 | } 44 | 45 | // newInternalRangeChecker creates a structure for checking if a host is from 46 | // a internal IP range whether its ipv4 or ipv6. 47 | func newInternalRangeChecker() (*internalRangeChecker, error) { 48 | rangeChecker := internalRangeChecker{} 49 | 50 | err := rangeChecker.appendIPv4Ranges(ipv4InternalRanges) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | err = rangeChecker.appendIPv6Ranges(ipv6InternalRanges) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return &rangeChecker, nil 60 | } 61 | 62 | // appendIPv4Ranges adds a list of IPv4 Ranges to the list. 63 | func (r *internalRangeChecker) appendIPv4Ranges(ranges []string) error { 64 | for _, ip := range ranges { 65 | _, rangeNet, err := net.ParseCIDR(ip) 66 | if err != nil { 67 | return err 68 | } 69 | r.ipv4 = append(r.ipv4, rangeNet) 70 | } 71 | return nil 72 | } 73 | 74 | // appendIPv6Ranges adds a list of IPv6 Ranges to the list. 75 | func (r *internalRangeChecker) appendIPv6Ranges(ranges []string) error { 76 | for _, ip := range ranges { 77 | _, rangeNet, err := net.ParseCIDR(ip) 78 | if err != nil { 79 | return err 80 | } 81 | r.ipv6 = append(r.ipv6, rangeNet) 82 | } 83 | return nil 84 | } 85 | 86 | // ContainsIPv4 checks whether a given IP address exists in the internal IPv4 ranges. 87 | func (r *internalRangeChecker) ContainsIPv4(IP net.IP) bool { 88 | for _, net := range r.ipv4 { 89 | if net.Contains(IP) { 90 | return true 91 | } 92 | } 93 | return false 94 | } 95 | 96 | // ContainsIPv6 checks whether a given IP address exists in the internal IPv6 ranges. 97 | func (r *internalRangeChecker) ContainsIPv6(IP net.IP) bool { 98 | for _, net := range r.ipv6 { 99 | if net.Contains(IP) { 100 | return true 101 | } 102 | } 103 | return false 104 | } 105 | --------------------------------------------------------------------------------