fix: 网段预筛从抽样改全覆盖,每台主机发 1 个探测包,消除漏报

This commit is contained in:
ZacharyZcR
2026-04-28 13:24:51 +08:00
parent d96695e602
commit 5616cc180e
+48 -79
View File
@@ -671,19 +671,20 @@ func tryHTTPFallbackDetection(host string, port int, addr string, config *common
// subnetProbeThreshold 触发网段预筛的主机数阈值(超过 1 个 /24) // subnetProbeThreshold 触发网段预筛的主机数阈值(超过 1 个 /24)
const subnetProbeThreshold = 256 const subnetProbeThreshold = 256
// subnetProbePorts 网段探活用的端口(覆盖面广、响应快) // subnetProbePorts 网段探活用的端口(覆盖率优先)
var subnetProbePorts = []int{80, 443, 22, 445, 3389} var subnetProbePorts = []int{80, 443, 22, 445, 3389, 8080, 3306, 6379}
// subnetProbeTimeout 每个探测的超时 // subnetProbeTimeout 每个探测的超时
const subnetProbeTimeout = 1 * time.Second const subnetProbeTimeout = 1500 * time.Millisecond
// subnetProbeConcurrency 网段探活并发数 // subnetProbeConcurrency 网段探活全局并发数
const subnetProbeConcurrency = 200 const subnetProbeConcurrency = 500
// probeSubnets 对每个 /24 网段做轻量探活,返回属于存活网段的主机列表 // probeSubnets 对每个 /24 网段做全覆盖探活,返回属于存活网段的主机列表
// 策略:每台主机打 1 个端口(端口在子网内轮换),子网内任一主机响应即保留
func probeSubnets(ctx context.Context, hosts []string, timeout time.Duration, session *common.ScanSession) []string { func probeSubnets(ctx context.Context, hosts []string, timeout time.Duration, session *common.ScanSession) []string {
// 按 /24 分组 // 按 /24 分组
subnets := make(map[string][]string) // "10.1.1" → [10.1.1.1, 10.1.1.2, ...] subnets := make(map[string][]string)
for _, h := range hosts { for _, h := range hosts {
prefix := subnetPrefix(h) prefix := subnetPrefix(h)
if prefix != "" { if prefix != "" {
@@ -691,111 +692,79 @@ func probeSubnets(ctx context.Context, hosts []string, timeout time.Duration, se
} }
} }
// 只有 1 个 /24,不需要预筛
if len(subnets) <= 1 { if len(subnets) <= 1 {
return hosts return hosts
} }
common.LogInfo(fmt.Sprintf("网段预筛: %d 个 /24 子网, %d 个主机", len(subnets), len(hosts))) common.LogInfo(fmt.Sprintf("网段预筛: %d 个 /24 子网, %d 个主机", len(subnets), len(hosts)))
// 并发探测每个 /24 // 全局并发控制
aliveSubnets := make(map[string]bool) aliveSubnets := sync.Map{}
var mu sync.Mutex
var wg sync.WaitGroup var wg sync.WaitGroup
limiter := make(chan struct{}, subnetProbeConcurrency) limiter := make(chan struct{}, subnetProbeConcurrency)
for prefix, subnetHosts := range subnets { for prefix, subnetHosts := range subnets {
wg.Add(1) // 对子网内每台主机发 1 个探测包(端口轮换覆盖多个端口)
limiter <- struct{}{} for i, host := range subnetHosts {
go func(pfx string, sHosts []string) {
defer func() {
<-limiter
wg.Done()
}()
select { select {
case <-ctx.Done(): case <-ctx.Done():
return goto done
default: default:
} }
if probeSubnetAlive(ctx, sHosts, session) { port := subnetProbePorts[i%len(subnetProbePorts)]
mu.Lock()
aliveSubnets[pfx] = true // 如果这个子网已经确认存活,跳过剩余主机
mu.Unlock() if _, alive := aliveSubnets.Load(prefix); alive {
break
} }
}(prefix, subnetHosts)
wg.Add(1)
limiter <- struct{}{}
go func(pfx, h string, p int) {
defer func() {
<-limiter
wg.Done()
}()
addr := fmt.Sprintf("%s:%d", h, p)
conn, err := net.DialTimeout("tcp", addr, subnetProbeTimeout)
if err == nil {
_ = conn.Close()
aliveSubnets.Store(pfx, true)
}
}(prefix, host, port)
}
} }
done:
wg.Wait() wg.Wait()
if len(aliveSubnets) == 0 { // 统计
aliveCount := 0
aliveSubnets.Range(func(_, _ interface{}) bool {
aliveCount++
return true
})
if aliveCount == 0 {
return nil return nil
} }
// 只保留存活网段的主机 // 只保留存活网段的主机
result := make([]string, 0, len(hosts)) result := make([]string, 0, len(hosts))
for _, h := range hosts { for _, h := range hosts {
if aliveSubnets[subnetPrefix(h)] { if _, alive := aliveSubnets.Load(subnetPrefix(h)); alive {
result = append(result, h) result = append(result, h)
} }
} }
skipped := len(subnets) - len(aliveSubnets) skipped := len(subnets) - aliveCount
common.LogInfo(fmt.Sprintf("网段预筛完成: %d 个存活, %d 个跳过, 剩余 %d 主机", len(aliveSubnets), skipped, len(result))) common.LogInfo(fmt.Sprintf("网段预筛完成: %d 个存活, %d 个跳过, 剩余 %d 主机", aliveCount, skipped, len(result)))
return result return result
} }
// probeSubnetAlive 探测一个 /24 网段是否存活
// 从网段中抽样最多 3 台主机,对每台并行打探测端口,任一响应即判定存活
func probeSubnetAlive(ctx context.Context, hosts []string, session *common.ScanSession) bool {
// 抽样:取网段中最多 3 台主机(首、中、尾)
samples := sampleHosts(hosts, 3)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
result := make(chan bool, len(samples)*len(subnetProbePorts))
total := 0
for _, host := range samples {
for _, port := range subnetProbePorts {
total++
go func(h string, p int) {
addr := fmt.Sprintf("%s:%d", h, p)
conn, err := net.DialTimeout("tcp", addr, subnetProbeTimeout)
if err == nil {
_ = conn.Close()
result <- true
return
}
result <- false
}(host, port)
}
}
for i := 0; i < total; i++ {
if <-result {
return true
}
}
return false
}
// sampleHosts 从列表中均匀抽样 n 台主机
func sampleHosts(hosts []string, n int) []string {
if len(hosts) <= n {
return hosts
}
samples := make([]string, n)
step := len(hosts) / n
for i := 0; i < n; i++ {
samples[i] = hosts[i*step]
}
return samples
}
// subnetPrefix 提取 IP 的 /24 前缀(如 "10.1.1") // subnetPrefix 提取 IP 的 /24 前缀(如 "10.1.1")
func subnetPrefix(ip string) string { func subnetPrefix(ip string) string {
lastDot := strings.LastIndex(ip, ".") lastDot := strings.LastIndex(ip, ".")