Files

256 lines
7.6 KiB
Go
Raw Permalink Normal View History

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(避免对不可达目标死磕)
2026-06-13 19:53:28 +08:00
if !isExplicit(config, "retry") {
if 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))
} else if config.MaxRetries > 2 {
// 无网络探测数据(-np 跳过存活探测),降低默认重试避免对不可达主机死磕
old := config.MaxRetries
config.MaxRetries = 2
session.LogDebug(fmt.Sprintf("MaxRetries: %d -> %d (no network probe data)", old, config.MaxRetries))
}
}
// ---------- 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
}