package scans import ( "log" "net" "os/exec" "runtime" "strconv" "sync" "time" ) type Runner struct { store Store } func NewRunner(store Store) *Runner { return &Runner{store: store} } func (r *Runner) Start(job ScanJob) { go r.run(job) } func (r *Runner) run(job ScanJob) { started := time.Now().UTC() _, _ = r.store.UpdateScan(job.ID, ScanUpdate{ Status: "running", Progress: 0, Stats: ScanStats{}, StartedAt: &started, }) ips, err := expandTargets(job.CIDRs, job.ExcludeIPs) if err != nil { finished := time.Now().UTC() _, _ = r.store.UpdateScan(job.ID, ScanUpdate{ Status: "failed", Progress: 100, Stats: ScanStats{}, FinishedAt: &finished, }) return } total := len(ips) if total == 0 { finished := time.Now().UTC() _, _ = r.store.UpdateScan(job.ID, ScanUpdate{ Status: "done", Progress: 100, Stats: ScanStats{HostsTotal: 0, HostsUp: 0}, FinishedAt: &finished, }) return } maxWorkers := job.Options.MaxParallelHosts if maxWorkers <= 0 { maxWorkers = 64 } var mu sync.Mutex var done int var up int workCh := make(chan string) wg := sync.WaitGroup{} worker := func() { defer wg.Done() for ip := range workCh { isUp := probeHost(ip, job.Options.PingTimeoutMS) _ = r.store.SaveHostResult(job.ID, HostResult{ IP: ip, IsUp: isUp, CheckedAt: time.Now().UTC(), }) mu.Lock() done++ if isUp { up++ } progress := done * 100 / total stats := ScanStats{HostsTotal: total, HostsUp: up} mu.Unlock() _, _ = r.store.UpdateScan(job.ID, ScanUpdate{ Status: "running", Progress: progress, Stats: stats, }) } } for i := 0; i < maxWorkers; i++ { wg.Add(1) go worker() } for _, ip := range ips { workCh <- ip } close(workCh) wg.Wait() finished := time.Now().UTC() _, _ = r.store.UpdateScan(job.ID, ScanUpdate{ Status: "done", Progress: 100, Stats: ScanStats{HostsTotal: total, HostsUp: up}, FinishedAt: &finished, }) } func expandTargets(cidrs []string, excludeIPs []string) ([]string, error) { excluded := make(map[string]struct{}, len(excludeIPs)) for _, ip := range excludeIPs { excluded[ip] = struct{}{} } seen := make(map[string]struct{}) out := make([]string, 0, 256) for _, c := range cidrs { ip, ipnet, err := net.ParseCIDR(c) if err != nil { return nil, err } start := ip.Mask(ipnet.Mask).To4() if start == nil { continue } for current := dupIP(start); ipnet.Contains(current); incIP(current) { host := current.String() if _, ok := excluded[host]; ok { continue } if _, ok := seen[host]; ok { continue } seen[host] = struct{}{} out = append(out, host) } } return out, nil } func probeHost(ip string, timeoutMS int) bool { if timeoutMS <= 0 { timeoutMS = 700 } timeoutSec := strconv.Itoa(max(1, timeoutMS/1000)) var cmd *exec.Cmd if runtime.GOOS == "windows" { // -n 1 one packet, -w timeout in ms cmd = exec.Command("ping", "-n", "1", "-w", strconv.Itoa(timeoutMS), ip) } else { // -c 1 one packet, -W timeout in sec on Linux/macOS cmd = exec.Command("ping", "-c", "1", "-W", timeoutSec, ip) } if err := cmd.Run(); err != nil { log.Printf("probe failed for %s: %v", ip, err) return false } return true } func dupIP(ip net.IP) net.IP { out := make(net.IP, len(ip)) copy(out, ip) return out } func incIP(ip net.IP) { for j := len(ip) - 1; j >= 0; j-- { ip[j]++ if ip[j] > 0 { break } } } func max(a, b int) int { if a > b { return a } return b }