Files
fscan/core/adaptive_timeout.go
T
逸航 9d0010927e fix: 修复高并发下自适应超时过低导致开放端口漏扫 (#598)
* fix: 修复高并发下自适应超时过低导致开放端口漏扫 (#503)

扫描本机/低 RTT 目标时,AdaptiveTimeout 在 10 次采样后迅速收敛到 100ms 下限。高并发(600+ 线程)下 TCP 握手尾延迟可能超过 100ms,加上超时错误不会重试,导致开放端口被误判为关闭。

- AdaptiveTimeout 下限从 100ms 提升至 max(500ms, maxTimeout/5)
- connectWithRetry 对超时错误用完整超时重试一次
- slidingWindowSchedule 任务丢弃时记录日志,便于排查漏扫

* refactor: 按 review 意见移除无条件超时重试,补充 minTO 下限测试

根据 #598 review 反馈:

1. 移除 connectWithRetry 中 timeout->full maxTO 无条件重试
   - filtered/无响应端口占超时大头,盲目重试只烧时间
   - #503 主场景靠 minTO 抬升已足够覆盖
2. 移除不再使用的 MaxTimeout() 方法和 port_scan_timeout_retry i18n 条目
3. AdaptiveTimeout 收敛测试补充 minTO 下限断言(3s->600ms)
2026-07-16 01:07:27 +08:00

118 lines
2.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package core
import (
"math"
"sync"
"time"
)
// AdaptiveTimeout 基于 RTT 采样的自适应超时计算器
// 算法:timeout = mean(RTT) + 4 * stddev(RTT)clamp 到 [min, max]
// 冷启动阶段(样本不足)返回用户配置的固定超时
type AdaptiveTimeout struct {
mu sync.Mutex
samples []float64 // 环形缓冲区,单位 ms
pos int // 写入位置
count int // 已采集总数
size int // 缓冲区容量
minTO time.Duration
maxTO time.Duration
warmup int // 冷启动所需最小样本数
cachedTO time.Duration
dirty bool
}
// NewAdaptiveTimeout 创建自适应超时计算器
// maxTimeout: 用户配置的超时上限(即原始固定超时)
func NewAdaptiveTimeout(maxTimeout time.Duration) *AdaptiveTimeout {
// minTO: 自适应超时下限,取 max(500ms, maxTimeout/5)
// 依据:高并发下 TCP 握手存在尾延迟(OS 调度抖动、backlog 溢出、端口竞争),
// 过低的下限会导致开放端口被误判为关闭(issue #503)
minTO := maxTimeout / 5
if minTO < 500*time.Millisecond {
minTO = 500 * time.Millisecond
}
return &AdaptiveTimeout{
samples: make([]float64, 64),
size: 64,
minTO: minTO,
maxTO: maxTimeout,
warmup: 10,
}
}
// Record 记录一次成功连接的 RTT
func (a *AdaptiveTimeout) Record(rtt time.Duration) {
a.mu.Lock()
a.samples[a.pos%a.size] = float64(rtt.Milliseconds())
a.pos++
a.count++
a.dirty = true
a.mu.Unlock()
}
// Timeout 获取当前推荐超时值
// 样本不足时返回 maxTO(冷启动)
// 锁外执行均值/标准差计算,减少锁持有时间
func (a *AdaptiveTimeout) Timeout() time.Duration {
a.mu.Lock()
if a.count < a.warmup {
a.mu.Unlock()
return a.maxTO
}
if !a.dirty {
cached := a.cachedTO
a.mu.Unlock()
return cached
}
n := a.size
if a.count < a.size {
n = a.count
}
// 拷贝样本到本地,释放锁后再计算
localSamples := make([]float64, n)
start := a.pos % a.size
if a.count < a.size {
copy(localSamples, a.samples[:n])
} else {
copy(localSamples[:a.size-start], a.samples[start:])
copy(localSamples[a.size-start:], a.samples[:start])
}
a.mu.Unlock()
// 锁外计算
var sum float64
for _, s := range localSamples {
sum += s
}
mean := sum / float64(n)
var variance float64
for _, s := range localSamples {
d := s - mean
variance += d * d
}
stddev := math.Sqrt(variance / float64(n))
ms := mean + 4*stddev
to := time.Duration(ms) * time.Millisecond
if to < a.minTO {
to = a.minTO
}
if to > a.maxTO {
to = a.maxTO
}
// 短暂加锁更新缓存
a.mu.Lock()
a.cachedTO = to
a.dirty = false
a.mu.Unlock()
return to
}