Files
fscan/core/service_scanner.go
T
ZacharyZcR 88c7e4f2be feat: 自适应并发调度 — 网络探测 + AIMD + 参数智能推导
扫描前自动探测网络环境(RTT、丢包率、fd limit),基于探测数据
推导 6 个关键参数,替代硬编码默认值:

- Timeout: median_RTT + 4σ(覆盖 99.9% 正常连接)
- ModuleThreadNum: target_concurrency / 30
- MaxRetries: ceil(log(0.01)/log(loss_rate))(全失败概率 <1%)
- ICMPRate: 环境基准 × fd 系数
- PocNum: 跟随 ModuleThreadNum
- DisablePing: 已有 ICMP 权限降级机制

线程池从单信号(资源耗尽率)升级为 AIMD + 慢启动:
- 慢启动:target/4 起步,500ms 翻倍
- 稳态 AIMD:健康 +5%,拥塞 ×0.5
- 双信号:资源耗尽率 + RTT 趋势(双 EMA)

用户 -t 显式指定时作为 ceiling,探测仍调整其他参数。

测试:单元 + 边界 + 集成 + 真实网络,core 包 580+ 用例全通过。
2026-06-14 22:23:45 +08:00

481 lines
14 KiB
Go

package core
import (
"context"
"fmt"
"net"
"strconv"
"strings"
"sync"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/common/parsers"
"github.com/shadow1ng/fscan/plugins"
)
// ServiceScanStrategy 服务扫描策略
type ServiceScanStrategy struct {
*BaseScanStrategy
}
// NewServiceScanStrategy 创建新的服务扫描策略
func NewServiceScanStrategy() *ServiceScanStrategy {
return &ServiceScanStrategy{
BaseScanStrategy: NewBaseScanStrategy(i18n.GetText("scan_strategy_service_name"), FilterService),
}
}
// LogPluginInfo 重写以提供基于端口的插件过滤
func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) {
// 需要从命令行参数获取端口信息来进行过滤
// 如果没有指定端口,使用默认端口进行过滤显示
ports := config.Target.Ports
if ports == "" || ports == "all" {
// 默认端口扫描:显示所有插件
s.BaseScanStrategy.LogPluginInfo(config, session)
} else {
// 指定端口扫描:只显示匹配的插件
s.showPluginsForSpecifiedPorts(config, session)
}
}
// showPluginsForSpecifiedPorts 显示指定端口的匹配插件
func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config, session *common.ScanSession) {
allPlugins, isCustomMode := s.GetPlugins(config)
// 解析端口
ports := s.parsePortList(config.Target.Ports)
if len(ports) == 0 {
s.BaseScanStrategy.LogPluginInfo(config, session)
return
}
// 收集所有匹配的插件(去重)
pluginSet := make(map[string]struct{}, len(allPlugins))
for _, port := range ports {
for _, pluginName := range allPlugins {
if s.pluginExists(pluginName) {
if s.isPluginApplicableToPort(pluginName, port) && s.isPluginPassesFilterType(pluginName, isCustomMode, config) {
pluginSet[pluginName] = struct{}{}
}
}
}
}
// 转换为列表
var applicablePlugins []string
for pluginName := range pluginSet {
applicablePlugins = append(applicablePlugins, pluginName)
}
// 输出结果
if len(applicablePlugins) > 0 {
pluginStr := formatPluginList(applicablePlugins)
if isCustomMode {
session.LogInfo(i18n.Tr("service_plugin_custom", pluginStr))
} else {
session.LogInfo(i18n.Tr("service_plugin_info", pluginStr))
}
} else {
session.LogInfo(i18n.GetText("service_plugin_none"))
}
}
// parsePortList 解析端口列表
func (s *ServiceScanStrategy) parsePortList(portStr string) []int {
if portStr == "" || portStr == "all" {
return []int{}
}
ports := []int{} // 初始化为空切片而非nil
parts := strings.Split(portStr, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if port, err := strconv.Atoi(part); err == nil {
// 验证端口范围 1-65535(与 scanner.go 的 parsePort 保持一致)
if port >= 1 && port <= 65535 {
ports = append(ports, port)
} else {
common.LogError(i18n.Tr("port_out_of_range", port))
}
}
}
return ports
}
// Name 返回策略名称
func (s *ServiceScanStrategy) Name() string {
return i18n.GetText("scan_strategy_service_name")
}
// Description 返回策略描述
func (s *ServiceScanStrategy) Description() string {
return i18n.GetText("scan_strategy_service_desc")
}
// Execute 执行服务扫描策略
func (s *ServiceScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
config := session.Config
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
if info.Host == "" && session.Params.HostsFile == "" {
session.LogError(i18n.GetText("parse_error_target_empty"))
return
}
// 输出扫描开始信息
s.LogScanStart(session)
// 验证插件配置
if err := s.ValidateConfiguration(); err != nil {
session.LogError(err.Error())
return
}
// 输出插件信息(重写以提供端口过滤)
s.LogPluginInfo(config, session)
// 执行主机扫描流程
s.performHostScan(ctx, session, info, ch, wg)
}
// performHostScan 执行主机扫描的完整流程
// pipeline 模式:端口扫描和插件执行并行,扫到开放端口立即开始跑插件
func (s *ServiceScanStrategy) performHostScan(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
config := session.Config
state := session.State
excludes, err := loadHostExcludes(session.Params)
if err != nil {
session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err))
return
}
iter, err := parsers.NewHostIterator(info.Host, session.Params.HostsFile, excludes...)
if err != nil {
session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err))
return
}
defer func() {
_ = iter.Close()
}()
pluginsToRun, isCustomMode := s.GetPlugins(config)
totalAlive := 0
sawHosts := false
performedLiveness := false
envProfiled := false
// 系统能力探测(不需要网络目标)
sysProfile := ProbeSystem()
for {
hosts, err := iter.NextBatch(ctx, targetHostBatchSize(config))
if err != nil {
session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err))
return
}
if len(hosts) == 0 {
break
}
sawHosts = true
if s.shouldPerformLivenessCheck(hosts, config) {
performedLiveness = true
hosts = CheckLive(ctx, hosts, false, session)
}
totalAlive += len(hosts)
if len(hosts) == 0 {
continue
}
// 首批 alive hosts 出来后做网络探测,调整后续所有参数
if !envProfiled {
envProfiled = true
netProfile := ProbeNetwork(ctx, hosts, session)
ep := &EnvironmentProfile{Net: *netProfile, System: sysProfile}
ep.TuneConfig(config, session)
}
s.dispatchUDPPlugins(ctx, session, hosts, info, config, ch, wg)
s.scanHostBatch(ctx, session, hosts, info, pluginsToRun, isCustomMode, ch, wg)
}
if sawHosts && performedLiveness {
session.LogInfo(i18n.Tr("alive_hosts_count_info", totalAlive))
}
if !sawHosts && len(state.GetHostPorts()) == 0 {
return
}
// 合并预设的 host:port
hostPorts := state.GetHostPorts()
if len(hostPorts) > 0 {
merged := mergeHostPorts(nil, hostPorts)
targets := s.convertToTargetInfos(merged, info)
for _, target := range targets {
for _, pluginName := range pluginsToRun {
if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) {
executeScanTask(ctx, session, pluginName, target, ch, wg)
}
}
}
state.ClearHostPorts()
}
}
func (s *ServiceScanStrategy) scanHostBatch(ctx context.Context, session *common.ScanSession, hosts []string, info common.HostInfo, pluginsToRun []string, isCustomMode bool, ch chan struct{}, wg *sync.WaitGroup) {
config := session.Config
stream := make(chan string, 64)
go EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, stream)
cancelled := false
for addr := range stream {
if cancelled {
continue
}
select {
case <-ctx.Done():
cancelled = true
continue
default:
}
infos := s.convertToTargetInfos([]string{addr}, info)
for _, target := range infos {
for _, pluginName := range pluginsToRun {
if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) {
executeScanTask(ctx, session, pluginName, target, ch, wg)
}
}
}
}
}
// dispatchUDPPlugins 分发UDP协议插件,跳过TCP端口扫描链路
func (s *ServiceScanStrategy) dispatchUDPPlugins(ctx context.Context, session *common.ScanSession, hosts []string, baseInfo common.HostInfo, config *common.Config, ch chan struct{}, wg *sync.WaitGroup) {
_, isCustomMode := s.GetPlugins(config)
var udpPlugins []string
if isCustomMode {
// custom mode: 只跑用户指定的 UDP 插件
requested, _ := s.GetPlugins(config)
for _, name := range requested {
if plugins.IsUDP(name) {
udpPlugins = append(udpPlugins, name)
}
}
} else {
// auto mode: 跑所有已注册的安全 UDP 插件
for _, name := range plugins.All() {
if plugins.IsUDP(name) && plugins.IsSafe(name) {
udpPlugins = append(udpPlugins, name)
}
}
}
if len(udpPlugins) == 0 {
return
}
for _, host := range hosts {
for _, pluginName := range udpPlugins {
for _, port := range plugins.GetPluginPorts(pluginName) {
target := baseInfo
target.Host = host
target.Port = port
executeScanTask(ctx, session, pluginName, target, ch, wg)
}
}
}
}
// PrepareTargets 准备目标信息
func (s *ServiceScanStrategy) PrepareTargets(info common.HostInfo, session *common.ScanSession) []common.HostInfo {
// 发现目标主机和端口
targetInfos, err := s.discoverTargets(context.Background(), info.Host, info, session)
if err != nil {
session.LogError(err.Error())
return nil
}
return targetInfos
}
// LogVulnerabilityPluginInfo 输出服务扫描插件信息
func (s *ServiceScanStrategy) LogVulnerabilityPluginInfo(targets []common.HostInfo, config *common.Config) {
allPlugins, isCustomMode := s.GetPlugins(config)
// 获取实际会被使用的插件列表
servicePluginSet := make(map[string]struct{}, len(allPlugins))
for _, pluginName := range allPlugins {
// 使用统一插件系统检查插件存在性
if !s.pluginExists(pluginName) {
continue
}
// 检查插件是否通过过滤器类型检查
if !s.isPluginPassesFilterType(pluginName, isCustomMode, config) {
continue
}
// 检查插件是否适用于任意一个目标
for _, target := range targets {
if target.Port == 0 {
continue
}
// 使用 host:port 信息检查插件适用性(Web插件需要host信息)
if s.isPluginApplicableToPortWithHost(pluginName, target.Host, target.Port) {
servicePluginSet[pluginName] = struct{}{}
break // 只要适用于一个目标就添加
}
}
}
// 转换为切片
var servicePlugins []string
for pluginName := range servicePluginSet {
servicePlugins = append(servicePlugins, pluginName)
}
// 输出插件信息
if len(servicePlugins) > 0 {
common.LogInfo(i18n.Tr("service_plugin_info", strings.Join(servicePlugins, ", ")))
} else {
common.LogInfo(i18n.GetText("scan_no_service_plugins"))
}
}
// =============================================================================
// 端口发现功能(从 PortDiscoveryService 合并)
// =============================================================================
// discoverTargets 发现目标主机和端口
func (s *ServiceScanStrategy) discoverTargets(ctx context.Context, hostInput string, baseInfo common.HostInfo, session *common.ScanSession) ([]common.HostInfo, error) {
config := session.Config
state := session.State
// 标准流程:解析目标主机
excludes, err := loadHostExcludes(session.Params)
if err != nil {
return nil, fmt.Errorf("%s: %w", i18n.GetText("parse_target_failed"), err)
}
hosts, err := parsers.ParseIP(hostInput, session.Params.HostsFile, excludes...)
if err != nil {
return nil, fmt.Errorf("%s: %w", i18n.GetText("parse_target_failed"), err)
}
var targetInfos []common.HostInfo
// 主机存活性检测和端口扫描
if len(hosts) > 0 || len(state.GetHostPorts()) > 0 {
// 主机存活检测
if s.shouldPerformLivenessCheck(hosts, config) {
hosts = CheckLive(ctx, hosts, false, session)
session.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts)))
}
// 端口扫描
alivePorts := s.discoverAlivePorts(ctx, hosts, session)
if len(alivePorts) > 0 {
targetInfos = s.convertToTargetInfos(alivePorts, baseInfo)
}
}
return targetInfos, nil
}
// shouldPerformLivenessCheck 判断是否需要执行存活性检测
func (s *ServiceScanStrategy) shouldPerformLivenessCheck(hosts []string, config *common.Config) bool {
return !config.DisablePing && len(hosts) > 1
}
// discoverAlivePorts 发现存活的端口
// 执行正常端口扫描后,合并预设的 host:port(来自项目缓存或 CLI),确保不遗漏
func (s *ServiceScanStrategy) discoverAlivePorts(ctx context.Context, hosts []string, session *common.ScanSession) []string {
config := session.Config
state := session.State
var alivePorts []string
// 正常端口扫描
if len(hosts) > 0 {
alivePorts = EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, nil)
}
// 合并预设的 host:port(项目缓存 / CLI 注入)
hostPorts := state.GetHostPorts()
if len(hostPorts) > 0 {
alivePorts = mergeHostPorts(alivePorts, hostPorts)
session.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts)))
state.ClearHostPorts()
}
return alivePorts
}
// mergeHostPorts 合并两个 host:port 列表并去重
func mergeHostPorts(a, b []string) []string {
seen := make(map[string]struct{}, len(a)+len(b))
for _, s := range a {
seen[s] = struct{}{}
}
for _, s := range b {
seen[s] = struct{}{}
}
result := make([]string, 0, len(seen))
for s := range seen {
result = append(result, s)
}
return result
}
// convertToTargetInfos 将端口列表转换为目标信息
func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo common.HostInfo) []common.HostInfo {
var infos []common.HostInfo
for _, targetIP := range ports {
targetIP = strings.TrimSpace(targetIP)
host, portStr, err := net.SplitHostPort(targetIP)
if err != nil && strings.Count(targetIP, ":") == 1 {
parts := strings.SplitN(targetIP, ":", 2)
host, portStr = parts[0], parts[1]
err = nil
}
if err != nil {
common.LogError(i18n.Tr("invalid_target_format", targetIP))
continue
}
// 去除空格并过滤空值
host = strings.TrimSpace(host)
portStr = strings.TrimSpace(portStr)
if host == "" || portStr == "" {
common.LogError(i18n.Tr("invalid_target_format", targetIP))
continue
}
// 验证端口范围(与scanner.go中parsePort保持一致)
port, err := strconv.Atoi(portStr)
if err != nil {
common.LogError(i18n.Tr("host_port_invalid", host, portStr))
continue
}
if port < 1 || port > 65535 {
common.LogError(i18n.Tr("host_port_out_of_range", host, port))
continue
}
info := baseInfo
info.Host = host
info.Port = port
// 深拷贝Info避免多个target共享slice底层数组
if len(baseInfo.Info) > 0 {
info.Info = append([]string(nil), baseInfo.Info...)
}
infos = append(infos, info)
}
return infos
}