Improved DNS handling, pull servers from trickest when none supplied, added CNAME handling, updated preview picture

This commit is contained in:
Dionysus 2025-01-05 03:51:45 -05:00
parent bd6b92f179
commit 6eab4a29f4
Signed by: acidvegas
GPG Key ID: EF4B922DB85DC9DE
2 changed files with 213 additions and 92 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

@ -7,6 +7,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"net" "net"
"net/http"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@ -19,6 +20,8 @@ import (
"github.com/rivo/tview" "github.com/rivo/tview"
) )
const defaultResolversURL = "https://raw.githubusercontent.com/trickest/resolvers/refs/heads/main/resolvers.txt"
type Config struct { type Config struct {
concurrency int concurrency int
timeout time.Duration timeout time.Duration
@ -28,6 +31,8 @@ type Config struct {
debug bool debug bool
outputFile *os.File outputFile *os.File
mu sync.Mutex mu sync.Mutex
lastDNSUpdate time.Time
updateMu sync.Mutex
} }
type Stats struct { type Stats struct {
@ -37,6 +42,7 @@ type Stats struct {
lastCheckTime time.Time lastCheckTime time.Time
success uint64 success uint64
failed uint64 failed uint64
cnames uint64
speedHistory []float64 speedHistory []float64
mu sync.Mutex mu sync.Mutex
} }
@ -53,7 +59,15 @@ func (s *Stats) incrementFailed() {
atomic.AddUint64(&s.failed, 1) atomic.AddUint64(&s.failed, 1)
} }
func (s *Stats) incrementCNAME() {
atomic.AddUint64(&s.cnames, 1)
}
func (c *Config) getNextServer() string { func (c *Config) getNextServer() string {
if err := c.updateDNSServers(); err != nil {
fmt.Printf("Failed to update DNS servers: %v\n", err)
}
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -66,40 +80,81 @@ func (c *Config) getNextServer() string {
return server return server
} }
func loadDNSServers(filename string) ([]string, error) { func fetchDefaultResolvers() ([]string, error) {
if filename == "" { resp, err := http.Get(defaultResolversURL)
return nil, nil if err != nil {
return nil, fmt.Errorf("failed to fetch default resolvers: %v", err)
}
defer resp.Body.Close()
var resolvers []string
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
resolver := strings.TrimSpace(scanner.Text())
if resolver != "" {
resolvers = append(resolvers, resolver)
}
} }
file, err := os.Open(filename) if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading default resolvers: %v", err)
}
return resolvers, nil
}
func loadDNSServers(dnsFile string) ([]string, error) {
if dnsFile == "" {
resolvers, err := fetchDefaultResolvers()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(resolvers) == 0 {
return nil, fmt.Errorf("no default resolvers found")
}
return resolvers, nil
}
file, err := os.Open(dnsFile)
if err != nil {
return nil, fmt.Errorf("failed to open DNS servers file: %v", err)
}
defer file.Close() defer file.Close()
var servers []string var servers []string
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
server := strings.TrimSpace(scanner.Text()) server := strings.TrimSpace(scanner.Text())
if server != "" && !strings.HasPrefix(server, "#") { if server != "" {
if !strings.Contains(server, ":") {
server += ":53"
}
servers = append(servers, server) servers = append(servers, server)
} }
} }
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading DNS servers file: %v", err)
}
if len(servers) == 0 { if len(servers) == 0 {
return nil, fmt.Errorf("no valid DNS servers found in file") return nil, fmt.Errorf("no DNS servers found in file")
} }
return servers, scanner.Err() return servers, nil
} }
func lookupWithRetry(ip string, cfg *Config) ([]string, string, error) { type DNSResponse struct {
Names []string
Server string
RecordType string // "PTR" or "CNAME"
Target string // For CNAME records, stores the target
}
func lookupWithRetry(ip string, cfg *Config) (DNSResponse, error) {
var lastErr error
for i := 0; i < cfg.retries; i++ {
server := cfg.getNextServer() server := cfg.getNextServer()
if server == "" { if server == "" {
return nil, "", fmt.Errorf("no DNS servers available") return DNSResponse{}, fmt.Errorf("no DNS servers available")
} }
r := &net.Resolver{ r := &net.Resolver{
@ -112,32 +167,43 @@ func lookupWithRetry(ip string, cfg *Config) ([]string, string, error) {
}, },
} }
for i := 0; i < cfg.retries; i++ {
ctx, cancel := context.WithTimeout(context.Background(), cfg.timeout) ctx, cancel := context.WithTimeout(context.Background(), cfg.timeout)
names, err := r.LookupAddr(ctx, ip) names, err := r.LookupAddr(ctx, ip)
cancel() cancel()
if err == nil { if err == nil {
return names, server, nil logServer := server
if idx := strings.Index(server, ":"); idx != -1 {
logServer = server[:idx]
} }
if i < cfg.retries-1 { // Check if any of the names is a CNAME
server = cfg.getNextServer() for _, name := range names {
if server == "" { ctx, cancel := context.WithTimeout(context.Background(), cfg.timeout)
return nil, "", fmt.Errorf("no more DNS servers available") cname, err := r.LookupCNAME(ctx, strings.TrimSuffix(name, "."))
} cancel()
r = &net.Resolver{
PreferGo: true, if err == nil && cname != name {
Dial: func(ctx context.Context, network, address string) (net.Conn, error) { return DNSResponse{
d := net.Dialer{ Names: names,
Timeout: cfg.timeout, Server: logServer,
} RecordType: "CNAME",
return d.DialContext(ctx, "udp", server) Target: strings.TrimSuffix(cname, "."),
}, }, nil
} }
} }
return DNSResponse{
Names: names,
Server: logServer,
RecordType: "PTR",
}, nil
} }
return nil, "", fmt.Errorf("lookup failed after %d retries", cfg.retries)
lastErr = err
}
return DNSResponse{}, lastErr
} }
func reverse(ss []string) []string { func reverse(ss []string) []string {
@ -201,9 +267,16 @@ func colorizeIPInPtr(ptr, ip string) string {
} }
finalResult := result.String() finalResult := result.String()
finalResult = strings.ReplaceAll(finalResult, ".in-addr.arpa", ".[blue]in-addr.arpa")
finalResult = strings.ReplaceAll(finalResult, ".gov", ".[red]gov") if strings.HasSuffix(finalResult, ".in-addr.arpa") {
finalResult = strings.ReplaceAll(finalResult, ".mil", ".[red]mil") finalResult = finalResult[:len(finalResult)-13] + ".[blue]in-addr.arpa"
}
if strings.HasSuffix(finalResult, ".gov") {
finalResult = finalResult[:len(finalResult)-4] + ".[red]gov"
}
if strings.HasSuffix(finalResult, ".mil") {
finalResult = finalResult[:len(finalResult)-4] + ".[red]mil"
}
return finalResult return finalResult
} }
@ -219,18 +292,17 @@ const maxBufferLines = 1000
func worker(jobs <-chan string, wg *sync.WaitGroup, cfg *Config, stats *Stats, textView *tview.TextView, app *tview.Application) { func worker(jobs <-chan string, wg *sync.WaitGroup, cfg *Config, stats *Stats, textView *tview.TextView, app *tview.Application) {
defer wg.Done() defer wg.Done()
for ip := range jobs { for ip := range jobs {
var names []string
var server string
var err error
timestamp := time.Now() timestamp := time.Now()
var response DNSResponse
var err error
if len(cfg.dnsServers) > 0 { if len(cfg.dnsServers) > 0 {
names, server, err = lookupWithRetry(ip, cfg) response, err = lookupWithRetry(ip, cfg)
if idx := strings.Index(server, ":"); idx != -1 {
server = server[:idx]
}
} else { } else {
names, err = net.LookupAddr(ip) names, err := net.LookupAddr(ip)
if err == nil {
response = DNSResponse{Names: names, RecordType: "PTR"}
}
} }
stats.increment() stats.increment()
@ -255,7 +327,7 @@ func worker(jobs <-chan string, wg *sync.WaitGroup, cfg *Config, stats *Stats, t
continue continue
} }
if len(names) == 0 { if len(response.Names) == 0 {
stats.incrementFailed() stats.incrementFailed()
if cfg.debug { if cfg.debug {
timestamp := time.Now().Format("2006-01-02 15:04:05") timestamp := time.Now().Format("2006-01-02 15:04:05")
@ -273,7 +345,7 @@ func worker(jobs <-chan string, wg *sync.WaitGroup, cfg *Config, stats *Stats, t
stats.incrementSuccess() stats.incrementSuccess()
ptr := "" ptr := ""
for _, name := range names { for _, name := range response.Names {
if cleaned := strings.TrimSpace(strings.TrimSuffix(name, ".")); cleaned != "" { if cleaned := strings.TrimSpace(strings.TrimSuffix(name, ".")); cleaned != "" {
ptr = cleaned ptr = cleaned
break break
@ -284,21 +356,29 @@ func worker(jobs <-chan string, wg *sync.WaitGroup, cfg *Config, stats *Stats, t
continue continue
} }
writeNDJSON(cfg, timestamp, ip, server, ptr) writeNDJSON(cfg, timestamp, ip, response.Server, ptr, response.RecordType, response.Target)
timeStr := time.Now().Format("2006-01-02 15:04:05") timeStr := time.Now().Format("2006-01-02 15:04:05")
recordTypeColor := "[blue] PTR [-]"
if response.RecordType == "CNAME" {
stats.incrementCNAME()
recordTypeColor = "[fuchsia]CNAME[-]"
ptr = fmt.Sprintf("%s -> %s", ptr, response.Target)
}
var line string var line string
if len(cfg.dnsServers) > 0 { if len(cfg.dnsServers) > 0 {
line = fmt.Sprintf("[gray]%s[-] [purple]%15s[-] [gray]│[-] [yellow]%15s[-] [gray]│[-] %s\n", line = fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] [yellow]%-15s[-] [gray]│[-] %-5s [gray]│[-] %s\n",
timeStr, timeStr,
ip, ip,
server, response.Server,
recordTypeColor,
colorizeIPInPtr(ptr, ip)) colorizeIPInPtr(ptr, ip))
} else { } else {
line = fmt.Sprintf("[gray]%s[-] [purple]%15s[-] [gray]│[-] %s\n", line = fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] %-5s [gray]│[-] %s\n",
timeStr, timeStr,
ip, ip,
recordTypeColor,
colorizeIPInPtr(ptr, ip)) colorizeIPInPtr(ptr, ip))
} }
@ -343,6 +423,38 @@ func parseShardArg(shard string) (int, int, error) {
return shardNum, totalShards, nil return shardNum, totalShards, nil
} }
func (c *Config) updateDNSServers() error {
c.updateMu.Lock()
defer c.updateMu.Unlock()
if time.Since(c.lastDNSUpdate) < 24*time.Hour {
return nil
}
resolvers, err := fetchDefaultResolvers()
if err != nil {
return err
}
if len(resolvers) == 0 {
return fmt.Errorf("no resolvers found in update")
}
for i, server := range resolvers {
if !strings.Contains(server, ":") {
resolvers[i] = server + ":53"
}
}
c.mu.Lock()
c.dnsServers = resolvers
c.serverIndex = 0
c.lastDNSUpdate = time.Now()
c.mu.Unlock()
return nil
}
func main() { func main() {
concurrency := flag.Int("c", 100, "Concurrency level") concurrency := flag.Int("c", 100, "Concurrency level")
timeout := flag.Duration("t", 2*time.Second, "Timeout for DNS queries") timeout := flag.Duration("t", 2*time.Second, "Timeout for DNS queries")
@ -364,11 +476,25 @@ func main() {
*seed = time.Now().UnixNano() *seed = time.Now().UnixNano()
} }
servers, err := loadDNSServers(*dnsFile)
if err != nil {
fmt.Printf("Error loading DNS servers: %v\n", err)
return
}
for i, server := range servers {
if !strings.Contains(server, ":") {
servers[i] = server + ":53"
}
}
cfg := &Config{ cfg := &Config{
concurrency: *concurrency, concurrency: *concurrency,
timeout: *timeout, timeout: *timeout,
retries: *retries, retries: *retries,
debug: *debug, debug: *debug,
dnsServers: servers,
lastDNSUpdate: time.Now(),
} }
if *outputPath != "" { if *outputPath != "" {
@ -381,16 +507,6 @@ func main() {
defer f.Close() defer f.Close()
} }
if *dnsFile != "" {
servers, err := loadDNSServers(*dnsFile)
if err != nil {
fmt.Printf("Error loading DNS servers: %v\n", err)
return
}
cfg.dnsServers = servers
fmt.Printf("Loaded %d DNS servers\n", len(servers))
}
app := tview.NewApplication() app := tview.NewApplication()
textView := tview.NewTextView(). textView := tview.NewTextView().
@ -456,10 +572,11 @@ func main() {
return return
} }
statsText := fmt.Sprintf(" [aqua]Count:[:-] [white]%s [gray]│[-] [aqua]Progress:[:-] [darkgray]%7.2f%%[-] [gray]│[-] [aqua]Rate:[:-] %s [gray]│[-] [aqua]Successful:[:-] [green]✓%s [-][darkgray](%5.1f%%)[-] [gray]│[-] [aqua]Failed:[:-] [red]✗%s [-][darkgray](%5.1f%%)[-] ", statsText := fmt.Sprintf(" [aqua]Count:[:-] [white]%s [gray]│[-] [aqua]Progress:[:-] [darkgray]%7.2f%%[-] [gray]│[-] [aqua]Rate:[:-] %s [gray]│[-] [aqua]CNAMEs:[:-] [yellow]%s[-] [gray]│[-] [aqua]Successful:[:-] [green]✓%s [-][darkgray](%5.1f%%)[-] [gray]│[-] [aqua]Failed:[:-] [red]✗%s [-][darkgray](%5.1f%%)[-] ",
formatNumber(processed), formatNumber(processed),
percent, percent,
colorizeSpeed(avgSpeed), colorizeSpeed(avgSpeed),
formatNumber(atomic.LoadUint64(&stats.cnames)),
formatNumber(success), formatNumber(success),
float64(success)/float64(processed)*100, float64(success)/float64(processed)*100,
formatNumber(failed), formatNumber(failed),
@ -568,7 +685,7 @@ func visibleLength(s string) int {
return len(noColors) return len(noColors)
} }
func writeNDJSON(cfg *Config, timestamp time.Time, ip, server, ptr string) { func writeNDJSON(cfg *Config, timestamp time.Time, ip, server, ptr, recordType, target string) {
if cfg.outputFile == nil { if cfg.outputFile == nil {
return return
} }
@ -578,11 +695,15 @@ func writeNDJSON(cfg *Config, timestamp time.Time, ip, server, ptr string) {
IPAddr string `json:"ip_addr"` IPAddr string `json:"ip_addr"`
DNSServer string `json:"dns_server"` DNSServer string `json:"dns_server"`
PTRRecord string `json:"ptr_record"` PTRRecord string `json:"ptr_record"`
RecordType string `json:"record_type"`
Target string `json:"target,omitempty"`
}{ }{
Timestamp: timestamp.Format(time.RFC3339), Timestamp: timestamp.Format(time.RFC3339),
IPAddr: ip, IPAddr: ip,
DNSServer: server, DNSServer: server,
PTRRecord: ptr, PTRRecord: ptr,
RecordType: recordType,
Target: target,
} }
if data, err := json.Marshal(record); err == nil { if data, err := json.Marshal(record); err == nil {