ptrstream- endless stream of rdns |
git clone git://git.acid.vegas/ptrstream.git |
Log | Files | Refs | Archive | README | LICENSE |
ptrstream.go (24626B)
1 package main 2 3 import ( 4 "bufio" 5 "encoding/json" 6 "flag" 7 "fmt" 8 "net" 9 "net/http" 10 "os" 11 "regexp" 12 "strconv" 13 "strings" 14 "sync" 15 "sync/atomic" 16 "time" 17 18 "github.com/acidvegas/golcg" 19 "github.com/miekg/dns" 20 "github.com/rivo/tview" 21 ) 22 23 const defaultResolversURL = "https://raw.githubusercontent.com/trickest/resolvers/refs/heads/main/resolvers.txt" 24 25 type Config struct { 26 concurrency int 27 timeout time.Duration 28 retries int 29 dnsServers []string 30 serverIndex int 31 debug bool 32 outputFile *os.File 33 mu sync.Mutex 34 lastDNSUpdate time.Time 35 updateMu sync.Mutex 36 loop bool 37 } 38 39 type Stats struct { 40 processed uint64 41 total uint64 42 lastProcessed uint64 43 lastCheckTime time.Time 44 startTime time.Time 45 success uint64 46 failed uint64 47 cnames uint64 48 speedHistory []float64 49 mu sync.Mutex 50 round uint64 51 } 52 53 func (s *Stats) increment() { 54 atomic.AddUint64(&s.processed, 1) 55 } 56 57 func (s *Stats) incrementSuccess() { 58 atomic.AddUint64(&s.success, 1) 59 } 60 61 func (s *Stats) incrementFailed() { 62 atomic.AddUint64(&s.failed, 1) 63 } 64 65 func (s *Stats) incrementCNAME() { 66 atomic.AddUint64(&s.cnames, 1) 67 } 68 69 func (c *Config) getNextServer() string { 70 if err := c.updateDNSServers(); err != nil { 71 fmt.Printf("Failed to update DNS servers: %v\n", err) 72 } 73 74 c.mu.Lock() 75 defer c.mu.Unlock() 76 77 if len(c.dnsServers) == 0 { 78 return "" 79 } 80 81 server := c.dnsServers[c.serverIndex] 82 c.serverIndex = (c.serverIndex + 1) % len(c.dnsServers) 83 return server 84 } 85 86 func fetchDefaultResolvers() ([]string, error) { 87 resp, err := http.Get(defaultResolversURL) 88 if err != nil { 89 return nil, fmt.Errorf("failed to fetch default resolvers: %v", err) 90 } 91 defer resp.Body.Close() 92 93 var resolvers []string 94 scanner := bufio.NewScanner(resp.Body) 95 for scanner.Scan() { 96 resolver := strings.TrimSpace(scanner.Text()) 97 if resolver != "" { 98 resolvers = append(resolvers, resolver) 99 } 100 } 101 102 if err := scanner.Err(); err != nil { 103 return nil, fmt.Errorf("error reading default resolvers: %v", err) 104 } 105 106 return resolvers, nil 107 } 108 109 func loadDNSServers(dnsFile string) ([]string, error) { 110 if dnsFile == "" { 111 resolvers, err := fetchDefaultResolvers() 112 if err != nil { 113 return nil, err 114 } 115 if len(resolvers) == 0 { 116 return nil, fmt.Errorf("no default resolvers found") 117 } 118 return resolvers, nil 119 } 120 121 file, err := os.Open(dnsFile) 122 if err != nil { 123 return nil, fmt.Errorf("failed to open DNS servers file: %v", err) 124 } 125 defer file.Close() 126 127 var servers []string 128 scanner := bufio.NewScanner(file) 129 for scanner.Scan() { 130 server := strings.TrimSpace(scanner.Text()) 131 if server != "" { 132 servers = append(servers, server) 133 } 134 } 135 136 if err := scanner.Err(); err != nil { 137 return nil, fmt.Errorf("error reading DNS servers file: %v", err) 138 } 139 140 if len(servers) == 0 { 141 return nil, fmt.Errorf("no DNS servers found in file") 142 } 143 144 return servers, nil 145 } 146 147 type DNSResponse struct { 148 Names []string 149 Server string 150 RecordType string // "PTR" or "CNAME" 151 Target string // For CNAME records, stores the target 152 TTL uint32 // Add TTL field 153 } 154 155 func translateRcode(rcode int) string { 156 switch rcode { 157 case dns.RcodeSuccess: 158 return "Success" 159 case dns.RcodeFormatError: 160 return "Format Error" 161 case dns.RcodeServerFailure: 162 return "Server Failure" 163 case dns.RcodeNameError: // NXDOMAIN 164 return "No Such Domain" 165 case dns.RcodeNotImplemented: 166 return "Not Implemented" 167 case dns.RcodeRefused: 168 return "Query Refused" 169 default: 170 return fmt.Sprintf("DNS Error %d", rcode) 171 } 172 } 173 174 func lookupWithRetry(ip string, cfg *Config) (DNSResponse, string, error) { 175 var lastErr error 176 var lastServer string 177 178 for i := 0; i < cfg.retries; i++ { 179 server := cfg.getNextServer() 180 if server == "" { 181 return DNSResponse{}, "", fmt.Errorf("no DNS servers available") 182 } 183 lastServer = server 184 185 // Create DNS message 186 m := new(dns.Msg) 187 arpa, err := dns.ReverseAddr(ip) 188 if err != nil { 189 return DNSResponse{}, "", err 190 } 191 m.SetQuestion(arpa, dns.TypePTR) 192 m.RecursionDesired = true 193 194 // Create DNS client 195 c := new(dns.Client) 196 c.Timeout = cfg.timeout 197 198 // Make the query 199 r, _, err := c.Exchange(m, server) 200 if err != nil { 201 lastErr = err 202 continue 203 } 204 205 if r.Rcode != dns.RcodeSuccess { 206 lastErr = fmt.Errorf("%s", translateRcode(r.Rcode)) 207 continue 208 } 209 210 // Process the response 211 if len(r.Answer) > 0 { 212 var names []string 213 var ttl uint32 214 var isCNAME bool 215 var target string 216 217 for _, ans := range r.Answer { 218 switch rr := ans.(type) { 219 case *dns.PTR: 220 names = append(names, rr.Ptr) 221 ttl = rr.Hdr.Ttl 222 case *dns.CNAME: 223 isCNAME = true 224 names = append(names, rr.Hdr.Name) 225 target = rr.Target 226 ttl = rr.Hdr.Ttl 227 } 228 } 229 230 if len(names) > 0 { 231 if isCNAME { 232 return DNSResponse{ 233 Names: names, 234 Server: server, 235 RecordType: "CNAME", 236 Target: strings.TrimSuffix(target, "."), 237 TTL: ttl, 238 }, server, nil 239 } 240 return DNSResponse{ 241 Names: names, 242 Server: server, 243 RecordType: "PTR", 244 TTL: ttl, 245 }, server, nil 246 } 247 } 248 249 lastErr = fmt.Errorf("no PTR records found") 250 } 251 252 return DNSResponse{}, lastServer, lastErr 253 } 254 255 func reverse(ss []string) []string { 256 reversed := make([]string, len(ss)) 257 for i, s := range ss { 258 reversed[len(ss)-1-i] = s 259 } 260 return reversed 261 } 262 263 func colorizeIPInPtr(ptr, ip string) string { 264 specialHosts := []string{"localhost", "undefined.hostname.localhost", "unknown"} 265 for _, host := range specialHosts { 266 if strings.EqualFold(ptr, host) { 267 return "[gray]" + ptr 268 } 269 } 270 271 octets := strings.Split(ip, ".") 272 273 patterns := []string{ 274 strings.ReplaceAll(ip, ".", "\\."), 275 strings.Join(reverse(strings.Split(ip, ".")), "\\."), 276 strings.ReplaceAll(ip, ".", "-"), 277 strings.Join(reverse(strings.Split(ip, ".")), "-"), 278 } 279 280 zeroPadded := make([]string, 4) 281 for i, octet := range octets { 282 zeroPadded[i] = fmt.Sprintf("%03d", parseInt(octet)) 283 } 284 patterns = append(patterns, 285 strings.Join(zeroPadded, "-"), 286 strings.Join(reverse(zeroPadded), "-"), 287 ) 288 289 pattern := strings.Join(patterns, "|") 290 re := regexp.MustCompile("(" + pattern + ")") 291 292 matches := re.FindAllStringIndex(ptr, -1) 293 if matches == nil { 294 return "[white]" + ptr 295 } 296 297 var result strings.Builder 298 lastEnd := 0 299 300 for _, match := range matches { 301 if match[0] > lastEnd { 302 result.WriteString("[white]") 303 result.WriteString(ptr[lastEnd:match[0]]) 304 } 305 result.WriteString("[aqua]") 306 result.WriteString(ptr[match[0]:match[1]]) 307 lastEnd = match[1] 308 } 309 310 if lastEnd < len(ptr) { 311 result.WriteString("[white]") 312 result.WriteString(ptr[lastEnd:]) 313 } 314 315 finalResult := result.String() 316 317 if strings.HasSuffix(finalResult, ".in-addr.arpa") { 318 finalResult = finalResult[:len(finalResult)-13] + ".[blue]in-addr.arpa" 319 } 320 if strings.HasSuffix(finalResult, ".gov") { 321 finalResult = finalResult[:len(finalResult)-4] + ".[red]gov" 322 } 323 if strings.HasSuffix(finalResult, ".mil") { 324 finalResult = finalResult[:len(finalResult)-4] + ".[red]mil" 325 } 326 327 return finalResult 328 } 329 330 func parseInt(s string) int { 331 num := 0 332 fmt.Sscanf(s, "%d", &num) 333 return num 334 } 335 336 const maxBufferLines = 1000 337 338 func worker(jobs <-chan string, wg *sync.WaitGroup, cfg *Config, stats *Stats, textView *tview.TextView, app *tview.Application) { 339 defer wg.Done() 340 for ip := range jobs { 341 timestamp := time.Now() 342 var response DNSResponse 343 var err error 344 var server string 345 346 if len(cfg.dnsServers) > 0 { 347 response, server, err = lookupWithRetry(ip, cfg) 348 if idx := strings.Index(server, ":"); idx != -1 { 349 server = server[:idx] 350 } 351 } else { 352 names, err := net.LookupAddr(ip) 353 if err == nil { 354 response = DNSResponse{Names: names, RecordType: "PTR"} 355 } 356 } 357 358 stats.increment() 359 360 if err != nil { 361 stats.incrementFailed() 362 if cfg.debug { 363 errRecord := formatErrorAsHostname(err) 364 timeStr := time.Now().Format("2006-01-02 15:04:05") 365 line := fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] [aqua]%-15s[-] [gray]│[-] [red] ERR [-] [gray]│[-] [gray]%-6s[-] [gray]│[-] [gray]%s[-]\n", 366 timeStr, 367 ip, 368 server, 369 "", 370 errRecord) 371 app.QueueUpdateDraw(func() { 372 fmt.Fprint(textView, line) 373 textView.ScrollToEnd() 374 }) 375 376 // Write to NDJSON if enabled 377 writeNDJSON(cfg, time.Now(), ip, server, errRecord, "ERR", "", 0) 378 } 379 continue 380 } 381 382 if len(response.Names) == 0 { 383 stats.incrementFailed() 384 if cfg.debug { 385 timeStr := time.Now().Format("2006-01-02 15:04:05") 386 line := fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] [aqua]%-15s[-] [gray]│[-] [red] ERR [-] [gray]│[-] [gray]%-6s[-] [gray]│[-] [red]No PTR record[-]\n", 387 timeStr, 388 ip, 389 server, 390 "") 391 app.QueueUpdateDraw(func() { 392 fmt.Fprint(textView, line) 393 textView.ScrollToEnd() 394 }) 395 } 396 continue 397 } 398 399 stats.incrementSuccess() 400 401 ptr := "" 402 for _, name := range response.Names { 403 if cleaned := strings.TrimSpace(strings.TrimSuffix(name, ".")); cleaned != "" { 404 ptr = strings.ToLower(cleaned) 405 break 406 } 407 } 408 409 if ptr == "" { 410 continue 411 } 412 413 writeNDJSON(cfg, timestamp, ip, server, ptr, response.RecordType, response.Target, response.TTL) 414 415 timeStr := time.Now().Format("2006-01-02 15:04:05") 416 recordTypeColor := "[blue] PTR [-]" 417 if response.RecordType == "CNAME" { 418 stats.incrementCNAME() 419 recordTypeColor = "[fuchsia]CNAME[-]" 420 ptr = fmt.Sprintf("%s -> %s", strings.ToLower(ptr), strings.ToLower(response.Target)) 421 } 422 423 var line string 424 if len(cfg.dnsServers) > 0 { 425 line = fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] [aqua]%-15s[-] [gray]│[-] %-5s [gray]│[-] %s [gray]│[-] %s\n", 426 timeStr, 427 ip, 428 server, 429 recordTypeColor, 430 colorizeTTL(response.TTL), 431 colorizeIPInPtr(ptr, ip)) 432 } else { 433 line = fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] %-5s [gray]│[-] %s [gray]│[-] %s\n", 434 timeStr, 435 ip, 436 recordTypeColor, 437 colorizeTTL(response.TTL), 438 colorizeIPInPtr(ptr, ip)) 439 } 440 441 app.QueueUpdateDraw(func() { 442 fmt.Fprint(textView, line) 443 content := textView.GetText(false) 444 lines := strings.Split(content, "\n") 445 if len(lines) > maxBufferLines { 446 newContent := strings.Join(lines[len(lines)-maxBufferLines:], "\n") 447 textView.Clear() 448 fmt.Fprint(textView, newContent) 449 } 450 textView.ScrollToEnd() 451 }) 452 } 453 } 454 455 func parseShardArg(shard string) (int, int, error) { 456 if shard == "" { 457 return 1, 1, nil 458 } 459 460 parts := strings.Split(shard, "/") 461 if len(parts) != 2 { 462 return 0, 0, fmt.Errorf("invalid shard format (expected n/total)") 463 } 464 465 shardNum, err := strconv.Atoi(parts[0]) 466 if err != nil { 467 return 0, 0, fmt.Errorf("invalid shard number: %v", err) 468 } 469 470 totalShards, err := strconv.Atoi(parts[1]) 471 if err != nil { 472 return 0, 0, fmt.Errorf("invalid total shards: %v", err) 473 } 474 475 if shardNum < 1 || shardNum > totalShards { 476 return 0, 0, fmt.Errorf("shard number must be between 1 and total shards") 477 } 478 479 return shardNum, totalShards, nil 480 } 481 482 func (c *Config) updateDNSServers() error { 483 c.updateMu.Lock() 484 defer c.updateMu.Unlock() 485 486 if time.Since(c.lastDNSUpdate) < 24*time.Hour { 487 return nil 488 } 489 490 resolvers, err := fetchDefaultResolvers() 491 if err != nil { 492 return err 493 } 494 495 if len(resolvers) == 0 { 496 return fmt.Errorf("no resolvers found in update") 497 } 498 499 for i, server := range resolvers { 500 if !strings.Contains(server, ":") { 501 resolvers[i] = server + ":53" 502 } 503 } 504 505 c.mu.Lock() 506 c.dnsServers = resolvers 507 c.serverIndex = 0 508 c.lastDNSUpdate = time.Now() 509 c.mu.Unlock() 510 511 return nil 512 } 513 514 func main() { 515 concurrency := flag.Int("c", 100, "Concurrency level") 516 timeout := flag.Duration("t", 2*time.Second, "Timeout for DNS queries") 517 retries := flag.Int("r", 2, "Number of retries for failed lookups") 518 dnsFile := flag.String("dns", "", "File containing DNS servers (one per line)") 519 debug := flag.Bool("debug", false, "Show unsuccessful lookups") 520 outputPath := flag.String("o", "", "Path to NDJSON output file") 521 seed := flag.Int64("s", 0, "Seed for IP generation (0 for random)") 522 shard := flag.String("shard", "", "Shard specification (e.g., 1/4 for first shard of 4)") 523 loop := flag.Bool("l", false, "Loop continuously after completion") 524 jsonOutput := flag.Bool("j", false, "Output NDJSON to stdout (no TUI)") 525 flag.Parse() 526 527 shardNum, totalShards, err := parseShardArg(*shard) 528 if err != nil { 529 fmt.Printf("Error parsing shard argument: %v\n", err) 530 return 531 } 532 533 if *seed == 0 { 534 *seed = time.Now().UnixNano() 535 } 536 537 servers, err := loadDNSServers(*dnsFile) 538 if err != nil { 539 fmt.Printf("Error loading DNS servers: %v\n", err) 540 return 541 } 542 543 for i, server := range servers { 544 if !strings.Contains(server, ":") { 545 servers[i] = server + ":53" 546 } 547 } 548 549 cfg := &Config{ 550 concurrency: *concurrency, 551 timeout: *timeout, 552 retries: *retries, 553 debug: *debug, 554 dnsServers: servers, 555 lastDNSUpdate: time.Now(), 556 loop: *loop, 557 } 558 559 if *outputPath != "" { 560 f, err := os.OpenFile(*outputPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 561 if err != nil { 562 fmt.Printf("Error opening output file: %v\n", err) 563 return 564 } 565 cfg.outputFile = f 566 defer f.Close() 567 } 568 569 app := tview.NewApplication() 570 571 textView := tview.NewTextView(). 572 SetDynamicColors(true). 573 SetScrollable(true). 574 SetChangedFunc(func() { 575 app.Draw() 576 }) 577 textView.SetBorder(true).SetTitle(" PTR Records ") 578 579 progress := tview.NewTextView(). 580 SetDynamicColors(true). 581 SetTextAlign(tview.AlignLeft) 582 progress.SetBorder(true).SetTitle(" Progress ") 583 584 flex := tview.NewFlex(). 585 SetDirection(tview.FlexRow). 586 AddItem(textView, 0, 1, false). 587 AddItem(progress, 4, 0, false) 588 589 stats := &Stats{ 590 total: 1 << 32, 591 lastCheckTime: time.Now(), 592 startTime: time.Now(), 593 } 594 595 go func() { 596 const movingAverageWindow = 5 597 stats.speedHistory = make([]float64, 0, movingAverageWindow) 598 stats.lastCheckTime = time.Now() 599 600 for { 601 processed := atomic.LoadUint64(&stats.processed) 602 success := atomic.LoadUint64(&stats.success) 603 failed := atomic.LoadUint64(&stats.failed) 604 605 now := time.Now() 606 duration := now.Sub(stats.lastCheckTime).Seconds() 607 608 if duration >= 1.0 { 609 stats.mu.Lock() 610 speed := float64(processed-stats.lastProcessed) / duration 611 stats.speedHistory = append(stats.speedHistory, speed) 612 if len(stats.speedHistory) > movingAverageWindow { 613 stats.speedHistory = stats.speedHistory[1:] 614 } 615 616 var avgSpeed float64 617 for _, s := range stats.speedHistory { 618 avgSpeed += s 619 } 620 avgSpeed /= float64(len(stats.speedHistory)) 621 622 stats.lastProcessed = processed 623 stats.lastCheckTime = now 624 stats.mu.Unlock() 625 626 percent := float64(processed) / float64(stats.total) * 100 627 628 app.QueueUpdateDraw(func() { 629 var width int 630 _, _, width, _ = progress.GetInnerRect() 631 if width <= 0 { 632 return 633 } 634 635 // First line: stats 636 statsLine := fmt.Sprintf(" [aqua]Elapsed:[:-] [white]%s [gray]│[-] [aqua]Round:[:-] [white]%d [gray]│[-] [aqua]Count:[:-] [white]%s [gray]│[-] [aqua]Progress:[:-] [darkgray]%.2f%%[-] [gray]│[-] [aqua]Rate:[:-] %s [gray]│[-] [aqua]CNAMEs:[:-] [yellow]%s[-][darkgray] (%.1f%%)[-] [gray]│[-] [aqua]Successful:[:-] [green]✓ %s[-][darkgray] (%.1f%%)[-] [gray]│[-] [aqua]Failed:[:-] [red]✗ %s[-][darkgray] (%.1f%%)[-]\n", 637 formatDuration(time.Since(stats.startTime)), 638 atomic.LoadUint64(&stats.round)+1, 639 formatNumber(processed), 640 percent, 641 colorizeSpeed(avgSpeed), 642 formatNumber(atomic.LoadUint64(&stats.cnames)), 643 float64(atomic.LoadUint64(&stats.cnames))/float64(processed)*100, 644 formatNumber(success), 645 float64(success)/float64(processed)*100, 646 formatNumber(failed), 647 float64(failed)/float64(processed)*100) 648 649 // Second line: progress bar 650 barWidth := width - 3 // -3 for the [] and space 651 if barWidth < 1 { 652 progress.Clear() 653 fmt.Fprint(progress, statsLine) 654 return 655 } 656 657 filled := int(float64(barWidth) * (percent / 100)) 658 if filled > barWidth { 659 filled = barWidth 660 } 661 662 barLine := fmt.Sprintf(" [%s%s]", 663 strings.Repeat("█", filled), 664 strings.Repeat("░", barWidth-filled)) 665 666 // Combine both lines with explicit newline 667 progress.Clear() 668 fmt.Fprintf(progress, "%s%s", statsLine, barLine) 669 }) 670 } 671 672 time.Sleep(100 * time.Millisecond) 673 } 674 }() 675 676 if *jsonOutput { 677 // JSON-only mode 678 jobs := make(chan string, cfg.concurrency) 679 var wg sync.WaitGroup 680 681 // Start workers 682 for i := 0; i < cfg.concurrency; i++ { 683 wg.Add(1) 684 go func() { 685 defer wg.Done() 686 for ip := range jobs { 687 var response DNSResponse 688 var err error 689 var server string 690 691 if len(cfg.dnsServers) > 0 { 692 response, server, err = lookupWithRetry(ip, cfg) 693 if idx := strings.Index(server, ":"); idx != -1 { 694 server = server[:idx] 695 } 696 } else { 697 names, err := net.LookupAddr(ip) 698 if err == nil { 699 response = DNSResponse{Names: names, RecordType: "PTR"} 700 } 701 } 702 703 if err != nil { 704 if cfg.debug { 705 errRecord := formatErrorAsHostname(err) 706 record := struct { 707 Seen string `json:"seen"` 708 IP string `json:"ip"` 709 Nameserver string `json:"nameserver"` 710 Record string `json:"record"` 711 RecordType string `json:"record_type"` 712 TTL uint32 `json:"ttl"` 713 }{ 714 Seen: time.Now().Format(time.RFC3339), 715 IP: ip, 716 Nameserver: server, 717 Record: errRecord, 718 RecordType: "ERR", 719 TTL: 0, 720 } 721 if data, err := json.Marshal(record); err == nil { 722 fmt.Println(string(data)) 723 } 724 } 725 continue 726 } 727 728 if len(response.Names) == 0 { 729 if cfg.debug { 730 record := struct { 731 Seen string `json:"seen"` 732 IP string `json:"ip"` 733 Nameserver string `json:"nameserver"` 734 Record string `json:"record"` 735 RecordType string `json:"record_type"` 736 TTL uint32 `json:"ttl"` 737 }{ 738 Seen: time.Now().Format(time.RFC3339), 739 IP: ip, 740 Nameserver: server, 741 Record: "FAIL.NO-PTR-RECORD.in-addr.arpa", 742 RecordType: "ERR", 743 TTL: 0, 744 } 745 if data, err := json.Marshal(record); err == nil { 746 fmt.Println(string(data)) 747 } 748 } 749 continue 750 } 751 752 ptr := "" 753 for _, name := range response.Names { 754 if cleaned := strings.TrimSpace(strings.TrimSuffix(name, ".")); cleaned != "" { 755 ptr = cleaned 756 break 757 } 758 } 759 760 if ptr == "" { 761 continue 762 } 763 764 record := struct { 765 Seen string `json:"seen"` 766 IP string `json:"ip"` 767 Nameserver string `json:"nameserver"` 768 Record string `json:"record"` 769 RecordType string `json:"record_type"` 770 TTL uint32 `json:"ttl"` 771 }{ 772 Seen: time.Now().Format(time.RFC3339), 773 IP: ip, 774 Nameserver: server, 775 Record: response.Target, 776 RecordType: response.RecordType, 777 TTL: response.TTL, 778 } 779 780 if response.RecordType != "CNAME" { 781 record.Record = ptr 782 } 783 784 if data, err := json.Marshal(record); err == nil { 785 fmt.Println(string(data)) 786 } 787 } 788 }() 789 } 790 791 // Feed IPs to workers 792 for { 793 stream, err := golcg.IPStream("0.0.0.0/0", shardNum, totalShards, int(*seed), nil) 794 if err != nil { 795 fmt.Fprintf(os.Stderr, "Error creating IP stream: %v\n", err) 796 return 797 } 798 799 for ip := range stream { 800 jobs <- ip 801 } 802 803 if !cfg.loop { 804 break 805 } 806 } 807 close(jobs) 808 wg.Wait() 809 return 810 } 811 812 jobs := make(chan string, cfg.concurrency) 813 814 go func() { 815 for { 816 stream, err := golcg.IPStream("0.0.0.0/0", shardNum, totalShards, int(*seed), nil) 817 if err != nil { 818 fmt.Printf("Error creating IP stream: %v\n", err) 819 return 820 } 821 822 for ip := range stream { 823 jobs <- ip 824 } 825 826 if !cfg.loop { 827 break 828 } 829 } 830 close(jobs) 831 }() 832 833 var wg sync.WaitGroup 834 for i := 0; i < cfg.concurrency; i++ { 835 wg.Add(1) 836 go worker(jobs, &wg, cfg, stats, textView, app) 837 } 838 839 go func() { 840 wg.Wait() 841 app.Stop() 842 }() 843 844 if err := app.SetRoot(flex, true).EnableMouse(true).Run(); err != nil { 845 panic(err) 846 } 847 } 848 849 func formatNumber(n uint64) string { 850 s := fmt.Sprint(n) 851 parts := make([]string, 0) 852 for i := len(s); i > 0; i -= 3 { 853 start := i - 3 854 if start < 0 { 855 start = 0 856 } 857 parts = append([]string{s[start:i]}, parts...) 858 } 859 return strings.Join(parts, ",") 860 } 861 862 func colorizeSpeed(speed float64) string { 863 switch { 864 case speed >= 500: 865 return fmt.Sprintf("[green]%5.0f/s[-]", speed) 866 case speed >= 350: 867 return fmt.Sprintf("[yellow]%5.0f/s[-]", speed) 868 case speed >= 200: 869 return fmt.Sprintf("[orange]%5.0f/s[-]", speed) 870 case speed >= 100: 871 return fmt.Sprintf("[red]%5.0f/s[-]", speed) 872 default: 873 return fmt.Sprintf("[gray]%5.0f/s[-]", speed) 874 } 875 } 876 877 func visibleLength(s string) int { 878 noColors := regexp.MustCompile(`\[[a-zA-Z:-]*\]`).ReplaceAllString(s, "") 879 return len(noColors) 880 } 881 882 func writeNDJSON(cfg *Config, timestamp time.Time, ip, server, ptr, recordType, target string, ttl uint32) { 883 if cfg.outputFile == nil { 884 return 885 } 886 887 record := struct { 888 Seen string `json:"seen"` 889 IP string `json:"ip"` 890 Nameserver string `json:"nameserver"` 891 Record string `json:"record"` 892 RecordType string `json:"record_type"` 893 TTL uint32 `json:"ttl"` 894 }{ 895 Seen: timestamp.Format(time.RFC3339), 896 IP: ip, 897 Nameserver: server, 898 Record: target, // For CNAME records, use the target 899 RecordType: recordType, 900 TTL: ttl, 901 } 902 903 // If it's not a CNAME, use the PTR record 904 if recordType != "CNAME" { 905 record.Record = ptr 906 } 907 908 if data, err := json.Marshal(record); err == nil { 909 cfg.mu.Lock() 910 cfg.outputFile.Write(data) 911 cfg.outputFile.Write([]byte("\n")) 912 cfg.mu.Unlock() 913 } 914 } 915 916 func formatDuration(d time.Duration) string { 917 d = d.Round(time.Second) 918 919 days := d / (24 * time.Hour) 920 d -= days * 24 * time.Hour 921 922 hours := d / time.Hour 923 d -= hours * time.Hour 924 925 minutes := d / time.Minute 926 d -= minutes * time.Minute 927 928 seconds := d / time.Second 929 930 var result string 931 932 if days > 0 { 933 if hours > 0 && minutes > 0 { 934 result = fmt.Sprintf("%dd %dh %dm", days, hours, minutes) 935 } else if hours > 0 { 936 result = fmt.Sprintf("%dd %dh", days, hours) 937 } else { 938 result = fmt.Sprintf("%dd", days) 939 } 940 } else if hours > 0 { 941 if minutes > 0 { 942 result = fmt.Sprintf("%dh %dm", hours, minutes) 943 } else { 944 result = fmt.Sprintf("%dh", hours) 945 } 946 } else if minutes > 0 { 947 if seconds > 0 { 948 result = fmt.Sprintf("%dm %ds", minutes, seconds) 949 } else { 950 result = fmt.Sprintf("%dm", minutes) 951 } 952 } else { 953 result = fmt.Sprintf("%ds", seconds) 954 } 955 956 return result 957 } 958 959 func colorizeTTL(ttl uint32) string { 960 switch { 961 case ttl >= 86400: // 1 day or more 962 return fmt.Sprintf("[#00FF00::b]%-6d[-]", ttl) // Bright green with bold 963 case ttl >= 3600: // 1 hour or more 964 return fmt.Sprintf("[yellow]%-6d[-]", ttl) 965 case ttl >= 300: // 5 minutes or more 966 return fmt.Sprintf("[orange]%-6d[-]", ttl) 967 case ttl >= 60: // 1 minute or more 968 return fmt.Sprintf("[red]%-6d[-]", ttl) 969 default: // Less than 60 seconds 970 return fmt.Sprintf("[gray]%-6d[-]", ttl) 971 } 972 } 973 974 func formatErrorAsHostname(err error) string { 975 errMsg := err.Error() 976 if idx := strings.LastIndex(errMsg, ": "); idx != -1 { 977 errMsg = errMsg[idx+2:] 978 } 979 980 switch { 981 case strings.Contains(errMsg, "i/o timeout"): 982 return "FAIL.TIMEOUT.in-addr.arpa" 983 case strings.Contains(errMsg, "Server Failure"): 984 return "FAIL.SERVER-FAILURE.in-addr.arpa" 985 case strings.Contains(errMsg, "No Such Domain"): 986 return "FAIL.NON-AUTHORITATIVE.in-addr.arpa" 987 case strings.Contains(errMsg, "refused"): 988 return "FAIL.REFUSED.in-addr.arpa" 989 case strings.Contains(errMsg, "no such host"): 990 return "FAIL.NO-SUCH-HOST.in-addr.arpa" 991 case strings.Contains(errMsg, "connection refused"): 992 return "FAIL.CONNECTION-REFUSED.in-addr.arpa" 993 case strings.Contains(errMsg, "network is unreachable"): 994 return "FAIL.NETWORK-UNREACHABLE.in-addr.arpa" 995 case strings.Contains(errMsg, "no route to host"): 996 return "FAIL.NO-ROUTE.in-addr.arpa" 997 case strings.Contains(errMsg, "Format error"): 998 return "FAIL.FORMAT-ERROR.in-addr.arpa" 999 case strings.Contains(errMsg, "Not Implemented"): 1000 return "FAIL.NOT-IMPLEMENTED.in-addr.arpa" 1001 case strings.Contains(errMsg, "truncated"): 1002 return "FAIL.TRUNCATED.in-addr.arpa" 1003 default: 1004 return fmt.Sprintf("FAIL.%s.in-addr.arpa", strings.ReplaceAll(strings.ToUpper(errMsg), " ", "-")) 1005 } 1006 }