Files
fscan/core/network_profiler.go
T
ZacharyZcR 8402be98e3 优化自适应扫描系统 & 修复 POC 调度问题
自适应扫描优化:
- target/ceiling 分离,自适应池可向上探索而非锁死在 target
- assessHealth 阈值按网络环境区分(LAN 收紧 / Internet 放宽)
- RTT 漂移时动态压低 target,配合 AIMD 双重降速
- 去掉 semaphore 双层流控,由 ants pool 统一反压
- 探测端口从 3 个扩充到 8 个,减少 RTT 采样偏差
- computeRetries 按环境调整目标概率和上限

Bug 修复:
- AdaptivePool.Wait() 加 10 分钟超时,防止 goroutine 卡死时永久挂起
- CEL 环境初始化失败后允许重试(sync.Once → sync.Mutex + 标志位)
- CAS 自旋加 runtime.Gosched() 退避,减少高并发下 CPU 空转
- -full 模式下 web 插件跳过 IsMarkedWebService 检查 #588
- 不确定服务补做 HTTP 回退探测,覆盖自定义框架漏网场景
- POC sets 纯字面量值跳过 CEL 编译,消除大量误报错误日志
2026-06-14 22:23:48 +08:00

282 lines
6.1 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 (
"context"
"math"
"net"
"sort"
"strconv"
"sync"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
)
// NetworkEnv 网络环境分类
type NetworkEnv int
const (
EnvLAN NetworkEnv = iota // 内网: RTT < 5ms, 丢包 < 1%
EnvWAN // 局域网/专线: RTT 5~50ms, 丢包 < 5%
EnvInternet // 公网: RTT 50~200ms
EnvSlow // 慢速/高丢包: RTT > 200ms 或 丢包 > 10%
)
func (e NetworkEnv) String() string {
switch e {
case EnvLAN:
return i18n.GetText("net_env_lan")
case EnvWAN:
return i18n.GetText("net_env_wan")
case EnvInternet:
return i18n.GetText("net_env_internet")
default:
return i18n.GetText("net_env_slow")
}
}
// NetworkProfile 网络探测结果
type NetworkProfile struct {
Env NetworkEnv
RTTMin time.Duration
RTTMedian time.Duration
RTTP95 time.Duration
RTTStddev time.Duration
LossRate float64
Samples int
}
// RecommendConcurrency 根据探测结果推荐并发参数
// 返回 (target, ceiling)
// - target: 推荐的目标并发数
// - ceiling: 允许的最大并发数
//
// 如果用户显式指定了 -tceiling = 用户值,target 取 min(推荐值, 用户值)
// 如果用户未指定,target 和 ceiling 均为推荐值
func (p *NetworkProfile) RecommendConcurrency(userThreadNum int, explicit bool) (target, ceiling int) {
// 基于网络环境的缩放因子
var factor float64
switch p.Env {
case EnvLAN:
factor = 1.5
case EnvWAN:
factor = 1.0
case EnvInternet:
factor = 0.4
case EnvSlow:
factor = 0.15
}
recommended := int(float64(userThreadNum) * factor)
if recommended < 10 {
recommended = 10
}
// 丢包率高时进一步压缩
if p.LossRate > 0.05 {
recommended = int(float64(recommended) * (1.0 - p.LossRate))
if recommended < 10 {
recommended = 10
}
}
if explicit {
ceiling = userThreadNum
target = recommended
if target > ceiling {
target = ceiling
}
} else {
target = recommended
ceiling = recommended
}
return
}
// probePorts 探测用的端口列表(高响应率的常见端口)
var probePorts = []int{80, 443, 22, 445, 8080, 3389, 21, 8443}
func networkProbeAddress(host string, port int) string {
return net.JoinHostPort(host, strconv.Itoa(port))
}
// ProbeNetwork 探测目标网络环境
// 从 hosts 中抽样,用低并发 TCP 连接测量 RTT 和丢包率
// 整个过程控制在数秒内完成
func ProbeNetwork(ctx context.Context, hosts []string, session *common.ScanSession) *NetworkProfile {
if len(hosts) == 0 {
return defaultProfile()
}
// 抽样:均匀分布,最多 10 个
samples := pickSamples(hosts, 10)
probeTimeout := session.Config.Timeout
if probeTimeout > time.Second {
probeTimeout = time.Second
}
if probeTimeout < 500*time.Millisecond {
probeTimeout = 500 * time.Millisecond
}
var (
mu sync.Mutex
rtts []time.Duration
failures int
total int
)
sem := make(chan struct{}, 10)
var wg sync.WaitGroup
for _, host := range samples {
for _, port := range probePorts {
select {
case <-ctx.Done():
goto done
default:
}
total++
wg.Add(1)
sem <- struct{}{}
go func(h string, p int) {
defer func() { <-sem; wg.Done() }()
addr := networkProbeAddress(h, p)
start := time.Now()
conn, err := session.DialTCP(ctx, "tcp", addr, probeTimeout)
rtt := time.Since(start)
mu.Lock()
defer mu.Unlock()
if err != nil {
// 连接拒绝也是有效的 RTT 样本(说明对端可达)
if isConnectionRefused(err) {
rtts = append(rtts, rtt)
}
failures++
} else {
_ = conn.Close()
rtts = append(rtts, rtt)
}
}(host, port)
}
}
done:
wg.Wait()
return classifyNetwork(rtts, failures, total)
}
func classifyNetwork(rtts []time.Duration, failures, total int) *NetworkProfile {
if len(rtts) == 0 {
return defaultProfile()
}
sort.Slice(rtts, func(i, j int) bool { return rtts[i] < rtts[j] })
n := len(rtts)
median := rtts[n/2]
p95idx := int(float64(n) * 0.95)
if p95idx >= n {
p95idx = n - 1
}
p95 := rtts[p95idx]
// 标准差
var sum float64
for _, r := range rtts {
sum += float64(r)
}
mean := sum / float64(n)
var variance float64
for _, r := range rtts {
d := float64(r) - mean
variance += d * d
}
stddev := time.Duration(math.Sqrt(variance / float64(n)))
// 丢包率:只计算超时的(非 refused),但简化为 1 - 有效响应数/总数
lossRate := 1.0 - float64(n)/float64(total)
if lossRate < 0 {
lossRate = 0
}
// 分类
env := classifyEnv(median, lossRate)
return &NetworkProfile{
Env: env,
RTTMin: rtts[0],
RTTMedian: median,
RTTP95: p95,
RTTStddev: stddev,
LossRate: lossRate,
Samples: n,
}
}
func classifyEnv(median time.Duration, lossRate float64) NetworkEnv {
switch {
case lossRate > 0.10:
return EnvSlow
case median < 5*time.Millisecond && lossRate < 0.01:
return EnvLAN
case median < 50*time.Millisecond && lossRate < 0.05:
return EnvWAN
case median < 200*time.Millisecond:
return EnvInternet
default:
return EnvSlow
}
}
func defaultProfile() *NetworkProfile {
return &NetworkProfile{
Env: EnvWAN,
RTTMedian: 10 * time.Millisecond,
LossRate: 0,
Samples: 0,
}
}
// pickSamples 均匀抽样
func pickSamples(hosts []string, maxSamples int) []string {
if maxSamples <= 0 {
return nil
}
n := len(hosts)
if n <= maxSamples {
return hosts
}
step := n / maxSamples
samples := make([]string, 0, maxSamples)
for i := 0; i < n && len(samples) < maxSamples; i += step {
samples = append(samples, hosts[i])
}
return samples
}
func isConnectionRefused(err error) bool {
if err == nil {
return false
}
// connection refused 通常包含 "refused" 关键词
// 在不同 OS 上表现一致
return containsFold(err.Error(), "refused")
}
// isTimeoutError 判断是否为超时错误
func isTimeoutError(err error) bool {
if err == nil {
return false
}
if ne, ok := err.(net.Error); ok {
return ne.Timeout()
}
return containsFold(err.Error(), "timeout") || containsFold(err.Error(), "deadline")
}