├── .gitignore ├── README.md ├── main.go └── resolver └── resolver.go /.gitignore: -------------------------------------------------------------------------------- 1 | test 2 | resolvers.txt 3 | reckdns 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reckdns 2 | A kinda reckless dns resolver. **This is VERYYY much still under development,** feel free to open issues and/or make pull-requests 3 | 4 | ## Installation: 5 | via `go get` 6 | 7 | ``` 8 | ▻ go get github.com/lc/reckdns 9 | ``` 10 | 11 | from repo: 12 | ``` 13 | ▻ git clone https://github.com/lc/reckdns && cd reckdns 14 | ▻ go build -o $GOPATH/bin/reckdns main.go 15 | ``` 16 | 17 | ## Usage: 18 | ``` 19 | ▻ printf 'www.yahoo.com\nwww.google.com\nwww.amazon.com' | reckdns -r resolvers.txt 20 | ▻ reckdns -r resolvers.txt -i hosts.txt 21 | ``` 22 | 23 | ### Warning: 24 | For each concurrent thread, there will be however many workers as you have resolvers in your resolvers.txt file. I usually have around 10 resolvers in mine. I should probably change this soon... 25 | 26 | From: https://github.com/lc/reckdns/blob/af03707e918a92d215d7a3ef9e3d12895ed51140/resolver/resolver.go#L79 27 | 28 | ```go 29 | for i := 0; i < r.Concurrency; i++ { 30 | ... 31 | for _, resolver := range r.Resolvers { 32 | c, err := net.Dial("udp", resolver) 33 | ... 34 | r.doresolve(c, jobChan, resultChan) 35 | } 36 | ... 37 | } 38 | 39 | ``` 40 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | "github.com/lc/reckdns/resolver" 11 | ) 12 | 13 | func main() { 14 | outfile := flag.String("o", "", "file to save output to") 15 | workers := flag.Int("t", 5, "number of concurrent workers") 16 | input := flag.String("i", "", "File to read domains from.") 17 | resolvers := flag.String("r", "", "path to file containing resolvers (ip:port)") 18 | pps := flag.Int("pps", 200, "DNS packets per second") 19 | jsonout := flag.Bool("json", false, "format output as json") 20 | 21 | flag.Usage = func() { 22 | fmt.Fprintf(os.Stderr, strings.Join([]string{ 23 | "reckdns - a kinda reckless dns resolver", 24 | "", 25 | "Usage: reckdns [options ...]", 26 | "", 27 | }, "\n")) 28 | flag.PrintDefaults() 29 | } 30 | flag.Parse() 31 | if *resolvers == "" { 32 | flag.Usage() 33 | os.Exit(1) 34 | } 35 | r := resolver.New() 36 | r.OutputFile = *outfile 37 | if *jsonout { 38 | r.EnableJsonOutput() 39 | } 40 | if err := r.SetConcurrency(*workers); err != nil { 41 | log.Fatal(err) 42 | } 43 | if *input != "" { 44 | err := r.SetInputFile(*input) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | } 49 | if err := r.SetResolversFile(*resolvers); err != nil { 50 | log.Fatal(err) 51 | } 52 | if err := r.SetMaxPPS(*pps); err != nil { 53 | log.Fatal(err) 54 | } 55 | err := r.Resolve() 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /resolver/resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "net" 10 | "os" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/lc/safewrite" 16 | 17 | "github.com/miekg/dns" 18 | ) 19 | 20 | type JsonOutput struct { 21 | Domain string `json:"domain"` 22 | CNAME []string `json:"CNAME"` 23 | A []string `json:"A"` 24 | } 25 | type dnsrecord struct { 26 | CNAME bool 27 | record string 28 | } 29 | type result struct { 30 | Host string 31 | Results []dnsrecord 32 | } 33 | type toResolve struct { 34 | Host string 35 | } 36 | type Resolver struct { 37 | JsonOut bool 38 | Input string 39 | Resolvers []string 40 | ResolversFile string 41 | Concurrency int 42 | Hosts []string 43 | OutputFile string 44 | ReadTimeout time.Duration // defaults to 2 seconds 45 | WriteTimeout time.Duration // defaults to 2 seconds 46 | delay time.Duration // defaults 5 milliseconds (5000000 nano seconds) 47 | } 48 | 49 | func New() *Resolver { 50 | return &Resolver{ReadTimeout: time.Second * 2, WriteTimeout: time.Second * 2, JsonOut: false} 51 | } 52 | func (r *Resolver) EnableJsonOutput() { 53 | r.JsonOut = true 54 | } 55 | func (r *Resolver) SetResolversFile(ResolversFile string) error { 56 | if _, err := os.Stat(ResolversFile); err != nil { 57 | return fmt.Errorf("error, invalid resolvers file: %v", err) 58 | } 59 | r.ResolversFile = ResolversFile 60 | return nil 61 | } 62 | func (r *Resolver) SetConcurrency(i int) error { 63 | if i > 0 { 64 | r.Concurrency = i 65 | return nil 66 | } 67 | return errors.New("cannot set concurrent workers, number must be greater than 0.") 68 | } 69 | func (r *Resolver) SetInputFile(File string) error { 70 | _, err := os.Stat(File) 71 | if err != nil { 72 | return fmt.Errorf("Could not set input file: %s", err.Error()) 73 | } 74 | r.Input = File 75 | return nil 76 | } 77 | func (r *Resolver) SetMaxPPS(Packets int) error { 78 | if Packets > 0 { 79 | r.delay = time.Duration(1000000000/Packets) * time.Nanosecond 80 | return nil 81 | } 82 | return errors.New("packets per second must be greater than 0") 83 | } 84 | func (r *Resolver) Resolve() error { 85 | if err := r.readResolvers(); err != nil { 86 | return err 87 | } 88 | var appender safewrite.SafeAppend 89 | var jobWg, resultWg sync.WaitGroup 90 | jobChan := make(chan string) 91 | resultChan := make(chan result) 92 | // concurrent workers spawn how ever many resolvers 93 | // there are * the num threads. should make this different 94 | for i := 0; i < r.Concurrency; i++ { 95 | jobWg.Add(1) 96 | go func() { 97 | for _, resolver := range r.Resolvers { 98 | c, err := net.Dial("udp", resolver) 99 | if err != nil { 100 | if strings.Contains(err.Error(), "too many open files") { 101 | log.Fatal(err) 102 | } else { 103 | fmt.Fprintf(os.Stderr, "bind(udp,%s) error: %v", resolver, err) 104 | continue 105 | } 106 | } 107 | r.doresolve(c, jobChan, resultChan) 108 | } 109 | jobWg.Done() 110 | }() 111 | } 112 | 113 | resultWg.Add(1) 114 | if r.OutputFile != "" { 115 | outfp, err := os.OpenFile(r.OutputFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 116 | if err != nil { 117 | return err 118 | } 119 | wr := safewrite.NewWriter(outfp) 120 | appender = safewrite.NewAppender(wr) 121 | } 122 | if r.JsonOut == false { 123 | go func() { 124 | for res := range resultChan { 125 | o := r.NormalOutput(res) 126 | if r.OutputFile != "" { 127 | appender.Append([]byte(o)) 128 | } else { 129 | fmt.Println(o) 130 | } 131 | } 132 | resultWg.Done() 133 | }() 134 | } else { 135 | go func() { 136 | for res := range resultChan { 137 | fin, err := r.JsonOutput(res) 138 | if err != nil { 139 | fmt.Fprintf(os.Stderr, "error marshalling output: %v", err) 140 | continue 141 | } 142 | if r.OutputFile != "" { 143 | appender.Append(fin) 144 | } else { 145 | fmt.Println(string(fin)) 146 | } 147 | } 148 | resultWg.Done() 149 | }() 150 | } 151 | var b *bufio.Scanner 152 | switch r.Input { 153 | case "": 154 | b = bufio.NewScanner(os.Stdin) 155 | default: 156 | ifp, err := os.Open(r.Input) 157 | if err != nil { 158 | return fmt.Errorf("error opening input file: %s", err.Error()) 159 | } 160 | b = bufio.NewScanner(ifp) 161 | } 162 | go func() { 163 | for b.Scan() { 164 | server := b.Text() 165 | if !isDomainName(server) { 166 | continue 167 | } 168 | jobChan <- server 169 | } 170 | close(jobChan) 171 | }() 172 | 173 | jobWg.Wait() 174 | close(resultChan) 175 | resultWg.Wait() 176 | 177 | return nil 178 | } 179 | func (r *Resolver) readResolvers() error { 180 | rfp, err := os.Open(r.ResolversFile) 181 | if err != nil { 182 | return fmt.Errorf("could not open resolvers file: %v", err) 183 | } 184 | defer rfp.Close() 185 | sc := bufio.NewScanner(rfp) 186 | for sc.Scan() { 187 | resolver := sc.Text() 188 | if strings.Contains(resolver, ":") { 189 | r.Resolvers = append(r.Resolvers, resolver) 190 | } else { 191 | r.Resolvers = append(r.Resolvers, fmt.Sprintf("%s:53", resolver)) 192 | } 193 | } 194 | if err := sc.Err(); err != nil { 195 | return fmt.Errorf("error reading in resolvers: %v", err) 196 | } 197 | return nil 198 | } 199 | func (r *Resolver) doresolve(c net.Conn, hostChan chan string, resultChan chan result) { 200 | for host := range hostChan { 201 | in, err := r.exchange(c, msg(host)) 202 | if err != nil { 203 | if strings.HasSuffix(err.Error(), "i/o timeout") { 204 | //host.Retries-- 205 | continue 206 | } else { 207 | fmt.Fprintf(os.Stderr, "exchange error: %v\n", err) 208 | continue 209 | } 210 | } 211 | if in != nil && in.Rcode != dns.RcodeSuccess { 212 | continue 213 | } 214 | res := result{Host: host} 215 | // if answer is 1 then assume it's an A record. 216 | if len(in.Answer) == 1 { 217 | if re, ok := in.Answer[0].(*dns.A); ok { 218 | res.Results = append(res.Results, dnsrecord{CNAME: false, record: re.A.String()}) 219 | } 220 | } else { 221 | // otherwise answer contains multiple records. 222 | for _, record := range in.Answer { 223 | switch rec := record.(type) { 224 | case *dns.CNAME: 225 | res.Results = append(res.Results, dnsrecord{CNAME: true, record: rec.Target}) 226 | case *dns.A: 227 | res.Results = append(res.Results, dnsrecord{CNAME: false, record: rec.A.String()}) 228 | } 229 | } 230 | } 231 | // if results aren't empty, send them back. 232 | if len(res.Results) > 0 { 233 | resultChan <- res 234 | } 235 | res.Host = "" 236 | res.Results = nil 237 | time.Sleep(r.delay) 238 | } 239 | c.Close() 240 | } 241 | 242 | // creates a new dns message and returns it 243 | func msg(host string) *dns.Msg { 244 | m := &dns.Msg{} 245 | m.Id = dns.Id() 246 | m.RecursionDesired = true 247 | m.Question = make([]dns.Question, 1) 248 | m.Question[0] = dns.Question{Name: dns.Fqdn(host), Qtype: dns.TypeA, Qclass: dns.ClassINET} 249 | return m 250 | } 251 | 252 | func (r *Resolver) exchange(c net.Conn, m *dns.Msg) (res *dns.Msg, err error) { 253 | co := new(dns.Conn) 254 | co.Conn = c 255 | co.SetReadDeadline(time.Now().Add(r.WriteTimeout)) 256 | if err = co.WriteMsg(m); err != nil { 257 | if e, ok := err.(net.Error); !ok || !e.Timeout() { 258 | return nil, e 259 | } 260 | return nil, err 261 | } 262 | co.SetReadDeadline(time.Now().Add(r.ReadTimeout)) 263 | res, err = co.ReadMsg() 264 | if e, ok := err.(net.Error); !ok || !e.Timeout() { 265 | return res, e 266 | } 267 | if err == nil && res.Id != m.Id { 268 | err = dns.ErrId 269 | } 270 | return res, err 271 | } 272 | 273 | // from: https://github.com/majek/goplayground/blob/73ec9678fd70a04f3afdcd1b63ce66aec4d812fc/resolve/dnsclient.go#L118 274 | func isDomainName(s string) bool { 275 | // See RFC 1035, RFC 3696. 276 | if len(s) == 0 { 277 | return false 278 | } 279 | if len(s) > 255 { 280 | return false 281 | } 282 | 283 | last := byte('.') 284 | ok := false // Ok once we've seen a letter. 285 | partlen := 0 286 | for i := 0; i < len(s); i++ { 287 | c := s[i] 288 | switch { 289 | default: 290 | return false 291 | case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': 292 | ok = true 293 | partlen++ 294 | case '0' <= c && c <= '9': 295 | // fine 296 | partlen++ 297 | case c == '-': 298 | // Byte before dash cannot be dot. 299 | if last == '.' { 300 | return false 301 | } 302 | partlen++ 303 | case c == '.': 304 | // Byte before dot cannot be dot, dash. 305 | if last == '.' || last == '-' { 306 | return false 307 | } 308 | if partlen > 63 || partlen == 0 { 309 | return false 310 | } 311 | partlen = 0 312 | } 313 | last = c 314 | } 315 | if last == '-' || partlen > 63 { 316 | return false 317 | } 318 | 319 | return ok 320 | } 321 | func (r *Resolver) NormalOutput(res result) string { 322 | var output []string 323 | output = append(output, fmt.Sprintf("%s ->", res.Host)) 324 | for _, result := range res.Results { 325 | if result.CNAME { 326 | output = append(output, fmt.Sprintf(" %s ->", result.record)) 327 | } else { 328 | output = append(output, fmt.Sprintf(" %s", result.record)) 329 | } 330 | } 331 | o := strings.Join(output, "") 332 | return o 333 | } 334 | func (r *Resolver) JsonOutput(res result) ([]byte, error) { 335 | out := JsonOutput{} 336 | out.Domain = res.Host 337 | for _, result := range res.Results { 338 | if result.CNAME { 339 | out.CNAME = append(out.CNAME, result.record) 340 | } else { 341 | out.A = append(out.A, result.record) 342 | } 343 | } 344 | fin, err := json.Marshal(out) 345 | if err != nil { 346 | return nil, fmt.Errorf("error marshalling output: %v", err) 347 | } 348 | return fin, nil 349 | } 350 | --------------------------------------------------------------------------------