diff --git a/README.md b/README.md index 35ef2b4..35bb5f1 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,23 @@ An open-source, cross-platform powerful network analysis tool for discovering websites hosted on specific IP addresses and ASN ranges. ## Features -- ASN scanning (Autonomous System Number) + +- ASN scanning (Autonomous System Number) with IPv4/IPv6 support - IP block scanning (CIDR format) -- HTTPS/HTTP support -- DNS resolution +- HTTPS/HTTP automatic fallback +- Firewall bypass techniques (IP shuffling, header randomization, jitter) +- Proxy support (HTTP/HTTPS/SOCKS5) +- Custom DNS servers +- Rate limiting (token bucket algorithm) +- Dynamic timeout calculation - Text and JSON output formats - Configurable concurrent workers (1-1000) - Real-time progress bar -- Graceful interrupt handling with result export +- Graceful Ctrl+C handling with result export ## Installation -Download the latest version from [Releases](https://github.com/sercanarga/ipmap/releases) and run: +Download the latest version from [Releases](https://github.com/lordixir/ipmap/releases) and run: ```bash unzip ipmap.zip @@ -25,23 +30,27 @@ chmod +x ipmap ## Usage ### Parameters + ```bash -asn AS13335 # Scan all IP blocks in the ASN -ip 103.21.244.0/22 # Scan specified IP blocks -d example.com # Search for specific domain --t 200 # Request timeout in milliseconds +-t 2000 # Request timeout in milliseconds (auto-calculated if not set) --export # Auto-export results -format json # Output format (text or json) --workers 100 # Number of concurrent workers +-workers 100 # Number of concurrent workers (default: 100) -v # Verbose mode -c # Continue scanning until completion +-proxy http://127.0.0.1:8080 # Proxy URL (HTTP/HTTPS/SOCKS5) +-rate 50 # Rate limit (requests/second, 0 = unlimited) +-dns 8.8.8.8,1.1.1.1 # Custom DNS servers ``` ### Examples -**Scan ASN:** +**Basic ASN scan (auto timeout):** ```bash -ipmap -asn AS13335 -t 300 +ipmap -asn AS13335 ``` **Find domain in ASN:** @@ -51,12 +60,7 @@ ipmap -asn AS13335 -d example.com **Scan IP blocks:** ```bash -ipmap -ip 103.21.244.0/22,103.22.200.0/22 -t 300 -``` - -**Export results:** -```bash -ipmap -asn AS13335 -d example.com --export +ipmap -ip 103.21.244.0/22,103.22.200.0/22 ``` **High-performance scan:** @@ -64,57 +68,67 @@ ipmap -asn AS13335 -d example.com --export ipmap -asn AS13335 -workers 200 -v ``` -## Proxy Usage - -ipmap supports HTTP, HTTPS, and SOCKS5 proxies for anonymous scanning and bypassing network restrictions. +**Export results:** +```bash +ipmap -asn AS13335 -d example.com --export +``` -### Proxy Parameters +**JSON output:** ```bash --proxy http://127.0.0.1:8080 # HTTP proxy --proxy https://127.0.0.1:8080 # HTTPS proxy --proxy socks5://127.0.0.1:1080 # SOCKS5 proxy --rate 50 # Rate limit (requests/second) --dns 8.8.8.8,1.1.1.1 # Custom DNS servers +ipmap -asn AS13335 -format json --export ``` -### Proxy Examples +## Proxy & Rate Limiting + +ipmap supports HTTP, HTTPS, and SOCKS5 proxies for anonymous scanning. -**Basic HTTP proxy:** +**HTTP proxy:** ```bash ipmap -asn AS13335 -proxy http://127.0.0.1:8080 ``` -**SOCKS5 proxy with Tor:** +**SOCKS5 proxy (Tor):** ```bash ipmap -asn AS13335 -proxy socks5://127.0.0.1:9050 ``` -**Proxy with authentication:** +**Proxy with auth:** ```bash -ipmap -asn AS13335 -proxy http://user:password@proxy.example.com:8080 +ipmap -asn AS13335 -proxy http://user:pass@proxy.com:8080 ``` -**Proxy with rate limiting:** +**Rate limiting:** ```bash -ipmap -asn AS13335 -proxy http://127.0.0.1:8080 -rate 50 +ipmap -asn AS13335 -rate 50 -workers 50 ``` -**Proxy with custom DNS:** +**Full configuration:** ```bash -ipmap -asn AS13335 -proxy socks5://127.0.0.1:1080 -dns 8.8.8.8,1.1.1.1 +ipmap -asn AS13335 -d example.com -proxy http://127.0.0.1:8080 -rate 100 -workers 50 -dns 8.8.8.8 -v --export ``` -**Full configuration example:** -```bash -ipmap -asn AS13335 -d example.com -proxy http://127.0.0.1:8080 -rate 100 -workers 50 -v --export -``` +> **Note:** When using proxies, reduce worker count and enable rate limiting to avoid overwhelming the proxy. + +## Firewall Bypass Features -> **Note:** When using proxies, consider reducing the worker count (`-workers`) and enabling rate limiting (`-rate`) to avoid overwhelming the proxy server. +ipmap includes built-in firewall bypass techniques: + +- **IP Shuffling:** Randomizes scan order to avoid sequential pattern detection +- **Header Randomization:** Rotates User-Agent, Accept-Language, Chrome versions, platforms +- **Request Jitter:** Adds random 0-50ms delay between requests +- **Dynamic Timeout:** Auto-adjusts timeout based on worker count + +## Interrupt Handling (Ctrl+C) + +Press Ctrl+C during scan to: +1. Immediately stop all scanning +2. View found results count +3. Option to export partial results ## Building ```bash -git clone https://github.com/sercanarga/ipmap.git +git clone https://github.com/lordixir/ipmap.git cd ipmap go build -o ipmap . ``` @@ -125,6 +139,19 @@ go build -o ipmap . go test ./... -v ``` +## Changelog (v2.0) + +- ✅ Added IP shuffling for firewall bypass +- ✅ Added request jitter (0-50ms random delay) +- ✅ Added header randomization (language, chrome version, platform) +- ✅ Fixed Ctrl+C interrupt handling (immediate stop) +- ✅ Added dynamic timeout calculation based on workers +- ✅ Added IPv6 support for ASN scanning +- ✅ Improved error logging +- ✅ Fixed result collection bug with high workers +- ✅ Removed gzip to fix response parsing +- ✅ Added scan statistics at completion + ## License This project is open-source and available under the MIT License. diff --git a/main.go b/main.go index ee3107a..8b7118f 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" "syscall" + "time" ) var ( @@ -46,7 +47,7 @@ func main() { } // Setup interrupt handler - interruptData = &modules.InterruptData{} + interruptData = modules.NewInterruptData() setupInterruptHandler() // Log configuration if verbose @@ -84,9 +85,19 @@ func main() { return } + // Set default timeout if not specified and no domain to calculate from if *timeout == 0 && *domain == "" { - fmt.Println("Timeout parameter( -t ) is not set. By entering the domain, you can have it calculated automatically.") - return + // Base timeout: 2000ms, scale up for high worker counts + baseTimeout := 2000 + if *workers > 200 { + // Add extra time for high concurrency (network saturation) + baseTimeout = baseTimeout + (*workers / 100 * 500) + } + if baseTimeout > 10000 { + baseTimeout = 10000 // Max 10 seconds + } + *timeout = baseTimeout + config.InfoLog("Using auto-calculated timeout: %dms (workers: %d)", *timeout, *workers) } if *domain != "" { @@ -130,7 +141,15 @@ func setupInterruptHandler() { go func() { <-sigChan - fmt.Println("\n\n[!] Scan interrupted by user") + fmt.Println("\n\n[!] Scan interrupted by user - stopping...") + + // Signal all goroutines to stop immediately + if interruptData != nil { + interruptData.Cancel() + } + + // Give goroutines a moment to stop + time.Sleep(100 * time.Millisecond) if interruptData != nil && len(interruptData.Websites) > 0 { fmt.Printf("\n[*] Found %d websites before interruption\n", len(interruptData.Websites)) diff --git a/modules/interrupt_handler.go b/modules/interrupt_handler.go index 7361d28..037a24f 100644 --- a/modules/interrupt_handler.go +++ b/modules/interrupt_handler.go @@ -4,11 +4,43 @@ import "sync" // InterruptData holds scan data for interrupt handling type InterruptData struct { - Websites [][]string - IPBlocks []string - Domain string - Timeout int - mu sync.Mutex + Websites [][]string + IPBlocks []string + Domain string + Timeout int + Cancelled bool // Flag to indicate cancellation + CancelCh chan struct{} // Channel to signal cancellation + mu sync.Mutex +} + +// NewInterruptData creates a new InterruptData with initialized cancel channel +func NewInterruptData() *InterruptData { + return &InterruptData{ + CancelCh: make(chan struct{}), + } +} + +// Cancel signals all goroutines to stop +func (id *InterruptData) Cancel() { + if id == nil { + return + } + id.mu.Lock() + defer id.mu.Unlock() + if !id.Cancelled { + id.Cancelled = true + close(id.CancelCh) + } +} + +// IsCancelled returns whether the scan has been cancelled +func (id *InterruptData) IsCancelled() bool { + if id == nil { + return false + } + id.mu.Lock() + defer id.mu.Unlock() + return id.Cancelled } // AddWebsite safely adds a website to the interrupt data diff --git a/modules/request.go b/modules/request.go index 7eab349..705a04b 100644 --- a/modules/request.go +++ b/modules/request.go @@ -125,6 +125,16 @@ func createDialContext() func(ctx context.Context, network, addr string) (net.Co } func createHTTPClientWithConfig() *http.Client { + // Calculate connection pool size based on worker count + maxConns := config.Workers + if maxConns < 100 { + maxConns = 100 + } + maxConnsPerHost := maxConns / 10 + if maxConnsPerHost < 10 { + maxConnsPerHost = 10 + } + transport := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, @@ -138,14 +148,16 @@ func createHTTPClientWithConfig() *http.Client { tls.TLS_RSA_WITH_AES_128_GCM_SHA256, }, }, - MaxIdleConns: 100, - MaxIdleConnsPerHost: 10, + MaxIdleConns: maxConns, + MaxIdleConnsPerHost: maxConnsPerHost, + MaxConnsPerHost: maxConnsPerHost * 2, // Allow more active connections IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ResponseHeaderTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, DialContext: createDialContext(), ForceAttemptHTTP2: true, + DisableKeepAlives: false, // Keep connections alive for reuse } // Configure proxy if specified @@ -210,8 +222,18 @@ func RequestFuncWithRetry(ip string, url string, timeout int, maxRetries int) [] ua := uarand.GetRandom() req.Header.Set("User-Agent", ua) req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8") - req.Header.Set("Accept-Language", "en-US,en;q=0.9,tr;q=0.8") - req.Header.Set("Accept-Encoding", "gzip, deflate, br") + + // Randomize Accept-Language to avoid fingerprinting + languages := []string{ + "en-US,en;q=0.9", + "en-GB,en;q=0.9", + "en-US,en;q=0.9,tr;q=0.8", + "de-DE,de;q=0.9,en;q=0.8", + "fr-FR,fr;q=0.9,en;q=0.8", + } + req.Header.Set("Accept-Language", languages[time.Now().UnixNano()%int64(len(languages))]) + + req.Header.Set("Accept-Encoding", "identity") // No compression to avoid decompression issues req.Header.Set("Connection", "keep-alive") req.Header.Set("Upgrade-Insecure-Requests", "1") req.Header.Set("Sec-Fetch-Dest", "document") @@ -219,9 +241,20 @@ func RequestFuncWithRetry(ip string, url string, timeout int, maxRetries int) [] req.Header.Set("Sec-Fetch-Site", "none") req.Header.Set("Sec-Fetch-User", "?1") req.Header.Set("Cache-Control", "max-age=0") - req.Header.Set("Sec-Ch-Ua", `"Chromium";v="120", "Not_A Brand";v="24"`) + + // Randomize browser version fingerprint + chromeVersions := []string{ + `"Chromium";v="120", "Not_A Brand";v="24"`, + `"Chromium";v="119", "Not_A Brand";v="24"`, + `"Chromium";v="121", "Not_A Brand";v="24"`, + `"Google Chrome";v="120", "Chromium";v="120"`, + } + req.Header.Set("Sec-Ch-Ua", chromeVersions[time.Now().UnixNano()%int64(len(chromeVersions))]) req.Header.Set("Sec-Ch-Ua-Mobile", "?0") - req.Header.Set("Sec-Ch-Ua-Platform", `"Windows"`) + + // Randomize platform + platforms := []string{`"Windows"`, `"macOS"`, `"Linux"`} + req.Header.Set("Sec-Ch-Ua-Platform", platforms[time.Now().UnixNano()%int64(len(platforms))]) resp, err := GetHTTPClient().Do(req) diff --git a/modules/resolve_site.go b/modules/resolve_site.go index 212a789..8e866a7 100644 --- a/modules/resolve_site.go +++ b/modules/resolve_site.go @@ -3,15 +3,54 @@ package modules import ( "fmt" "ipmap/config" + "math/rand" "sync" + "time" "github.com/schollz/progressbar/v3" ) +// Package-level random generator (initialized once) +var rng = rand.New(rand.NewSource(time.Now().UnixNano())) +var rngMu sync.Mutex + +// ShuffleIPs randomizes the order of IP addresses to avoid sequential scanning patterns +func ShuffleIPs(ips []string) []string { + shuffled := make([]string, len(ips)) + copy(shuffled, ips) + rngMu.Lock() + rng.Shuffle(len(shuffled), func(i, j int) { + shuffled[i], shuffled[j] = shuffled[j], shuffled[i] + }) + rngMu.Unlock() + return shuffled +} + +// AddJitter adds random delay to avoid detection patterns +func AddJitter(maxMs int) { + if maxMs <= 0 { + return + } + rngMu.Lock() + jitter := rng.Intn(maxMs) + rngMu.Unlock() + time.Sleep(time.Duration(jitter) * time.Millisecond) +} + func ResolveSite(IPAddress []string, Websites [][]string, DomainTitle string, IPBlocks []string, domain string, con bool, export bool, timeout int, interruptData *InterruptData) { var wg sync.WaitGroup var mu sync.Mutex + // Use local slice to collect results (fix for slice passed by value issue) + var foundSites [][]string + + // Scan statistics + var scannedCount, foundCount int + + // Shuffle IPs to avoid sequential scanning patterns (firewall bypass) + shuffledIPs := ShuffleIPs(IPAddress) + config.VerboseLog("IP addresses shuffled for firewall bypass") + // Use configurable worker pool size workerCount := config.Workers config.VerboseLog("Starting scan with %d concurrent workers", workerCount) @@ -24,7 +63,7 @@ func ResolveSite(IPAddress []string, Websites [][]string, DomainTitle string, IP } // Create progress bar - bar := progressbar.NewOptions(len(IPAddress), + bar := progressbar.NewOptions(len(shuffledIPs), progressbar.OptionEnableColorCodes(true), progressbar.OptionShowBytes(false), progressbar.OptionShowCount(), @@ -39,7 +78,24 @@ func ResolveSite(IPAddress []string, Websites [][]string, DomainTitle string, IP }), ) - for _, ip := range IPAddress { + // Atomic flag for early exit + var stopped bool + + for _, ip := range shuffledIPs { + // Check if already stopped or cancelled via Ctrl+C + mu.Lock() + if stopped { + mu.Unlock() + break + } + mu.Unlock() + + // Check for interrupt signal + if interruptData != nil && interruptData.IsCancelled() { + config.VerboseLog("Scan cancelled by user") + break + } + wg.Add(1) sem <- struct{}{} @@ -47,15 +103,35 @@ func ResolveSite(IPAddress []string, Websites [][]string, DomainTitle string, IP defer wg.Done() defer func() { <-sem }() + // Check if stopped or cancelled before making request + if interruptData != nil && interruptData.IsCancelled() { + return + } + mu.Lock() + if stopped { + mu.Unlock() + return + } + mu.Unlock() + // Wait for rate limiter before making request rateLimiter.Wait() + // Add random jitter (0-50ms) to avoid detection patterns + AddJitter(50) + site := GetSite(ip, domain, timeout) + + mu.Lock() + scannedCount++ + mu.Unlock() + if len(site) > 0 { fmt.Println("\n", site) mu.Lock() - Websites = append(Websites, site) + foundSites = append(foundSites, site) + foundCount++ mu.Unlock() // Add to interrupt data for Ctrl+C handling @@ -63,9 +139,12 @@ func ResolveSite(IPAddress []string, Websites [][]string, DomainTitle string, IP interruptData.AddWebsite(site) } - if DomainTitle != "" && site[2] == DomainTitle && !con { + if DomainTitle != "" && len(site) > 2 && site[2] == DomainTitle && !con { + mu.Lock() + stopped = true + mu.Unlock() _ = bar.Finish() - PrintResult("Search Domain by ASN", DomainTitle, timeout, IPBlocks, Websites, export) + PrintResult("Search Domain by ASN", DomainTitle, timeout, IPBlocks, foundSites, export) return } } @@ -79,6 +158,15 @@ func ResolveSite(IPAddress []string, Websites [][]string, DomainTitle string, IP wg.Wait() _ = bar.Finish() - // Process and print results - PrintResult("Search All ASN/IP", DomainTitle, timeout, IPBlocks, Websites, export) + // Print scan statistics + fmt.Printf("\n[*] Scan Statistics: %d/%d IPs scanned, %d sites found\n", scannedCount, len(IPAddress), foundCount) + + // Process and print results (only if not already printed) + mu.Lock() + if !stopped { + mu.Unlock() + PrintResult("Search All ASN/IP", DomainTitle, timeout, IPBlocks, foundSites, export) + } else { + mu.Unlock() + } } diff --git a/program_linux_386 b/program_linux_386 deleted file mode 100644 index e651798..0000000 Binary files a/program_linux_386 and /dev/null differ diff --git a/tools/find_asn.go b/tools/find_asn.go index 6ce486d..010db3a 100644 --- a/tools/find_asn.go +++ b/tools/find_asn.go @@ -2,6 +2,7 @@ package tools import ( "fmt" + "ipmap/config" "ipmap/modules" "regexp" "strconv" @@ -14,30 +15,53 @@ func FindASN(asn string, domain string, domainTitle string, con bool, export boo var ipAddress []string var websites [][]string - re := regexp.MustCompile(`(?m)route:\s+([0-9\.\/]+)$`) + // Match both IPv4 (route:) and IPv6 (route6:) blocks + re := regexp.MustCompile(`(?m)route6?:\s+([0-9a-fA-F:\.\/]+)$`) for _, match := range re.FindAllStringSubmatch(modules.FindIPBlocks(asn), -1) { ipBlocks = append(ipBlocks, match[1]) } + if len(ipBlocks) == 0 { + config.ErrorLog("No IP blocks found for ASN: %s", asn) + return + } + for _, block := range ipBlocks { ips, err := modules.CalcIPAddress(block) if err != nil { - return + config.ErrorLog("Failed to parse CIDR block '%s': %v", block, err) + continue // Continue with other blocks instead of returning } ipAddress = append(ipAddress, ips...) } + if len(ipAddress) == 0 { + config.ErrorLog("No valid IP addresses to scan") + return + } + // Update interrupt data with IP blocks if interruptData != nil { interruptData.IPBlocks = ipBlocks } + // Calculate estimated end time based on workers (parallel processing) + workerCount := config.Workers + if workerCount <= 0 { + workerCount = 100 + } + estimatedSeconds := (len(ipAddress) / workerCount) * timeout / 1000 + if estimatedSeconds < 1 { + estimatedSeconds = 1 + } + fmt.Println("ASN: " + asn + "\nIP Block: " + strconv.Itoa(len(ipBlocks)) + "\nIP Address: " + strconv.Itoa(len(ipAddress)) + - "\nStart Time: " + time.Now().Local().String() + - "\nEnd Time: " + time.Now().Add((time.Millisecond*time.Duration(timeout))*time.Duration(len(ipAddress))).Local().String()) + "\nWorkers: " + strconv.Itoa(workerCount) + + "\nStart Time: " + time.Now().Local().Format("2006-01-02 15:04:05") + + "\nEst. End: " + time.Now().Add(time.Duration(estimatedSeconds)*time.Second).Local().Format("2006-01-02 15:04:05")) modules.ResolveSite(ipAddress, websites, domainTitle, ipBlocks, domain, con, export, timeout, interruptData) } diff --git a/tools/find_ip.go b/tools/find_ip.go index bbacf08..9baab5e 100644 --- a/tools/find_ip.go +++ b/tools/find_ip.go @@ -2,6 +2,7 @@ package tools import ( "fmt" + "ipmap/config" "ipmap/modules" "strconv" "time" @@ -15,16 +16,33 @@ func FindIP(ipBlocks []string, domain string, domainTitle string, con bool, expo for _, block := range ipBlocks { ips, err := modules.CalcIPAddress(block) if err != nil { - return + config.ErrorLog("Failed to parse CIDR block '%s': %v", block, err) + continue // Continue with other blocks instead of returning } ipAddress = append(ipAddress, ips...) } + if len(ipAddress) == 0 { + config.ErrorLog("No valid IP addresses to scan") + return + } + + // Calculate estimated end time based on workers (parallel processing) + workerCount := config.Workers + if workerCount <= 0 { + workerCount = 100 + } + estimatedSeconds := (len(ipAddress) / workerCount) * timeout / 1000 + if estimatedSeconds < 1 { + estimatedSeconds = 1 + } + fmt.Println("IP Block: " + strconv.Itoa(len(ipBlocks)) + "\nIP Address: " + strconv.Itoa(len(ipAddress)) + - "\nStart Time: " + time.Now().Local().String() + - "\nEnd Time: " + time.Now().Add((time.Millisecond*time.Duration(timeout))*time.Duration(len(ipAddress))).Local().String()) + "\nWorkers: " + strconv.Itoa(workerCount) + + "\nStart Time: " + time.Now().Local().Format("2006-01-02 15:04:05") + + "\nEst. End: " + time.Now().Add(time.Duration(estimatedSeconds)*time.Second).Local().Format("2006-01-02 15:04:05")) modules.ResolveSite(ipAddress, websites, domainTitle, ipBlocks, domain, con, export, timeout, interruptData) }