2026-06-12 09:46:02 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-06-13 12:39:24 +08:00
|
|
|
|
// ---------- 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
|
|
|
|
|
|
}
|
2026-06-12 09:46:02 +08:00
|
|
|
|
|
|
|
|
|
|
// ---------- 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") {
|
2026-06-13 12:39:24 +08:00
|
|
|
|
computed := config.ThreadNum / 30
|
2026-06-12 09:46:02 +08:00
|
|
|
|
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
|
2026-06-13 12:39:24 +08:00
|
|
|
|
session.LogDebug(fmt.Sprintf("ModuleThreadNum: %d -> %d (threadNum=%d)", old, computed, config.ThreadNum))
|
2026-06-12 09:46:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 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 {
|
2026-06-13 12:39:24 +08:00
|
|
|
|
computed := computeRetries(net.LossRate, net.Env)
|
2026-06-12 09:46:02 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-06-13 12:39:24 +08:00
|
|
|
|
if config.ThreadCeiling > maxConcurrency {
|
|
|
|
|
|
config.ThreadCeiling = maxConcurrency
|
|
|
|
|
|
}
|
2026-06-12 09:46:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 12:39:24 +08:00
|
|
|
|
// computeRetries 基于丢包率和网络环境计算重试次数
|
|
|
|
|
|
// 内网丢包异常,用更严格的目标概率(0.5%)和更低上限
|
|
|
|
|
|
// 公网/慢速丢包常见,放宽目标概率(2%)和更高上限
|
|
|
|
|
|
func computeRetries(lossRate float64, env NetworkEnv) int {
|
2026-06-12 09:46:02 +08:00
|
|
|
|
if lossRate <= 0.001 {
|
2026-06-13 12:39:24 +08:00
|
|
|
|
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
|
2026-06-12 09:46:02 +08:00
|
|
|
|
}
|
2026-06-13 12:39:24 +08:00
|
|
|
|
|
2026-06-12 09:46:02 +08:00
|
|
|
|
if lossRate >= 0.95 {
|
2026-06-13 12:39:24 +08:00
|
|
|
|
return maxRetries
|
2026-06-12 09:46:02 +08:00
|
|
|
|
}
|
2026-06-13 12:39:24 +08:00
|
|
|
|
// P(N次全失败) = lossRate^N < targetProb
|
|
|
|
|
|
n := math.Ceil(math.Log(targetProb) / math.Log(lossRate))
|
|
|
|
|
|
return clampInt(int(n), 1, maxRetries)
|
2026-06-12 09:46:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-12 15:30:44 +08:00
|
|
|
|
// isExplicit 检查参数是否被用户显式指定。
|
|
|
|
|
|
// 显式标记来自 CLI flag.Visit;值比较保留 SDK/测试里直接构造 Config 的旧行为。
|
2026-06-12 09:46:02 +08:00
|
|
|
|
func isExplicit(config *common.Config, flagName string) bool {
|
|
|
|
|
|
switch flagName {
|
|
|
|
|
|
case "t":
|
|
|
|
|
|
return config.ThreadNumExplicit
|
|
|
|
|
|
case "time":
|
2026-06-12 15:30:44 +08:00
|
|
|
|
return config.TimeoutExplicit || config.Timeout != 3*time.Second
|
2026-06-12 09:46:02 +08:00
|
|
|
|
case "mt":
|
2026-06-12 15:30:44 +08:00
|
|
|
|
return config.ModuleThreadNumExplicit || config.ModuleThreadNum != 20
|
2026-06-12 09:46:02 +08:00
|
|
|
|
case "retry":
|
2026-06-12 15:30:44 +08:00
|
|
|
|
return config.MaxRetriesExplicit || config.MaxRetries != 3
|
2026-06-12 09:46:02 +08:00
|
|
|
|
case "icmp-rate":
|
2026-06-12 15:30:44 +08:00
|
|
|
|
return config.Network.ICMPRateExplicit || config.Network.ICMPRate != 0.1
|
2026-06-12 09:46:02 +08:00
|
|
|
|
case "num":
|
2026-06-12 15:30:44 +08:00
|
|
|
|
return config.POC.NumExplicit || config.POC.Num != 20
|
2026-06-12 09:46:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|