Files
fscan/core/env_profiler.go
T
ZacharyZcR 45ebe7040e
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
优化自适应扫描系统 & 修复 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-13 12:39:24 +08:00

250 lines
7.3 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 (
"fmt"
"math"
"runtime"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
)
// EnvironmentProfile 综合环境探测结果
type EnvironmentProfile struct {
Net NetworkProfile
System SystemProfile
}
// SystemProfile 系统能力信息
type SystemProfile struct {
FDLimit int // 文件描述符上限(0 表示未知)
NumCPU int
}
// ProbeSystem 探测系统能力(不需要网络目标)
func ProbeSystem() SystemProfile {
p := SystemProfile{
NumCPU: runtime.NumCPU(),
}
p.FDLimit = getFDLimit()
return p
}
// TuneConfig 根据探测结果调整 Config 中的参数
// 只调整用户未显式指定的参数
// 每个参数的推导都有明确的公式和探测依据
func (ep *EnvironmentProfile) TuneConfig(config *common.Config, session *common.ScanSession) {
net := &ep.Net
sys := &ep.System
// ---------- NetworkEnv ----------
config.DetectedNetworkEnv = int(net.Env)
// ---------- ThreadNum / ThreadCeiling ----------
if !isExplicit(config, "t") {
target, ceiling := net.RecommendConcurrency(config.ThreadNum, config.ThreadNumExplicit)
old := config.ThreadNum
config.ThreadNum = target
config.ThreadCeiling = ceiling
session.LogDebug(fmt.Sprintf("ThreadNum: %d -> %d, Ceiling: %d (env=%s)", old, target, ceiling, net.Env))
} else {
config.ThreadCeiling = config.ThreadNum
}
// ---------- Timeout ----------
// 公式: median_rtt + 4 * stddev,下限 1s,上限 10s
// 依据: 与 AdaptiveTimeout 相同的统计原理(覆盖 99.9% 的正常连接)
if !isExplicit(config, "time") && net.Samples > 0 {
computed := net.RTTMedian + 4*net.RTTStddev
// 下限:连接建立至少需要 2 个 RTT(SYN + SYN-ACK+ 处理时间
minTO := net.RTTMedian*3 + 200*time.Millisecond
if computed < minTO {
computed = minTO
}
computed = clampDuration(computed, time.Second, 10*time.Second)
old := config.Timeout
config.Timeout = computed
session.LogDebug(fmt.Sprintf("Timeout: %v -> %v (RTT median=%v stddev=%v)",
old, computed, net.RTTMedian, net.RTTStddev))
}
// ---------- ModuleThreadNum ----------
// 公式: ThreadNum / 30,下限 5,上限 50
// 依据: 插件级并发(爆破等)不应超过端口扫描并发的 ~3%
// 单个服务的连接能力远低于 TCP SYN 扫描
// 公网服务通常有限流(MaxStartups 等),并发过高适得其反
if !isExplicit(config, "mt") {
computed := config.ThreadNum / 30
computed = clampInt(computed, 5, 50)
// 高丢包环境进一步压低,避免大量连接被丢弃浪费
if net.LossRate > 0.1 {
computed = computed * 2 / 3
if computed < 5 {
computed = 5
}
}
old := config.ModuleThreadNum
config.ModuleThreadNum = computed
session.LogDebug(fmt.Sprintf("ModuleThreadNum: %d -> %d (threadNum=%d)", old, computed, config.ThreadNum))
}
// ---------- MaxRetries ----------
// 公式: ceil(log(0.01) / log(loss_rate))
// 含义: 重试 N 次后仍然全部丢包的概率 < 1%
// 例: 丢包率 5% → N=2, 丢包率 20% → N=3, 丢包率 50% → N=7
// 下限 1(零丢包也至少试一次),上限 6(避免对不可达目标死磕)
if !isExplicit(config, "retry") && net.Samples > 0 {
computed := computeRetries(net.LossRate, net.Env)
old := config.MaxRetries
config.MaxRetries = computed
session.LogDebug(fmt.Sprintf("MaxRetries: %d -> %d (loss_rate=%.2f%%)", old, computed, net.LossRate*100))
}
// ---------- ICMPRate ----------
// 公式: 基于 fd limit 和网络环境
// 内网 fd 充裕: 0.5(高速发包)
// 公网或 fd 紧张: 0.1(默认保守)
// 依据: ICMP 发包速率受两个约束:网络带宽和本机 fd/socket 资源
if !isExplicit(config, "icmp-rate") && net.Samples > 0 {
computed := computeICMPRate(net, sys)
old := config.Network.ICMPRate
config.Network.ICMPRate = computed
session.LogDebug(fmt.Sprintf("ICMPRate: %.2f -> %.2f (env=%s fd=%d)", old, computed, net.Env, sys.FDLimit))
}
// ---------- PocNum ----------
// 公式: 与 ModuleThreadNum 一致
// 依据: POC 检测和凭据爆破的并发约束相同——都是对目标服务发起连接
if !isExplicit(config, "num") {
old := config.POC.Num
config.POC.Num = config.ModuleThreadNum
session.LogDebug(fmt.Sprintf("PocNum: %d -> %d (follows ModuleThreadNum)", old, config.POC.Num))
}
// ---------- DisablePing ----------
// 由 probeWithICMP 自动处理(尝试 → 失败 → 降级),无需在此干预
// 总结日志
if net.Samples > 0 {
session.LogInfo(i18n.Tr("env_tune_summary",
config.Timeout.Milliseconds(),
config.ModuleThreadNum,
config.MaxRetries,
fmt.Sprintf("%.2f", config.Network.ICMPRate),
config.POC.Num))
}
// fd limit 约束:总并发不应超过 fd limit 的 60%(留余量给系统)
if sys.FDLimit > 0 {
maxConcurrency := sys.FDLimit * 6 / 10
if config.ThreadNum > maxConcurrency {
session.LogInfo(i18n.Tr("env_fd_limit", config.ThreadNum, maxConcurrency, sys.FDLimit))
config.ThreadNum = maxConcurrency
}
if config.ThreadCeiling > maxConcurrency {
config.ThreadCeiling = maxConcurrency
}
}
}
// computeRetries 基于丢包率和网络环境计算重试次数
// 内网丢包异常,用更严格的目标概率(0.5%)和更低上限
// 公网/慢速丢包常见,放宽目标概率(2%)和更高上限
func computeRetries(lossRate float64, env NetworkEnv) int {
if lossRate <= 0.001 {
return 1
}
var targetProb float64
var maxRetries int
switch env {
case EnvLAN:
targetProb = 0.005
maxRetries = 4
case EnvWAN:
targetProb = 0.01
maxRetries = 5
default:
targetProb = 0.02
maxRetries = 6
}
if lossRate >= 0.95 {
return maxRetries
}
// P(N次全失败) = lossRate^N < targetProb
n := math.Ceil(math.Log(targetProb) / math.Log(lossRate))
return clampInt(int(n), 1, maxRetries)
}
// computeICMPRate 基于环境计算 ICMP 发包速率
func computeICMPRate(net *NetworkProfile, sys *SystemProfile) float64 {
// 基准:根据 RTT 估算网络可承受的速率
// RTT 越低,网络越快,可以发更快
var base float64
switch net.Env {
case EnvLAN:
base = 0.5
case EnvWAN:
base = 0.3
case EnvInternet:
base = 0.1
default:
base = 0.05
}
// fd 约束:fd limit 低时压低速率
if sys.FDLimit > 0 && sys.FDLimit < 1024 {
base = base * float64(sys.FDLimit) / 1024.0
if base < 0.02 {
base = 0.02
}
}
return base
}
// isExplicit 检查参数是否被用户显式指定。
// 显式标记来自 CLI flag.Visit;值比较保留 SDK/测试里直接构造 Config 的旧行为。
func isExplicit(config *common.Config, flagName string) bool {
switch flagName {
case "t":
return config.ThreadNumExplicit
case "time":
return config.TimeoutExplicit || config.Timeout != 3*time.Second
case "mt":
return config.ModuleThreadNumExplicit || config.ModuleThreadNum != 20
case "retry":
return config.MaxRetriesExplicit || config.MaxRetries != 3
case "icmp-rate":
return config.Network.ICMPRateExplicit || config.Network.ICMPRate != 0.1
case "num":
return config.POC.NumExplicit || config.POC.Num != 20
}
return false
}
func clampInt(v, min, max int) int {
if v < min {
return min
}
if v > max {
return max
}
return v
}
func clampDuration(v, min, max time.Duration) time.Duration {
if v < min {
return min
}
if v > max {
return max
}
return v
}