From d2dc07ecc09a6c1819b3d329e53ffa11882838d9 Mon Sep 17 00:00:00 2001 From: Night Owl Nerd <256460992+nightowlnerd@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:56:46 +0100 Subject: [PATCH] fix(scanner): detect DNS hijacking via private IP responses --- main.go | 7 +++++++ scanner.go | 49 +++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 668c981..6b9b42f 100644 --- a/main.go +++ b/main.go @@ -245,6 +245,7 @@ func main() { // Collect results var workingDNS []string + var suspiciousCount int resultLoop: for { select { @@ -254,6 +255,9 @@ resultLoop: if !ok { break resultLoop } + if result.Suspicious { + suspiciousCount++ + } if result.Working { workingDNS = append(workingDNS, result.IP) } @@ -272,6 +276,9 @@ resultLoop: fmt.Fprintf(os.Stderr, "---\n") fmt.Fprintf(os.Stderr, "Completed: %d IPs in %v\n", scanned, elapsed.Round(time.Millisecond)) fmt.Fprintf(os.Stderr, "Found: %d DNS candidates\n", found) + if suspiciousCount > 0 { + fmt.Fprintf(os.Stderr, "\033[33mWarning: %d servers returned private IPs (possible DNS hijacking)\033[0m\n", suspiciousCount) + } } // Phase 2: Verify with slipstream-client if requested diff --git a/scanner.go b/scanner.go index 7c84d8c..6cc810e 100644 --- a/scanner.go +++ b/scanner.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "encoding/base32" "encoding/hex" + "net" "sort" "sync" "sync/atomic" @@ -15,10 +16,10 @@ import ( // Burst test parameters - tune these for accuracy vs speed tradeoff const ( - BurstQueries = 20 // Number of queries per burst test - BurstConcurrency = 5 // Concurrent queries during burst - BurstMinSuccess = 70 // Minimum success rate % to pass - BurstSubdomainLen = 32 // Bytes for random subdomain (32 = ~52 base32 chars) + BurstQueries = 20 // Number of queries per burst test + BurstConcurrency = 5 // Concurrent queries during burst + BurstMinSuccess = 70 // Minimum success rate % to pass + BurstSubdomainLen = 32 // Bytes for random subdomain (32 = ~52 base32 chars) ) // randomSubdomain generates a random subdomain prefix @@ -37,10 +38,34 @@ func randomSlipstreamSubdomain() string { // ScanResult holds the result of a DNS probe type ScanResult struct { - IP string - Working bool - RTT time.Duration - Error error + IP string + Working bool + Suspicious bool + RTT time.Duration + Error error +} + +// isPrivateIP detects DNS hijacking by checking if response IPs are in reserved ranges +func isPrivateIP(ip net.IP) bool { + if ip == nil { + return false + } + privateRanges := []string{ + "10.0.0.0/8", // Common in corporate/ISP hijacking + "172.16.0.0/12", // Often used by captive portals + "192.168.0.0/16", // Home routers sometimes hijack DNS + "127.0.0.0/8", // Loopback, used to block domains + "169.254.0.0/16", // Link-local, indicates broken resolution + "100.64.0.0/10", // CGNAT, ISP-level interception + "0.0.0.0/8", // Invalid, used to sink traffic + } + for _, cidr := range privateRanges { + _, network, _ := net.ParseCIDR(cidr) + if network.Contains(ip) { + return true + } + } + return false } // Scanner manages the worker pool for DNS probing @@ -121,6 +146,14 @@ func (s *Scanner) Probe(ip string) ScanResult { return ScanResult{IP: ip, Working: false} } + for _, ans := range reply.Answer { + if a, ok := ans.(*dns.A); ok { + if isPrivateIP(a.A) { + return ScanResult{IP: ip, Working: false, Suspicious: true} + } + } + } + // If verify domain is set, check if query reaches our authoritative server // Slipstream uses TXT records exclusively, so test with TXT // Any response (NXDOMAIN, NOERROR, etc.) = query reached server