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 }