Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ func main() {

// Collect results
var workingDNS []string
var suspiciousCount int
resultLoop:
for {
select {
Expand All @@ -254,6 +255,9 @@ resultLoop:
if !ok {
break resultLoop
}
if result.Suspicious {
suspiciousCount++
}
if result.Working {
workingDNS = append(workingDNS, result.IP)
}
Expand All @@ -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
Expand Down
49 changes: 41 additions & 8 deletions scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/rand"
"encoding/base32"
"encoding/hex"
"net"
"sort"
"sync"
"sync/atomic"
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down