├── .gitattributes ├── core ├── domain.go ├── sources.go ├── source.go ├── type.go ├── response.go └── sources │ ├── quad9_test.go │ ├── google_test.go │ ├── cloudflare_test.go │ ├── multi_test.go │ ├── quad9.go │ ├── google.go │ └── cloudflare.go ├── LICENSE ├── README.md └── main.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /core/domain.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // Domain is an alias for a string. 4 | type Domain = string 5 | -------------------------------------------------------------------------------- /core/sources.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // Sources is a type alias for a slice of Source types. 4 | type Sources = []Source 5 | -------------------------------------------------------------------------------- /core/source.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "context" 4 | 5 | // Source defines the minimal interface for a DoH resolver. 6 | type Source interface { 7 | Query(context.Context, Domain, Type) (*Response, error) 8 | String() string 9 | } 10 | -------------------------------------------------------------------------------- /core/type.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // Type is an alias for a string. 4 | type Type = string 5 | 6 | var ( 7 | // IPv4Type for Query 8 | IPv4Type = Type("A") 9 | 10 | // IPv6Type for Query 11 | IPv6Type = Type("AAAA") 12 | 13 | // MailType for Query 14 | MailType = Type("MX") 15 | 16 | // AnyType for Query 17 | AnyType = Type("ANY") 18 | ) 19 | -------------------------------------------------------------------------------- /core/response.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // Response is a record returned from a DoH server. 4 | type Response struct { 5 | Status int `json:"Status"` 6 | TC bool `json:"TC"` 7 | RD bool `json:"RD"` 8 | RA bool `json:"RA"` 9 | AD bool `json:"AD"` 10 | CD bool `json:"CD"` 11 | Question []struct { 12 | Name string `json:"name"` 13 | Type int `json:"type"` 14 | } `json:"Question"` 15 | Answer []struct { 16 | Name string `json:"name"` 17 | Type int `json:"type"` 18 | TTL int `json:"TTL"` 19 | Data string `json:"data"` 20 | } `json:"Answer"` 21 | } 22 | -------------------------------------------------------------------------------- /core/sources/quad9_test.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestQuad9Query(t *testing.T) { 11 | var ( 12 | queryName = "yahoo.com" 13 | queryType = "A" 14 | ) 15 | 16 | src := &Quad9{} 17 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 18 | defer cancel() 19 | 20 | resp, err := src.Query(ctx, queryName, queryType) 21 | 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | if len(resp.Answer) == 0 { 27 | t.Error("got no answer for known domain") 28 | } 29 | 30 | fmt.Println(src, resp, err, ctx.Err()) 31 | } 32 | -------------------------------------------------------------------------------- /core/sources/google_test.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestGoogleQuery(t *testing.T) { 11 | var ( 12 | queryName = "yahoo.com" 13 | queryType = "A" 14 | ) 15 | 16 | src := &Google{} 17 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 18 | defer cancel() 19 | 20 | resp, err := src.Query(ctx, queryName, queryType) 21 | 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | if len(resp.Answer) == 0 { 27 | t.Error("got no answer for known domain") 28 | } 29 | 30 | fmt.Println(src, resp, err, ctx.Err()) 31 | } 32 | -------------------------------------------------------------------------------- /core/sources/cloudflare_test.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestCloudflareQuery(t *testing.T) { 11 | var ( 12 | queryName = "yahoo.com" 13 | queryType = "A" 14 | ) 15 | 16 | src := &Cloudflare{} 17 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 18 | defer cancel() 19 | 20 | resp, err := src.Query(ctx, queryName, queryType) 21 | 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if len(resp.Answer) == 0 { 27 | t.Error("got no answer for known domain") 28 | } 29 | 30 | fmt.Println(src, resp, err, ctx.Err()) 31 | } 32 | -------------------------------------------------------------------------------- /core/sources/multi_test.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | doh "github.com/picatz/doh/core" 11 | ) 12 | 13 | func TestMulti(t *testing.T) { 14 | var ( 15 | queryName = "yahoo.com" 16 | queryType = doh.IPv4Type 17 | ) 18 | 19 | var ( 20 | google = &Google{} 21 | quad9 = &Quad9{} 22 | cloudflare = &Cloudflare{} 23 | ) 24 | 25 | srcs := doh.Sources{google, quad9, cloudflare} 26 | 27 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 28 | defer cancel() 29 | 30 | wg := sync.WaitGroup{} 31 | 32 | wg.Add(len(srcs)) 33 | 34 | for _, src := range srcs { 35 | go func(src doh.Source) { 36 | defer wg.Done() 37 | resp, err := src.Query(ctx, queryName, queryType) 38 | 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | 43 | if len(resp.Answer) == 0 { 44 | t.Error("got no answer for known domain") 45 | } 46 | 47 | fmt.Println(src, resp, err, ctx.Err()) 48 | }(src) 49 | } 50 | 51 | wg.Wait() 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kent 'picat' Gruber 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. -------------------------------------------------------------------------------- /core/sources/quad9.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "runtime" 9 | 10 | doh "github.com/picatz/doh/core" 11 | "golang.org/x/sync/semaphore" 12 | ) 13 | 14 | // Quad9 is a DNS over HTTPs resolver. 15 | type Quad9 struct { 16 | Lock *semaphore.Weighted 17 | } 18 | 19 | // String is a custom printer for debugging purposes. 20 | func (s *Quad9) String() string { 21 | return "quad9" 22 | } 23 | 24 | var quad9Base = "https://dns.quad9.net/dns-query" 25 | 26 | // Query handles a resolving a given domain name to a list of IPs 27 | func (s *Quad9) Query(ctx context.Context, d doh.Domain, t doh.Type) (*doh.Response, error) { 28 | if s.Lock == nil { 29 | s.Lock = semaphore.NewWeighted(int64(runtime.GOMAXPROCS(0))) 30 | } 31 | 32 | if err := s.Lock.Acquire(ctx, 1); err != nil { 33 | return nil, err 34 | } 35 | defer s.Lock.Release(1) 36 | 37 | req, err := http.NewRequest("GET", quad9Base, nil) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | req.Cancel = ctx.Done() 43 | req.WithContext(ctx) 44 | 45 | q := req.URL.Query() 46 | q.Add("name", d) 47 | q.Add("type", t) 48 | 49 | req.URL.RawQuery = q.Encode() 50 | 51 | resp, err := http.DefaultClient.Do(req) 52 | if err != nil { 53 | return nil, err 54 | } 55 | defer resp.Body.Close() 56 | 57 | if resp.Body == nil { 58 | return nil, errors.New("no resp body from server") 59 | } 60 | 61 | record := &doh.Response{} 62 | 63 | err = json.NewDecoder(resp.Body).Decode(record) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return record, nil 69 | } 70 | -------------------------------------------------------------------------------- /core/sources/google.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "runtime" 9 | 10 | doh "github.com/picatz/doh/core" 11 | "golang.org/x/sync/semaphore" 12 | ) 13 | 14 | // Google is a DNS over HTTPs resolver. 15 | type Google struct { 16 | Lock *semaphore.Weighted 17 | } 18 | 19 | // String is a custom printer for debugging purposes. 20 | func (s *Google) String() string { 21 | return "google" 22 | } 23 | 24 | var googleBase = "https://dns.google.com/resolve" 25 | 26 | // Query handles a resolving a given domain name to a list of IPs 27 | func (s *Google) Query(ctx context.Context, d doh.Domain, t doh.Type) (*doh.Response, error) { 28 | if s.Lock == nil { 29 | s.Lock = semaphore.NewWeighted(int64(runtime.GOMAXPROCS(0))) 30 | } 31 | 32 | if err := s.Lock.Acquire(ctx, 1); err != nil { 33 | return nil, err 34 | } 35 | defer s.Lock.Release(1) 36 | 37 | req, err := http.NewRequest("GET", googleBase, nil) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | req.Cancel = ctx.Done() 43 | req.WithContext(ctx) 44 | 45 | q := req.URL.Query() 46 | q.Add("name", d) 47 | q.Add("type", t) 48 | 49 | req.URL.RawQuery = q.Encode() 50 | 51 | resp, err := http.DefaultClient.Do(req) 52 | if err != nil { 53 | return nil, err 54 | } 55 | defer resp.Body.Close() 56 | 57 | if resp.Body == nil { 58 | return nil, errors.New("no resp body from server") 59 | } 60 | 61 | record := &doh.Response{} 62 | 63 | err = json.NewDecoder(resp.Body).Decode(record) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return record, nil 69 | } 70 | -------------------------------------------------------------------------------- /core/sources/cloudflare.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "runtime" 9 | 10 | doh "github.com/picatz/doh/core" 11 | "golang.org/x/sync/semaphore" 12 | ) 13 | 14 | // Cloudflare is a DNS over HTTPs resolver. 15 | type Cloudflare struct { 16 | Lock *semaphore.Weighted 17 | } 18 | 19 | // String is a custom printer for debugging purposes. 20 | func (s *Cloudflare) String() string { 21 | return "cloudflare" 22 | } 23 | 24 | var cloudflareBase = "https://cloudflare-dns.com/dns-query" 25 | 26 | // Query handles a resolving a given domain name to a list of IPs 27 | func (s *Cloudflare) Query(ctx context.Context, d doh.Domain, t doh.Type) (*doh.Response, error) { 28 | if s.Lock == nil { 29 | s.Lock = semaphore.NewWeighted(int64(runtime.GOMAXPROCS(0))) 30 | } 31 | 32 | if err := s.Lock.Acquire(ctx, 1); err != nil { 33 | return nil, err 34 | } 35 | defer s.Lock.Release(1) 36 | 37 | req, err := http.NewRequest("GET", cloudflareBase, nil) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | req.Header.Set("Accept", "application/dns-json") 43 | 44 | req.Cancel = ctx.Done() 45 | req.WithContext(ctx) 46 | 47 | q := req.URL.Query() 48 | q.Add("name", d) 49 | q.Add("type", t) 50 | 51 | req.URL.RawQuery = q.Encode() 52 | 53 | resp, err := http.DefaultClient.Do(req) 54 | if err != nil { 55 | return nil, err 56 | } 57 | defer resp.Body.Close() 58 | 59 | if resp.Body == nil { 60 | return nil, errors.New("no resp body from server") 61 | } 62 | 63 | record := &doh.Response{} 64 | 65 | err = json.NewDecoder(resp.Body).Decode(record) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return record, nil 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # doh 2 | > 🍩 DNS over HTTPs command-line client 3 | 4 | Using [`cloudflare`](https://developers.cloudflare.com/1.1.1.1/dns-over-https/), [`google`](https://developers.google.com/speed/public-dns/docs/dns-over-https), and [`quad9`](https://quad9.net/doh-quad9-dns-servers/) the `doh` command-line utility can concurrently lookup all three sources for one or more given domain(s). 5 | 6 | > **Note**: Since `doh` outputs everything as JSON, it pairs really well with tools like [`jq`](https://stedolan.github.io/jq/) to parse relevant parts of the output for your purposes. 7 | 8 | # Install 9 | To get started, you will need [`go`](https://golang.org/doc/install) installed and properly configured. 10 | ```shell 11 | $ go get github.com/picatz/doh 12 | ``` 13 | 14 | # Update 15 | As new updates come out, you can update `doh` using the `-u` flag with `go get`. 16 | ```shell 17 | $ go get -u github.com/picatz/doh 18 | `` 19 | 20 | # Help Menus 21 | The `--help` command-line flag can show you the top-level help menu. 22 | ```shell 23 | $ doh --help 24 | ``` 25 | ``` 26 | Usage: 27 | doh [command] 28 | 29 | Available Commands: 30 | help Help about any command 31 | query Query domains for DNS records in JSON 32 | 33 | Flags: 34 | -h, --help help for doh 35 | 36 | Use "doh [command] --help" for more information about a command. 37 | ``` 38 | 39 | To get more information for the `query` command: 40 | ```shell 41 | $ doh query --help 42 | ``` 43 | ``` 44 | Query domains for DNS records in JSON 45 | 46 | Usage: 47 | doh query [domains] [flags] 48 | 49 | Flags: 50 | -h, --help help for query 51 | --joined join results into a JSON object 52 | --labels show source of the dns record 53 | --limit int limit the number of responses from backend sources (default 1) 54 | --lock int number of concurrent workers (default 8) 55 | --no-limit do not limit results 56 | --no-timeout do not timeout 57 | --sources strings sources to use for query (default [google,cloudflare,quad9]) 58 | --timeout int number of seconds until timeout (default 30) 59 | --type string dns record type to query for ("A", "AAAA", "MX" ...) (default "A") 60 | --verbose show errors and other available diagnostic information 61 | ``` 62 | 63 | # Example Usage 64 | Let's say I'm curious about `google.com`'s IPv4 address and want to use `doh` to find out what it is. 65 | ``` 66 | $ doh query google.com 67 | ``` 68 | ``` 69 | {"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"google.com.","type":1}],"Answer":[{"name":"google.com.","type":1,"TTL":100,"data":"172.217.8.206"}]} 70 | ``` 71 | 72 | You can see the source of the DNS record using the `--labels` flag: 73 | ``` 74 | $ doh query google.com --labels 75 | ``` 76 | ``` 77 | {"label":"quad9","resp":{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"google.com.","type":1}],"Answer":[{"name":"google.com.","type":1,"TTL":56,"data":"172.217.8.206"}]}} 78 | ``` 79 | 80 | You can wait for responses from all sources with the `--no-limit` flag: 81 | ``` 82 | $ doh query google.com --labels --no-limit 83 | ``` 84 | ``` 85 | {"label":"quad9","resp":{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"google.com.","type":1}],"Answer":[{"name":"google.com.","type":1,"TTL":40,"data":"216.58.216.238"}]}} 86 | {"label":"google","resp":{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"google.com.","type":1}],"Answer":[{"name":"google.com.","type":1,"TTL":213,"data":"108.177.111.113"},{"name":"google.com.","type":1,"TTL":213,"data":"108.177.111.101"},{"name":"google.com.","type":1,"TTL":213,"data":"108.177.111.100"},{"name":"google.com.","type":1,"TTL":213,"data":"108.177.111.138"},{"name":"google.com.","type":1,"TTL":213,"data":"108.177.111.139"},{"name":"google.com.","type":1,"TTL":213,"data":"108.177.111.102"}]}} 87 | {"label":"cloudflare","resp":{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"google.com.","type":1}],"Answer":[{"name":"google.com.","type":1,"TTL":195,"data":"172.217.1.46"}]}} 88 | ``` 89 | 90 | To get just all of the IPs from all of those sources, we could do the following: 91 | ``` 92 | $ doh query google.com --no-limit --joined | jq 'map(.Answer | map(.data)) | flatten | .[]' --raw-output 93 | ``` 94 | ``` 95 | 172.217.8.206 96 | 108.177.111.139 97 | 108.177.111.113 98 | 108.177.111.138 99 | 108.177.111.101 100 | 108.177.111.100 101 | 108.177.111.102 102 | 172.217.4.206 103 | ``` 104 | 105 | If we want to filter the output to just the first IP address in the first JSON record with `jq`: 106 | ``` 107 | $ doh query google.com | jq .Answer[0].data --raw-output 108 | ``` 109 | ``` 110 | 172.217.8.206 111 | ``` 112 | 113 | Now, perhaps `google.com` isn't the _only_ record we're also interested in, since we also want `bing.com`, which is where the _cool kids_ are at. 114 | ``` 115 | $ doh query bing.com apple.com --limit 2 | jq '(.Answer[0].name|rtrimstr(".")) + "\t" + .Answer[0].data' --raw-output 116 | ``` 117 | ``` 118 | apple.com 172.217.8.206 119 | bing.com 204.79.197.200 120 | ``` 121 | 122 | To get `IPv6` records, we'll need to specify the `--type` flag, like so: 123 | ``` 124 | $ doh query google.com --type AAAA 125 | ``` 126 | 127 | To get `MX` records: 128 | ``` 129 | $ doh query google.com --type MX 130 | ``` 131 | 132 | To get `ANY` records (which is only implemented by the `google` source): 133 | ``` 134 | $ doh query google.com --type ANY --sources=google 135 | ``` -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "runtime" 10 | "sync" 11 | "time" 12 | 13 | "golang.org/x/sync/semaphore" 14 | 15 | "github.com/picatz/doh/core" 16 | "github.com/picatz/doh/core/sources" 17 | 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | func init() { 22 | c := make(chan os.Signal, 1) 23 | signal.Notify(c, os.Interrupt) 24 | go func() { 25 | for range c { 26 | os.Exit(0) 27 | } 28 | }() 29 | } 30 | 31 | type result struct { 32 | Label string `json:"label"` 33 | Resp *core.Response `json:"resp"` 34 | } 35 | 36 | func main() { 37 | results := make(chan result) 38 | jobs := sync.WaitGroup{} 39 | 40 | // ctrl+C exit 41 | exit := func(reason string, code int) { 42 | if reason != "" { 43 | fmt.Println("exiting:", reason) 44 | } 45 | os.Exit(0) 46 | } 47 | 48 | c := make(chan os.Signal, 1) 49 | signal.Notify(c, os.Interrupt) 50 | go func() { 51 | for range c { 52 | exit("", 0) 53 | } 54 | }() 55 | 56 | // enumerate command options 57 | var ( 58 | ctx context.Context 59 | cancel context.CancelFunc 60 | 61 | cmdQueryVerboseOpt bool 62 | cmdQueryLabelsOpt bool 63 | cmdQueryJoinedOpt bool 64 | cmdQueryTimeoutOpt int64 65 | cmdQueryLockOpt int64 66 | cmdQueryLimitOpt int64 67 | cmdQueryNoLimitOpt bool 68 | cmdQueryNoTimeoutOpt bool 69 | cmdQuerySourcesOpt []string 70 | cmdQueryQueryTypeOpt string 71 | 72 | defaultQuerySources = []string{"google", "cloudflare", "quad9"} 73 | defaultLockValue = int64(runtime.GOMAXPROCS(0)) 74 | defaultQueryType = core.IPv4Type 75 | defaultLimitOpt = int64(1) 76 | 77 | querySources = core.Sources{} 78 | ) 79 | 80 | var cmdQuery = &cobra.Command{ 81 | Use: "query [domains]", 82 | Short: "Query domains for DNS records in JSON", 83 | Args: cobra.MinimumNArgs(1), 84 | PreRun: func(cmd *cobra.Command, args []string) { 85 | sharedLock := semaphore.NewWeighted(cmdQueryLockOpt) 86 | 87 | for _, sourceStr := range cmdQuerySourcesOpt { 88 | switch sourceStr { 89 | case "google": 90 | querySources = append(querySources, &sources.Google{sharedLock}) 91 | case "cloudflare": 92 | querySources = append(querySources, &sources.Cloudflare{sharedLock}) 93 | case "quad9": 94 | querySources = append(querySources, &sources.Quad9{sharedLock}) 95 | } 96 | } 97 | 98 | if len(querySources) == 0 { 99 | exit("no query sources", 1) 100 | } 101 | 102 | if cmdQueryNoTimeoutOpt { 103 | ctx, cancel = context.WithCancel(context.Background()) 104 | } else { 105 | ctx, cancel = context.WithTimeout(context.Background(), time.Second*time.Duration(cmdQueryTimeoutOpt)) 106 | } 107 | }, 108 | Run: func(cmd *cobra.Command, args []string) { 109 | if !cmdQueryNoLimitOpt && (cmdQueryLimitOpt < int64(len(args))) { 110 | exit("more domain arguments given than the default limit value (1)", 1) 111 | } 112 | 113 | jobs.Add(1) 114 | 115 | go func() { 116 | defer close(results) 117 | defer jobs.Done() 118 | defer cancel() 119 | 120 | wg := sync.WaitGroup{} 121 | wg.Add(len(args)) 122 | 123 | for _, arg := range args { 124 | go func(queryName core.Domain, queryType core.Type) { 125 | defer wg.Done() 126 | for _, src := range querySources { 127 | wg.Add(1) 128 | go func(src core.Source) { 129 | defer wg.Done() 130 | if ctx.Err() != nil { 131 | return 132 | } 133 | 134 | resp, err := src.Query(ctx, queryName, queryType) 135 | 136 | if err != nil && cmdQueryVerboseOpt { 137 | fmt.Println("error:", err) 138 | return 139 | } 140 | 141 | if resp != nil { 142 | select { 143 | case <-ctx.Done(): 144 | return 145 | case results <- result{Label: src.String(), Resp: resp}: 146 | return 147 | } 148 | } 149 | }(src) 150 | } 151 | }(arg, cmdQueryQueryTypeOpt) 152 | } 153 | 154 | wg.Wait() 155 | }() 156 | }, 157 | PostRun: func(cmd *cobra.Command, args []string) { 158 | defer cancel() 159 | 160 | jobs.Add(1) 161 | 162 | go func() { 163 | defer jobs.Done() 164 | 165 | var joined = []interface{}{} 166 | 167 | if cmdQueryJoinedOpt { 168 | defer func() { 169 | b, err := json.Marshal(joined) 170 | if err != nil { 171 | fmt.Println(err) 172 | return 173 | } 174 | fmt.Println(string(b)) 175 | }() 176 | } 177 | 178 | counter := int64(0) 179 | 180 | for result := range results { 181 | if !cmdQueryNoLimitOpt && (counter == cmdQueryLimitOpt) { 182 | cancel() 183 | return 184 | } 185 | 186 | var ( 187 | b []byte 188 | err error 189 | ) 190 | 191 | if cmdQueryJoinedOpt { 192 | if cmdQueryLabelsOpt { 193 | joined = append(joined, result) 194 | } else { 195 | joined = append(joined, result.Resp) 196 | } 197 | continue 198 | } 199 | 200 | if cmdQueryLabelsOpt { 201 | b, err = json.Marshal(result) 202 | } else { 203 | b, err = json.Marshal(result.Resp) 204 | } 205 | 206 | if err != nil && cmdQueryVerboseOpt { 207 | fmt.Println("error:", err) 208 | continue 209 | } 210 | fmt.Println(string(b)) 211 | 212 | counter++ 213 | } 214 | }() 215 | 216 | jobs.Wait() 217 | }, 218 | } 219 | 220 | cmdQuery.Flags().StringVar(&cmdQueryQueryTypeOpt, "type", defaultQueryType, "dns record type to query for (\"A\", \"AAAA\", \"MX\" ...)") 221 | cmdQuery.Flags().StringSliceVar(&cmdQuerySourcesOpt, "sources", defaultQuerySources, "sources to use for query") 222 | cmdQuery.Flags().Int64Var(&cmdQueryTimeoutOpt, "timeout", 30, "number of seconds until timeout") 223 | cmdQuery.Flags().BoolVar(&cmdQueryNoTimeoutOpt, "no-timeout", false, "do not timeout") 224 | cmdQuery.Flags().BoolVar(&cmdQueryNoLimitOpt, "no-limit", false, "do not limit results") 225 | cmdQuery.Flags().Int64Var(&cmdQueryLockOpt, "lock", defaultLockValue, "number of concurrent workers") 226 | cmdQuery.Flags().Int64Var(&cmdQueryLimitOpt, "limit", defaultLimitOpt, "limit the number of responses from backend sources") 227 | cmdQuery.Flags().BoolVar(&cmdQueryVerboseOpt, "verbose", false, "show errors and other available diagnostic information") 228 | cmdQuery.Flags().BoolVar(&cmdQueryLabelsOpt, "labels", false, "show source of the dns record") 229 | cmdQuery.Flags().BoolVar(&cmdQueryJoinedOpt, "joined", false, "join results into a JSON object") 230 | 231 | var rootCmd = &cobra.Command{Use: "doh"} 232 | rootCmd.AddCommand(cmdQuery) 233 | rootCmd.Execute() 234 | } 235 | --------------------------------------------------------------------------------