mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
v2.1.3 Release (#572)
* add CVE-2026-24061 detect logic (#562) * add CVE-2026-24061 detect logic * fix(telnet): 修复 errcheck 警告,统一错误处理风格 --------- Co-authored-by: ZacharyZcR <[email protected]> * fix: 修复 Hub 广播 data race 和端口扫描潜在死锁,清理死代码 - hub.go: broadcast 路径 RLock 改 Lock,修复并发 delete/close 竞争 - port_scan.go: pool.Invoke 失败时释放 wg 和 semaphore,防止死锁 - web_scanner.go: 删除只写不读的 fingerprintCache - webtitle.go: 移除对已删除 SetFingerprints 的调用 - keylogger.go: 删除未使用的 stopChan 和 isRunning 字段 * refactor: context 穿透扫描生命周期,修复长驻插件阻塞和 Web Stop 无效 - RunScan 接受 context.Context,创建可取消上下文并穿透到所有策略和插件 - 长驻插件(forwardshell/socks5proxy/reverseshell)不再进入 scan WaitGroup, 通过 ctx.Done() 管理生命周期,解除 wg.Wait() 死锁 - Web Stop API 从 stopChan 改为 context.CancelFunc,取消信号真正传播到扫描链路 - ExecuteScanTasks 和 executeScanTask 支持 context 取消检查,停止分发新任务 - CLI 模式传 context.Background(),行为完全不变 * fix: 修复 Web Stop 信号等待阻塞和 SMB 响应解析越界 panic - scanner.go: 长驻插件等待信号时同时监听 ctx.Done(),Web Stop 可正常返回 - smb_protocol.go: 响应长度检查修正为 47,远端偏移量全部做边界校验 * fix: POC 扫描接入调用方 context,修复 cachedPocPath 竞争和 ProxyStats data race - webscan/web_scan.go: WebScan 接受 ctx 参数,替换 context.Background(); sync.Once 改为 sync.Mutex 保护 POC 加载,消除 cachedPocPath 并发写竞争 - webtitle.go: ctx 从 Scan 穿透到 identifyFingerprintsMulti → triggerPocScan → WebScan - webpoc.go: 传递 ctx 到 WebScan - proxy/types.go: ProxyStats 增加 sync.Mutex - proxy/manager.go: LastConnectTime/LastError/AverageConnectTime 读写加锁 * fix: 修复 ProxyStats 含 mutex 导致的 copylocks 告警 Stats() 方法改为手动构造副本,避免值拷贝复制 sync.Mutex * fix: 补全 HTTP/TLS proxy stats 加锁,修复 RPC/SMB 解析越界和 POC 加载逻辑 - httpdialer.go/tlsdialer.go: LastError/LastConnectTime/AverageConnectTime 加 mutex - findnet.go: RPC 响应结束标记位置 < 4 时跳过截断,防止负数切片 panic - ms17010.go: SMB 会话响应最小长度改为 45,sessionSetupResponse 加长度校验 - web_scan.go: POC 加载失败时不标记 pocLoaded,允许后续重试 - Eval.go: DNSLog 配置去掉 sync.Once,允许多次扫描更新配置 * fix: Web 全局状态同步、字典文件错误提示、长驻插件连接可取消 - scan.go: Web API 构建 config/state 后同步到全局实例 - config_builder.go: 用户名/密码/URL 文件读取失败时输出错误日志 - reverseshell.go: 读命令设 1s 超时,超时后检查 ctx 实现可取消 - forwardshell.go: handleClient 接受 ctx,取消时关闭连接解除阻塞 - socks5proxy.go: handleClient 接受 ctx,取消时关闭连接解除 IO 阻塞 * refactor: 引入 ScanSession,替代全局状态穿透扫描管道 (Phase 1-3) - 新增 common/session.go: ScanSession 结构体封装 Config/State/Params/Dialer - RunScan/Strategy/ExecuteScanTasks/executeScanTask 全部接收 session - Plugin 接口从 Scan(ctx, info, config, state) 改为 Scan(ctx, info, session) - 48 个插件实现统一更新签名 - Web API 构建 ScanSession 传给 RunScan - CLI 模式通过 Initialize() 创建 session * refactor: 全量替换 WrapperTcpWithTimeout 为 session.DialTCP (Phase 4) - core/port_scan.go: EnhancedPortScan/connectWithRetry/scanSinglePort 接入 session - core/service_probe.go: SmartPortInfoScanner 持有 session,重连走 session.DialTCP - core/icmp.go: CheckLive/tcpProbeAlive 接入 session - 17 个 service 插件: 内部 helper 函数全部穿透 ctx+session - 移除插件中冗余的手动 TCP 计数(DialTCP 内部已处理) - plugins/core 下已无 WrapperTcpWithTimeout/SafeTCPDial 调用残留 * refactor: 清除 core/plugins 全局状态依赖,ProgressManager 缓存引用 (Phase 5) - core/alive_scanner.go: GetFlagVars() → session.Params - core/service_scanner.go: GetFlagVars() → session.Params 和 config.Target.Ports - common/progress_manager.go: 缓存 State 和 NoColor 到字段,不再运行时读全局 - common/output_api.go: SaveResult 改用 GetGlobalConfig().Output.DisableSave - common/network.go: WrapperTcpWithTimeout 标记 Deprecated - core/ 和 plugins/ 下已无全局状态调用残留 * fix: 修复 dialer timeout 锁死、CVE 检测绕过 session 和误报问题 * fix: 修复 pocDNSLog data race,穿透 ctx 到全链路,消除残余 net.DialTimeout 绕过 * perf: CVE-2026-24061 检测改并发执行,消除硬 sleep 用 deadline 替代 * feat: 项目缓存系统,跨扫描合并资产,缓存 host:port 避免漏报 * perf: 三阶段性能优化,ICMP 并发提升+TCP 并行探测,端口扫描退避调整,服务探测超时减半 * fix: 修复凭据测试清理 goroutine 无限阻塞导致的 goroutine 泄漏 * fix: 凭据测试连续网络错误短路、resultChan 缓冲防阻塞、timer 泄漏修复 * perf: 大规模扫描网段预筛,按 /24 探活跳过空子网,B 段扫描从 2h+ 降至 2min * fix: 网段预筛从抽样改全覆盖,每台主机发 1 个探测包,消除漏报 * perf: 网段预筛增加网关启发式,.1/.254 多端口优先探测,命中即跳过逐主机兜底 * fix: MSSQL 连接加 encrypt=disable 修复无 TLS 环境扫描失败,Web API 参数校验负数 * feat: Release 增加 armv5 架构支持 * chore: bump version to 2.1.3 * fix: 锁定 golangci-lint 版本为 v2.12.1 修复 CI checksum 校验失败 * fix: golangci-lint 改用 go install 安装,绕过上游安装脚本 checksum 校验问题 * feat: -silent 模式输出 NDJSON 到 stdout,支持 AI agent 管道消费 - 新增 StdoutNDJSONWriter,silent 模式下每条扫描结果实时输出一行 JSON - LogWithProgress 层拦截人类可读日志,绕过 logger sync.Once 初始化时序问题 - 支持 fscan -h xxx -silent | jq 管道用法 * fix rdp invalid random panic (#573) * restore ms17010 legacy detection and exploit (#574) * fix ms17010 legacy packet decoding (#574) * fix csv web title output (#575) * fix web result protocol output (#577) * feat: add -ntp flag to disable TCP supplementary probe * fix: skip TCP supplementary probe in icmp mode * feat: add -debug flag with file logging to fscan_debug.log * fix: resolve golangci-lint errcheck and staticcheck warnings * fix: skip proxy deep verification for SOCKS5 connections (#579) SOCKS5 protocol validates connection reachability at protocol level, deep verification was incorrectly rejecting non-banner services like SMB(445), RPC(139) and Kerberos(88). * fix: exclude timeout from scan failure rate calculation (#578) Timeout is a normal scan result when firewalls drop packets, not a scan failure. Only resource exhaustion errors count toward failure rate. * feat: flatten NDJSON output for AI agent consumption and add SKILL.md * perf: 端口扫描自适应超时,基于 RTT 采样动态调整连接超时 * perf: 四项扫描性能优化 - SO_LINGER=0 快速释放连接,减少 TIME_WAIT 堆积 - 服务探测超时自适应,RTT 采样约束读超时上限 - 端口扫描结果流式传递,pipeline 并行端口扫描和插件执行 - ICMP 批量预构建包和地址,减少发送循环开销 * perf: 六项性能优化 - DNS 解析缓存:sync.Map 缓存避免重复系统调用 - 凭据测试 TCP 预检:不可达目标直接跳过全部凭据 - Web 探测 HTTP Client 复用:全局共享连接池 - 端口扫描 Bloom Filter 去重:替代 map 降低内存 - 进度条 atomic 累加 + 50ms 节流渲染:消除锁竞争 - 服务探针预解码:Init 时预编译,运行时零解码开销 * refactor: replace bloom filter with map for deduplication Bloom filter has false positive risk which can silently drop valid scan results. Map provides exact deduplication with negligible memory overhead at the scale of open ports (typically thousands, not millions). * fix: credential TCP precheck bypass proxy and pipeline goroutine leak - Skip TCP precheck when proxy is enabled, net.DialTimeout cannot reach targets behind SOCKS5/HTTP proxy - Drain stream channel on ctx cancellation to prevent EnhancedPortScan goroutine from blocking on a full channel * fix: stream channel 提前返回未关闭导致 goroutine 泄漏,服务探测超时下限 500ms * fix: resolve golangci-lint errcheck and staticcheck warnings --------- Co-authored-by: r00t <[email protected]>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AdaptiveTimeout 基于 RTT 采样的自适应超时计算器
|
||||
// 算法:timeout = mean(RTT) + 4 * stddev(RTT),clamp 到 [min, max]
|
||||
// 冷启动阶段(样本不足)返回用户配置的固定超时
|
||||
type AdaptiveTimeout struct {
|
||||
mu sync.Mutex
|
||||
samples []float64 // 环形缓冲区,单位 ms
|
||||
pos int // 写入位置
|
||||
count int // 已采集总数
|
||||
size int // 缓冲区容量
|
||||
minTO time.Duration
|
||||
maxTO time.Duration
|
||||
warmup int // 冷启动所需最小样本数
|
||||
cachedTO time.Duration
|
||||
dirty bool
|
||||
}
|
||||
|
||||
// NewAdaptiveTimeout 创建自适应超时计算器
|
||||
// maxTimeout: 用户配置的超时上限(即原始固定超时)
|
||||
func NewAdaptiveTimeout(maxTimeout time.Duration) *AdaptiveTimeout {
|
||||
return &AdaptiveTimeout{
|
||||
samples: make([]float64, 64),
|
||||
size: 64,
|
||||
minTO: 100 * time.Millisecond,
|
||||
maxTO: maxTimeout,
|
||||
warmup: 10,
|
||||
}
|
||||
}
|
||||
|
||||
// Record 记录一次成功连接的 RTT
|
||||
func (a *AdaptiveTimeout) Record(rtt time.Duration) {
|
||||
a.mu.Lock()
|
||||
a.samples[a.pos%a.size] = float64(rtt.Milliseconds())
|
||||
a.pos++
|
||||
a.count++
|
||||
a.dirty = true
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// Timeout 获取当前推荐超时值
|
||||
// 样本不足时返回 maxTO(冷启动)
|
||||
func (a *AdaptiveTimeout) Timeout() time.Duration {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if a.count < a.warmup {
|
||||
return a.maxTO
|
||||
}
|
||||
|
||||
if !a.dirty {
|
||||
return a.cachedTO
|
||||
}
|
||||
|
||||
n := a.size
|
||||
if a.count < a.size {
|
||||
n = a.count
|
||||
}
|
||||
|
||||
var sum float64
|
||||
for i := 0; i < n; i++ {
|
||||
sum += a.samples[i]
|
||||
}
|
||||
mean := sum / float64(n)
|
||||
|
||||
var variance float64
|
||||
for i := 0; i < n; i++ {
|
||||
d := a.samples[i] - mean
|
||||
variance += d * d
|
||||
}
|
||||
stddev := math.Sqrt(variance / float64(n))
|
||||
|
||||
ms := mean + 4*stddev
|
||||
to := time.Duration(ms) * time.Millisecond
|
||||
|
||||
if to < a.minTO {
|
||||
to = a.minTO
|
||||
}
|
||||
if to > a.maxTO {
|
||||
to = a.maxTO
|
||||
}
|
||||
|
||||
a.cachedTO = to
|
||||
a.dirty = false
|
||||
return to
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -53,27 +54,24 @@ func (s *AliveScanStrategy) Description() string {
|
||||
}
|
||||
|
||||
// Execute 执行存活探测扫描策略
|
||||
func (s *AliveScanStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func (s *AliveScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
|
||||
fv := common.GetFlagVars()
|
||||
if info.Host == "" && fv.HostsFile == "" {
|
||||
if info.Host == "" && session.Params.HostsFile == "" {
|
||||
common.LogError(i18n.GetText("parse_error_target_empty"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 执行存活探测
|
||||
s.performAliveScan(info, config, state)
|
||||
s.performAliveScan(ctx, info, session)
|
||||
|
||||
// 输出统计信息
|
||||
s.outputStats()
|
||||
}
|
||||
|
||||
// performAliveScan 执行存活探测
|
||||
func (s *AliveScanStrategy) performAliveScan(info common.HostInfo, config *common.Config, state *common.State) {
|
||||
func (s *AliveScanStrategy) performAliveScan(ctx context.Context, info common.HostInfo, session *common.ScanSession) {
|
||||
// 解析目标主机
|
||||
fv := common.GetFlagVars()
|
||||
hosts, err := parsers.ParseIP(info.Host, fv.HostsFile, fv.ExcludeHosts)
|
||||
hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("parse_target_failed", err))
|
||||
return
|
||||
@@ -91,7 +89,7 @@ func (s *AliveScanStrategy) performAliveScan(info common.HostInfo, config *commo
|
||||
|
||||
|
||||
// 执行存活检测
|
||||
aliveList := CheckLive(hosts, false, config, state) // 使用ICMP探测
|
||||
aliveList := CheckLive(ctx, hosts, false, session) // 使用ICMP探测
|
||||
|
||||
// 更新统计信息
|
||||
s.stats.AliveHosts = len(aliveList)
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"hash/fnv"
|
||||
)
|
||||
|
||||
// BloomFilter 布隆过滤器,用于ICMP包去重
|
||||
type BloomFilter struct {
|
||||
bits []bool
|
||||
size uint32
|
||||
k uint32 // hash函数数量
|
||||
}
|
||||
|
||||
// NewBloomFilter 创建布隆过滤器
|
||||
// size: 预期元素数量
|
||||
// falsePositiveRate: 期望的误判率(通常0.01即1%)
|
||||
func NewBloomFilter(size int, falsePositiveRate float64) *BloomFilter {
|
||||
// 计算最优bit数组大小: m = -n*ln(p) / (ln(2)^2)
|
||||
// 简化计算:m ≈ n * 10 for p=0.01
|
||||
m := uint32(size * 10)
|
||||
if m < 1024 {
|
||||
m = 1024 // 最小1KB
|
||||
}
|
||||
|
||||
// 计算最优hash函数数量: k = (m/n) * ln(2)
|
||||
// 简化:k ≈ 7 for p=0.01
|
||||
k := uint32(7)
|
||||
|
||||
return &BloomFilter{
|
||||
bits: make([]bool, m),
|
||||
size: m,
|
||||
k: k,
|
||||
}
|
||||
}
|
||||
|
||||
// Add 添加元素到过滤器
|
||||
func (bf *BloomFilter) Add(data string) {
|
||||
for i := uint32(0); i < bf.k; i++ {
|
||||
pos := bf.hash(data, i)
|
||||
bf.bits[pos] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Contains 检查元素是否可能存在
|
||||
// 返回true:可能存在(有误判可能)
|
||||
// 返回false:一定不存在
|
||||
func (bf *BloomFilter) Contains(data string) bool {
|
||||
for i := uint32(0); i < bf.k; i++ {
|
||||
pos := bf.hash(data, i)
|
||||
if !bf.bits[pos] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// hash 计算hash值
|
||||
func (bf *BloomFilter) hash(data string, seed uint32) uint32 {
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(data))
|
||||
// 添加seed实现多个hash函数
|
||||
for i := uint32(0); i < seed; i++ {
|
||||
_, _ = h.Write([]byte{byte(i)})
|
||||
}
|
||||
return h.Sum32() % bf.size
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/*
|
||||
bloom_filter_test.go - BloomFilter 高价值测试
|
||||
|
||||
测试重点:
|
||||
1. 基本正确性 - Add后Contains返回true,未添加的返回false
|
||||
2. 误判率验证 - 实际误判率应接近理论值(1%)
|
||||
3. 大规模数据 - 模拟真实ICMP去重场景
|
||||
|
||||
不测试:
|
||||
- 内部哈希实现细节
|
||||
- 精确的数学公式验证
|
||||
*/
|
||||
|
||||
// TestBloomFilter_BasicCorrectness 基本正确性测试
|
||||
func TestBloomFilter_BasicCorrectness(t *testing.T) {
|
||||
bf := NewBloomFilter(1000, 0.01)
|
||||
|
||||
// 添加元素后应该能找到
|
||||
testData := []string{
|
||||
"192.168.1.1",
|
||||
"10.0.0.1",
|
||||
"172.16.0.1",
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
bf.Add(data)
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !bf.Contains(data) {
|
||||
t.Errorf("已添加的元素 %s 应该返回 true", data)
|
||||
}
|
||||
}
|
||||
|
||||
// 未添加的元素(大概率)返回false
|
||||
notAdded := []string{
|
||||
"8.8.8.8",
|
||||
"1.1.1.1",
|
||||
"255.255.255.255",
|
||||
}
|
||||
|
||||
falsePositives := 0
|
||||
for _, data := range notAdded {
|
||||
if bf.Contains(data) {
|
||||
falsePositives++
|
||||
}
|
||||
}
|
||||
|
||||
// 3个未添加元素全部误判的概率极低(<0.0001%)
|
||||
if falsePositives == len(notAdded) {
|
||||
t.Error("所有未添加元素都返回true,布隆过滤器可能有问题")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBloomFilter_FalsePositiveRate 误判率验证
|
||||
//
|
||||
// 对于 n=10000, p=0.01 的布隆过滤器:
|
||||
// 实际误判率应该在 0.5% - 2% 之间(允许统计波动)
|
||||
func TestBloomFilter_FalsePositiveRate(t *testing.T) {
|
||||
n := 10000 // 添加的元素数
|
||||
bf := NewBloomFilter(n, 0.01)
|
||||
|
||||
// 添加n个元素
|
||||
for i := 0; i < n; i++ {
|
||||
bf.Add(fmt.Sprintf("added_%d", i))
|
||||
}
|
||||
|
||||
// 测试n个未添加的元素
|
||||
falsePositives := 0
|
||||
testCount := n
|
||||
for i := 0; i < testCount; i++ {
|
||||
if bf.Contains(fmt.Sprintf("not_added_%d", i)) {
|
||||
falsePositives++
|
||||
}
|
||||
}
|
||||
|
||||
actualRate := float64(falsePositives) / float64(testCount)
|
||||
|
||||
// 允许的误判率范围:0.1% - 3%(考虑统计波动)
|
||||
if actualRate > 0.03 {
|
||||
t.Errorf("误判率过高: %.2f%% (期望 < 3%%)", actualRate*100)
|
||||
}
|
||||
|
||||
t.Logf("实际误判率: %.2f%% (%d/%d)", actualRate*100, falsePositives, testCount)
|
||||
}
|
||||
|
||||
// TestBloomFilter_LargeScale 大规模数据测试
|
||||
//
|
||||
// 模拟真实的ICMP去重场景:100万个IP地址
|
||||
func TestBloomFilter_LargeScale(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("跳过大规模测试")
|
||||
}
|
||||
|
||||
n := 1000000 // 100万
|
||||
bf := NewBloomFilter(n, 0.01)
|
||||
|
||||
// 添加100万个元素
|
||||
for i := 0; i < n; i++ {
|
||||
bf.Add(fmt.Sprintf("192.168.%d.%d", i/256, i%256))
|
||||
}
|
||||
|
||||
// 验证已添加的元素
|
||||
sampleSize := 1000
|
||||
for i := 0; i < sampleSize; i++ {
|
||||
idx := i * (n / sampleSize)
|
||||
data := fmt.Sprintf("192.168.%d.%d", idx/256, idx%256)
|
||||
if !bf.Contains(data) {
|
||||
t.Errorf("已添加的元素 %s 返回 false", data)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试未添加元素的误判率
|
||||
falsePositives := 0
|
||||
for i := 0; i < sampleSize; i++ {
|
||||
if bf.Contains(fmt.Sprintf("10.%d.%d.%d", i/65536, (i/256)%256, i%256)) {
|
||||
falsePositives++
|
||||
}
|
||||
}
|
||||
|
||||
actualRate := float64(falsePositives) / float64(sampleSize)
|
||||
if actualRate > 0.03 {
|
||||
t.Errorf("大规模场景误判率过高: %.2f%%", actualRate*100)
|
||||
}
|
||||
|
||||
t.Logf("100万元素场景误判率: %.2f%%", actualRate*100)
|
||||
}
|
||||
|
||||
// TestBloomFilter_NoFalseNegative 验证无假阴性
|
||||
//
|
||||
// 布隆过滤器的核心保证:已添加的元素必定返回true
|
||||
func TestBloomFilter_NoFalseNegative(t *testing.T) {
|
||||
bf := NewBloomFilter(10000, 0.01)
|
||||
|
||||
// 添加5000个元素
|
||||
added := make([]string, 5000)
|
||||
for i := range added {
|
||||
added[i] = fmt.Sprintf("element_%d", i)
|
||||
bf.Add(added[i])
|
||||
}
|
||||
|
||||
// 全部验证
|
||||
for _, data := range added {
|
||||
if !bf.Contains(data) {
|
||||
t.Fatalf("假阴性!已添加的元素 %s 返回 false", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBloomFilter_EmptyFilter 空过滤器测试
|
||||
func TestBloomFilter_EmptyFilter(t *testing.T) {
|
||||
bf := NewBloomFilter(100, 0.01)
|
||||
|
||||
// 空过滤器应该对任何查询返回false
|
||||
testCases := []string{"anything", "192.168.1.1", ""}
|
||||
for _, tc := range testCases {
|
||||
if bf.Contains(tc) {
|
||||
t.Errorf("空过滤器对 %q 返回 true", tc)
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
-31
@@ -2,6 +2,7 @@ package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -39,7 +40,9 @@ var pingErrorKeywords = []string{
|
||||
|
||||
// CheckLive 检测主机存活状态
|
||||
// 支持 ICMP/Ping 探测,并在响应率过低时自动启用 TCP 补充探测
|
||||
func CheckLive(hostslist []string, Ping bool, config *common.Config, state *common.State) []string {
|
||||
func CheckLive(ctx context.Context, hostslist []string, Ping bool, session *common.ScanSession) []string {
|
||||
config := session.Config
|
||||
state := session.State
|
||||
// 创建局部WaitGroup
|
||||
var livewg sync.WaitGroup
|
||||
|
||||
@@ -68,7 +71,7 @@ func CheckLive(hostslist []string, Ping bool, config *common.Config, state *comm
|
||||
|
||||
// TCP 补充探测:当 ICMP/Ping 响应率过低时自动启用
|
||||
// 这对防火墙过滤 ICMP 的环境特别有用
|
||||
aliveHosts = tcpSupplementaryProbe(hostslist, aliveHosts, config)
|
||||
aliveHosts = tcpSupplementaryProbe(ctx, hostslist, aliveHosts, session)
|
||||
|
||||
// 输出存活统计信息
|
||||
printAliveStats(aliveHosts, hostslist)
|
||||
@@ -78,7 +81,11 @@ func CheckLive(hostslist []string, Ping bool, config *common.Config, state *comm
|
||||
|
||||
// tcpSupplementaryProbe TCP 补充探测
|
||||
// 当 ICMP 响应率过低时(<10%),对未响应主机进行 TCP 探测
|
||||
func tcpSupplementaryProbe(allHosts []string, aliveHosts []string, config *common.Config) []string {
|
||||
func tcpSupplementaryProbe(ctx context.Context, allHosts []string, aliveHosts []string, session *common.ScanSession) []string {
|
||||
if session.Config.DisableTcpProbe || session.Config.Mode == "icmp" {
|
||||
return aliveHosts
|
||||
}
|
||||
|
||||
totalHosts := len(allHosts)
|
||||
if totalHosts == 0 {
|
||||
return aliveHosts
|
||||
@@ -102,7 +109,7 @@ func tcpSupplementaryProbe(allHosts []string, aliveHosts []string, config *commo
|
||||
common.LogInfo(i18n.Tr("tcp_probe_low_icmp_rate", fmt.Sprintf("%.1f%%", responseRate*100), len(unrespondedHosts)))
|
||||
|
||||
// 执行 TCP 补充探测
|
||||
tcpAliveHosts := runTcpProbeForHosts(unrespondedHosts, config)
|
||||
tcpAliveHosts := runTcpProbeForHosts(ctx, unrespondedHosts, session)
|
||||
|
||||
// 合并结果
|
||||
if len(tcpAliveHosts) > 0 {
|
||||
@@ -321,8 +328,8 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string,
|
||||
var endflag atomic.Bool
|
||||
var listenerWg sync.WaitGroup
|
||||
|
||||
// 创建布隆过滤器用于去重(自动根据主机数量调整大小)
|
||||
bloomFilter := NewBloomFilter(len(hostslist), 0.01)
|
||||
// 去重集合:过滤重复的ICMP响应
|
||||
seen := make(map[string]struct{}, len(hostslist))
|
||||
|
||||
// 启动监听协程
|
||||
listenerWg.Add(1)
|
||||
@@ -358,11 +365,10 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string,
|
||||
if sourceIP != nil && !endflag.Load() {
|
||||
ipStr := sourceIP.String()
|
||||
|
||||
// 使用布隆过滤器去重,过滤重复的ICMP响应和杂包
|
||||
if bloomFilter.Contains(ipStr) {
|
||||
if _, dup := seen[ipStr]; dup {
|
||||
continue
|
||||
}
|
||||
bloomFilter.Add(ipStr)
|
||||
seen[ipStr] = struct{}{}
|
||||
|
||||
livewg.Add(1)
|
||||
select {
|
||||
@@ -376,13 +382,22 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string,
|
||||
}
|
||||
}()
|
||||
|
||||
// 发送ICMP请求(应用令牌桶限速)
|
||||
limiter := state.GetICMPLimiter(config.Network.ICMPRate)
|
||||
// 发送ICMP请求(批量预构建 + 令牌桶限速)
|
||||
// 预构建所有 ICMP 包和目标地址,减少发送循环中的开销
|
||||
type icmpPacket struct {
|
||||
data []byte
|
||||
dst net.Addr
|
||||
}
|
||||
packets := make([]icmpPacket, 0, len(hostslist))
|
||||
for _, host := range hostslist {
|
||||
limiter.Wait(1) // 等待令牌,控制发包速率
|
||||
dst, _ := net.ResolveIPAddr("ip", host)
|
||||
IcmpByte := makemsg(host)
|
||||
_, _ = conn.WriteTo(IcmpByte, dst)
|
||||
dst, _ := common.DNSCache.ResolveIP(host)
|
||||
packets = append(packets, icmpPacket{data: makemsg(host), dst: dst})
|
||||
}
|
||||
|
||||
limiter := state.GetICMPLimiter(config.Network.ICMPRate)
|
||||
for i := range packets {
|
||||
limiter.Wait(1)
|
||||
_, _ = conn.WriteTo(packets[i].data, packets[i].dst)
|
||||
}
|
||||
|
||||
// 自适应等待响应
|
||||
@@ -470,8 +485,12 @@ func icmpalive(host string) bool {
|
||||
// RunPing 使用系统Ping命令并发探测主机存活
|
||||
func RunPing(hostslist []string, chanHosts chan string, livewg *sync.WaitGroup) {
|
||||
var wg sync.WaitGroup
|
||||
// 限制并发数为50
|
||||
limiter := make(chan struct{}, 50)
|
||||
// 并发数根据主机数动态调整,上限 200
|
||||
concurrency := len(hostslist)
|
||||
if concurrency > 200 {
|
||||
concurrency = 200
|
||||
}
|
||||
limiter := make(chan struct{}, concurrency)
|
||||
|
||||
// 并发探测
|
||||
for _, host := range hostslist {
|
||||
@@ -674,20 +693,34 @@ func ArrayCountValueTop(arrInit []string, length int, flag bool) (arrTop []strin
|
||||
var tcpProbeCommonPorts = []int{80, 443, 22, 445}
|
||||
|
||||
// tcpProbeTimeout TCP 探测超时时间(较短,只做存活判断)
|
||||
const tcpProbeTimeout = 2 * time.Second
|
||||
const tcpProbeTimeout = 1 * time.Second
|
||||
|
||||
// tcpProbeThreshold TCP 补充探测触发阈值
|
||||
// 当 ICMP 响应率低于此值时,自动启用 TCP 补充探测
|
||||
const tcpProbeThreshold = 0.1 // 10%
|
||||
|
||||
// tcpProbeAlive 使用 TCP 探测主机是否存活
|
||||
// 尝试连接常用端口,任一端口响应即认为存活
|
||||
func tcpProbeAlive(host string) bool {
|
||||
// tcpProbeAlive 使用 TCP 并行探测主机是否存活
|
||||
// 同时连接所有常用端口,任一响应即返回
|
||||
func tcpProbeAlive(ctx context.Context, session *common.ScanSession, host string) bool {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
result := make(chan bool, len(tcpProbeCommonPorts))
|
||||
for _, port := range tcpProbeCommonPorts {
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, tcpProbeTimeout)
|
||||
if err == nil {
|
||||
_ = conn.Close()
|
||||
go func(p int) {
|
||||
addr := fmt.Sprintf("%s:%d", host, p)
|
||||
conn, err := session.DialTCP(ctx, "tcp", addr, tcpProbeTimeout)
|
||||
if err == nil {
|
||||
_ = conn.Close()
|
||||
result <- true
|
||||
return
|
||||
}
|
||||
result <- false
|
||||
}(port)
|
||||
}
|
||||
|
||||
for range tcpProbeCommonPorts {
|
||||
if <-result {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -696,7 +729,8 @@ func tcpProbeAlive(host string) bool {
|
||||
|
||||
// runTcpProbeForHosts 对指定主机列表进行 TCP 补充探测
|
||||
// 返回存活的主机列表
|
||||
func runTcpProbeForHosts(hosts []string, config *common.Config) []string {
|
||||
func runTcpProbeForHosts(ctx context.Context, hosts []string, session *common.ScanSession) []string {
|
||||
config := session.Config
|
||||
if len(hosts) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -705,10 +739,10 @@ func runTcpProbeForHosts(hosts []string, config *common.Config) []string {
|
||||
var mu sync.Mutex
|
||||
aliveHosts := make([]string, 0)
|
||||
|
||||
// 并发控制,避免资源耗尽
|
||||
concurrency := 50
|
||||
if len(hosts) < concurrency {
|
||||
concurrency = len(hosts)
|
||||
// 并发控制,根据主机数动态调整,上限 200
|
||||
concurrency := len(hosts)
|
||||
if concurrency > 200 {
|
||||
concurrency = 200
|
||||
}
|
||||
limiter := make(chan struct{}, concurrency)
|
||||
|
||||
@@ -722,7 +756,7 @@ func runTcpProbeForHosts(hosts []string, config *common.Config) []string {
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
if tcpProbeAlive(h) {
|
||||
if tcpProbeAlive(ctx, session, h) {
|
||||
mu.Lock()
|
||||
aliveHosts = append(aliveHosts, h)
|
||||
mu.Unlock()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
@@ -41,7 +42,9 @@ func (s *LocalScanStrategy) Description() string {
|
||||
}
|
||||
|
||||
// Execute 执行本地扫描策略
|
||||
func (s *LocalScanStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func (s *LocalScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
config := session.Config
|
||||
|
||||
// 输出扫描开始信息
|
||||
s.LogScanStart()
|
||||
|
||||
@@ -66,7 +69,7 @@ func (s *LocalScanStrategy) Execute(config *common.Config, state *common.State,
|
||||
targets := s.PrepareTargets(info)
|
||||
|
||||
// 执行扫描任务
|
||||
ExecuteScanTasks(config, state, targets, s, ch, wg)
|
||||
ExecuteScanTasks(ctx, session, targets, s, ch, wg)
|
||||
}
|
||||
|
||||
// PrepareTargets 准备本地扫描目标
|
||||
|
||||
+217
-34
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
@@ -38,27 +39,32 @@ var resourceExhaustedPatterns = []string{
|
||||
}
|
||||
|
||||
// resultCollector 结果收集器,用于并发安全地收集扫描结果
|
||||
// 使用 map 实现:O(1) 的添加和删除,无顺序依赖问题
|
||||
type resultCollector struct {
|
||||
mu sync.Mutex
|
||||
addrs map[string]struct{}
|
||||
mu sync.Mutex
|
||||
addrs map[string]struct{}
|
||||
stream chan<- string
|
||||
}
|
||||
|
||||
// newResultCollector 创建结果收集器
|
||||
func newResultCollector() *resultCollector {
|
||||
func newResultCollector(stream chan<- string) *resultCollector {
|
||||
return &resultCollector{
|
||||
addrs: make(map[string]struct{}),
|
||||
addrs: make(map[string]struct{}),
|
||||
stream: stream,
|
||||
}
|
||||
}
|
||||
|
||||
// Add 添加一个扫描结果
|
||||
func (c *resultCollector) Add(addr string) {
|
||||
c.mu.Lock()
|
||||
if _, dup := c.addrs[addr]; dup {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.addrs[addr] = struct{}{}
|
||||
c.mu.Unlock()
|
||||
if c.stream != nil {
|
||||
c.stream <- addr
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll 获取所有结果
|
||||
func (c *resultCollector) GetAll() []string {
|
||||
c.mu.Lock()
|
||||
result := make([]string, 0, len(c.addrs))
|
||||
@@ -110,13 +116,31 @@ func (f *failedPortCollector) Count() int {
|
||||
|
||||
// EnhancedPortScan 高性能端口扫描函数
|
||||
// 使用滑动窗口调度 + 自适应线程池 + 流式迭代器
|
||||
func EnhancedPortScan(hosts []string, ports string, timeout int64, config *common.Config, state *common.State) []string {
|
||||
// stream: 可选,非 nil 时每发现开放端口立即发送 addr,扫描结束后关闭
|
||||
func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout int64, session *common.ScanSession, stream chan<- string) []string {
|
||||
config := session.Config
|
||||
state := session.State
|
||||
common.LogDebug(fmt.Sprintf("[PortScan] 开始: %d个主机, 线程数=%d", len(hosts), config.ThreadNum))
|
||||
|
||||
// 大规模扫描预筛:跨多个 /24 时先做网段探活,跳过空网段
|
||||
if len(hosts) > subnetProbeThreshold {
|
||||
hosts = probeSubnets(ctx, hosts, time.Duration(timeout)*time.Second, session)
|
||||
if len(hosts) == 0 {
|
||||
common.LogInfo(i18n.GetText("port_scan_no_alive_subnet"))
|
||||
if stream != nil {
|
||||
close(stream)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 解析端口和排除端口
|
||||
portList := parsers.ParsePort(ports)
|
||||
if len(portList) == 0 {
|
||||
common.LogError(i18n.Tr("invalid_port", ports))
|
||||
if stream != nil {
|
||||
close(stream)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
common.LogDebug(fmt.Sprintf("[PortScan] 端口解析完成: %d个端口", len(portList)))
|
||||
@@ -161,8 +185,9 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
|
||||
|
||||
// 初始化并发控制
|
||||
to := time.Duration(timeout) * time.Second
|
||||
adaptiveTO := NewAdaptiveTimeout(to)
|
||||
var count int64
|
||||
collector := newResultCollector()
|
||||
collector := newResultCollector(stream)
|
||||
failedCollector := &failedPortCollector{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
@@ -179,11 +204,14 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
|
||||
}()
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", taskInfo.host, taskInfo.port)
|
||||
scanSinglePort(taskInfo.host, taskInfo.port, addr, to, &count, collector, failedCollector, config, state)
|
||||
scanSinglePort(ctx, taskInfo.host, taskInfo.port, addr, adaptiveTO, &count, collector, failedCollector, session)
|
||||
common.UpdateProgressBar(1)
|
||||
}, state)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("thread_pool_create_failed", err))
|
||||
if stream != nil {
|
||||
close(stream)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
common.LogDebug("[PortScan] 线程池创建成功")
|
||||
@@ -197,6 +225,11 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
|
||||
// 收集结果
|
||||
aliveAddrs := collector.GetAll()
|
||||
|
||||
// 关闭流式通知 channel
|
||||
if stream != nil {
|
||||
close(stream)
|
||||
}
|
||||
|
||||
// 完成端口扫描进度条
|
||||
if common.IsProgressActive() {
|
||||
common.FinishProgressBar()
|
||||
@@ -252,7 +285,10 @@ func slidingWindowSchedule(iter *SocketIterator, pool *AdaptivePool, wg *sync.Wa
|
||||
port: port,
|
||||
semaphore: semaphore,
|
||||
}
|
||||
_ = pool.Invoke(task)
|
||||
if err := pool.Invoke(task); err != nil {
|
||||
<-semaphore
|
||||
wg.Done()
|
||||
}
|
||||
}
|
||||
|
||||
// 等待所有任务完成
|
||||
@@ -260,11 +296,11 @@ func slidingWindowSchedule(iter *SocketIterator, pool *AdaptivePool, wg *sync.Wa
|
||||
}
|
||||
|
||||
// connectWithRetry 带重试的TCP连接 - 只对资源耗尽错误重试
|
||||
func connectWithRetry(addr string, timeout time.Duration, maxRetries int, state *common.State) (net.Conn, error) {
|
||||
func connectWithRetry(ctx context.Context, session *common.ScanSession, addr string, timeout time.Duration, maxRetries int) (net.Conn, error) {
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
||||
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
|
||||
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
@@ -278,11 +314,11 @@ func connectWithRetry(addr string, timeout time.Duration, maxRetries int, state
|
||||
}
|
||||
|
||||
// 记录资源耗尽错误
|
||||
state.IncrementResourceExhaustedCount()
|
||||
session.State.IncrementResourceExhaustedCount()
|
||||
|
||||
// 指数退避:第1次等50ms,第2次等150ms
|
||||
// 指数退避:200ms → 600ms → 1200ms
|
||||
if attempt < maxRetries-1 {
|
||||
waitTime := time.Duration(50*(attempt+1)) * time.Millisecond
|
||||
waitTime := time.Duration(200*(1<<uint(attempt))) * time.Millisecond
|
||||
time.Sleep(waitTime)
|
||||
}
|
||||
}
|
||||
@@ -341,13 +377,17 @@ func buildServiceLogMessage(addr string, serviceInfo *ServiceInfo, isWeb bool) s
|
||||
}
|
||||
|
||||
// scanSinglePort 扫描单个端口并进行服务识别(重构后的简洁版本)
|
||||
func scanSinglePort(host string, port int, addr string, timeout time.Duration, count *int64, collector *resultCollector, failedCollector *failedPortCollector, config *common.Config, state *common.State) {
|
||||
func scanSinglePort(ctx context.Context, host string, port int, addr string, adaptiveTO *AdaptiveTimeout, count *int64, collector *resultCollector, failedCollector *failedPortCollector, session *common.ScanSession) {
|
||||
config := session.Config
|
||||
timeout := adaptiveTO.Timeout()
|
||||
// 步骤1:建立连接
|
||||
conn, err := connectWithRetry(addr, timeout, 3, state)
|
||||
start := time.Now()
|
||||
conn, err := connectWithRetry(ctx, session, addr, timeout, 2)
|
||||
if err != nil {
|
||||
handleConnectionFailure(err, host, port, addr, failedCollector)
|
||||
return
|
||||
}
|
||||
adaptiveTO.Record(time.Since(start))
|
||||
|
||||
// 步骤1.5:代理连接深度验证(防止透明代理/全回显代理的假连接问题)
|
||||
valid, verifyMethod := verifyProxyConnectionDeep(conn, addr)
|
||||
@@ -362,7 +402,7 @@ func scanSinglePort(host string, port int, addr string, timeout time.Duration, c
|
||||
if common.IsProxyEnabled() && verifyMethod != "direct" {
|
||||
_ = conn.Close()
|
||||
// 重新建立干净的连接用于服务识别
|
||||
conn, err = connectWithRetry(addr, timeout, 3, state)
|
||||
conn, err = connectWithRetry(ctx, session, addr, timeout, 2)
|
||||
if err != nil {
|
||||
handleConnectionFailure(err, host, port, addr, failedCollector)
|
||||
return
|
||||
@@ -375,26 +415,31 @@ func scanSinglePort(host string, port int, addr string, timeout time.Duration, c
|
||||
saveOpenPort(host, port)
|
||||
|
||||
// 步骤3:服务识别(Scanner负责关闭连接,包括探测中可能创建的新连接)
|
||||
scanner := NewSmartPortInfoScanner(host, port, conn, timeout, config)
|
||||
scanner := NewSmartPortInfoScanner(ctx, host, port, conn, timeout, config, session)
|
||||
// 服务探测超时自适应:用 RTT 采样值约束读超时上限
|
||||
// 下限 500ms:服务处理需要时间,不能太激进
|
||||
if rttTO := adaptiveTO.Timeout(); rttTO < timeout {
|
||||
maxMS := int(rttTO.Milliseconds()) * 6
|
||||
if maxMS < 500 {
|
||||
maxMS = 500
|
||||
}
|
||||
scanner.info.maxReadTimeoutMS = maxMS
|
||||
}
|
||||
defer scanner.Close()
|
||||
serviceInfo, _ := scanner.SmartIdentify()
|
||||
|
||||
// 步骤4:处理结果
|
||||
processServiceResult(host, port, addr, serviceInfo, config)
|
||||
processServiceResult(host, port, addr, serviceInfo, config, session)
|
||||
}
|
||||
|
||||
// handleConnectionFailure 处理连接失败
|
||||
// 只收集资源耗尽类错误,timeout 是正常的扫描结果(防火墙 drop)不计入失败
|
||||
func handleConnectionFailure(err error, host string, port int, addr string, failedCollector *failedPortCollector) {
|
||||
if isResourceExhaustedError(err) || isTimeoutError(err) {
|
||||
if isResourceExhaustedError(err) {
|
||||
failedCollector.Add(host, port, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// isTimeoutError 判断是否为超时错误
|
||||
func isTimeoutError(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "i/o timeout")
|
||||
}
|
||||
|
||||
// verifyProxyConnectionDeep 深度验证代理连接是否真正可用
|
||||
// 防止透明代理/全回显代理的假连接问题
|
||||
// 返回: (是否有效, 验证方式)
|
||||
@@ -404,8 +449,9 @@ func isTimeoutError(err error) bool {
|
||||
// 2. 轻量探测 (发送 \r\n) - 触发某些服务响应,同时不污染协议状态
|
||||
// 3. 短超时等待 (500ms) - 平衡准确性和性能
|
||||
func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
|
||||
// 如果没有使用代理,跳过验证
|
||||
if !common.IsProxyEnabled() {
|
||||
// 无代理或SOCKS5代理:跳过深度验证
|
||||
// SOCKS5协议层已验证连接可达性,连接成功即端口开放
|
||||
if !common.IsProxyEnabled() || common.IsSOCKS5Proxy() {
|
||||
return true, "direct"
|
||||
}
|
||||
|
||||
@@ -550,10 +596,10 @@ func saveOpenPort(host string, port int) {
|
||||
}
|
||||
|
||||
// processServiceResult 处理服务识别结果
|
||||
func processServiceResult(host string, port int, addr string, serviceInfo *ServiceInfo, config *common.Config) {
|
||||
func processServiceResult(host string, port int, addr string, serviceInfo *ServiceInfo, config *common.Config, session *common.ScanSession) {
|
||||
if serviceInfo == nil {
|
||||
// 服务识别失败,尝试 HTTP 回退探测
|
||||
if !tryHTTPFallbackDetection(host, port, addr, config) {
|
||||
if !tryHTTPFallbackDetection(host, port, addr, config, session) {
|
||||
common.LogInfo(i18n.Tr("port_open", addr))
|
||||
}
|
||||
return
|
||||
@@ -613,10 +659,10 @@ func buildServiceDetails(port int, info *ServiceInfo) map[string]interface{} {
|
||||
}
|
||||
|
||||
// tryHTTPFallbackDetection 尝试HTTP回退探测,返回是否成功识别为HTTP服务
|
||||
func tryHTTPFallbackDetection(host string, port int, addr string, config *common.Config) bool {
|
||||
func tryHTTPFallbackDetection(host string, port int, addr string, config *common.Config, session *common.ScanSession) bool {
|
||||
// 使用WebDetection进行HTTP协议探测
|
||||
webDetector := GetWebPortDetector()
|
||||
if !webDetector.DetectHTTPServiceOnly(host, port, config) {
|
||||
if !webDetector.DetectHTTPServiceOnly(host, port, config, session) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -647,3 +693,140 @@ func tryHTTPFallbackDetection(host string, port int, addr string, config *common
|
||||
common.LogInfo(i18n.Tr("port_open_http", addr))
|
||||
return true
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 网段预筛 — 大规模扫描时跳过空 /24 网段
|
||||
// =============================================================================
|
||||
|
||||
// subnetProbeThreshold 触发网段预筛的主机数阈值(超过 1 个 /24)
|
||||
const subnetProbeThreshold = 256
|
||||
|
||||
// subnetProbePorts 逐主机探活用的端口(轮换)
|
||||
var subnetProbePorts = []int{80, 443, 22, 445, 3389, 8080, 3306, 6379}
|
||||
|
||||
// gatewayProbePorts 网关启发式探测端口(网关常开的服务)
|
||||
var gatewayProbePorts = []int{22, 80, 443, 23, 8080, 161, 53, 3389}
|
||||
|
||||
// gatewayOffsets 网关候选地址偏移量
|
||||
var gatewayOffsets = []string{".1", ".254"}
|
||||
|
||||
// subnetProbeTimeout 每个探测的超时
|
||||
const subnetProbeTimeout = 1500 * time.Millisecond
|
||||
|
||||
// subnetProbeConcurrency 网段探活全局并发数
|
||||
const subnetProbeConcurrency = 500
|
||||
|
||||
// probeSubnets 对每个 /24 网段做探活,返回属于存活网段的主机列表
|
||||
// 两阶段策略:
|
||||
//
|
||||
// 阶段 1(快速):对每个子网的 .1/.254 网关做多端口探测,命中即标记存活
|
||||
// 阶段 2(兜底):未命中的子网,逐主机单端口轮换扫描
|
||||
func probeSubnets(ctx context.Context, hosts []string, timeout time.Duration, session *common.ScanSession) []string {
|
||||
// 按 /24 分组
|
||||
subnets := make(map[string][]string)
|
||||
for _, h := range hosts {
|
||||
prefix := subnetPrefix(h)
|
||||
if prefix != "" {
|
||||
subnets[prefix] = append(subnets[prefix], h)
|
||||
}
|
||||
}
|
||||
|
||||
if len(subnets) <= 1 {
|
||||
return hosts
|
||||
}
|
||||
|
||||
common.LogInfo(fmt.Sprintf("网段预筛: %d 个 /24 子网, %d 个主机", len(subnets), len(hosts)))
|
||||
|
||||
aliveSubnets := sync.Map{}
|
||||
var wg sync.WaitGroup
|
||||
limiter := make(chan struct{}, subnetProbeConcurrency)
|
||||
|
||||
// ── 阶段 1:网关启发式 ──────────────────────────────────
|
||||
// 对每个子网的 .1 和 .254 打多个端口,命中率高且速度极快
|
||||
for prefix := range subnets {
|
||||
for _, suffix := range gatewayOffsets {
|
||||
gw := prefix + suffix
|
||||
for _, port := range gatewayProbePorts {
|
||||
wg.Add(1)
|
||||
limiter <- struct{}{}
|
||||
go func(pfx, addr string) {
|
||||
defer func() { <-limiter; wg.Done() }()
|
||||
conn, err := net.DialTimeout("tcp", addr, subnetProbeTimeout)
|
||||
if err == nil {
|
||||
_ = conn.Close()
|
||||
aliveSubnets.Store(pfx, true)
|
||||
}
|
||||
}(prefix, fmt.Sprintf("%s:%d", gw, port))
|
||||
}
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// 统计阶段 1 命中
|
||||
gwHits := 0
|
||||
aliveSubnets.Range(func(_, _ interface{}) bool { gwHits++; return true })
|
||||
|
||||
// ── 阶段 2:逐主机兜底(仅对网关未命中的子网)──────────
|
||||
for prefix, subnetHosts := range subnets {
|
||||
if _, alive := aliveSubnets.Load(prefix); alive {
|
||||
continue // 网关已命中,跳过
|
||||
}
|
||||
|
||||
for i, host := range subnetHosts {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
goto done
|
||||
default:
|
||||
}
|
||||
|
||||
if _, alive := aliveSubnets.Load(prefix); alive {
|
||||
break
|
||||
}
|
||||
|
||||
port := subnetProbePorts[i%len(subnetProbePorts)]
|
||||
wg.Add(1)
|
||||
limiter <- struct{}{}
|
||||
|
||||
go func(pfx, h string, p int) {
|
||||
defer func() { <-limiter; wg.Done() }()
|
||||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", h, p), subnetProbeTimeout)
|
||||
if err == nil {
|
||||
_ = conn.Close()
|
||||
aliveSubnets.Store(pfx, true)
|
||||
}
|
||||
}(prefix, host, port)
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
wg.Wait()
|
||||
|
||||
// 统计
|
||||
aliveCount := 0
|
||||
aliveSubnets.Range(func(_, _ interface{}) bool { aliveCount++; return true })
|
||||
|
||||
if aliveCount == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(hosts))
|
||||
for _, h := range hosts {
|
||||
if _, alive := aliveSubnets.Load(subnetPrefix(h)); alive {
|
||||
result = append(result, h)
|
||||
}
|
||||
}
|
||||
|
||||
skipped := len(subnets) - aliveCount
|
||||
common.LogInfo(fmt.Sprintf("网段预筛完成: %d 个存活 (网关命中 %d), %d 个跳过, 剩余 %d 主机",
|
||||
aliveCount, gwHits, skipped, len(result)))
|
||||
return result
|
||||
}
|
||||
|
||||
// subnetPrefix 提取 IP 的 /24 前缀(如 "10.1.1")
|
||||
func subnetPrefix(ip string) string {
|
||||
lastDot := strings.LastIndex(ip, ".")
|
||||
if lastDot <= 0 {
|
||||
return ""
|
||||
}
|
||||
return ip[:lastDot]
|
||||
}
|
||||
|
||||
@@ -25,6 +25,23 @@ func (vs *VScan) Init() {
|
||||
vs.parseProbesToMapKName()
|
||||
vs.SetusedProbes()
|
||||
vs.compileFallbacks() // 编译 fallback 数组
|
||||
vs.preDecodeProbeData() // 预解码探针数据
|
||||
}
|
||||
|
||||
// preDecodeProbeData 预解码所有探针的 Data 字段,避免运行时重复解码
|
||||
func (vs *VScan) preDecodeProbeData() {
|
||||
for i := range vs.Probes {
|
||||
if vs.Probes[i].Data != "" {
|
||||
decoded, err := DecodeData(vs.Probes[i].Data)
|
||||
if err == nil {
|
||||
vs.Probes[i].DecodedData = decoded
|
||||
}
|
||||
}
|
||||
}
|
||||
// 同步到 map
|
||||
for i := range vs.Probes {
|
||||
vs.ProbesMapKName[vs.Probes[i].Name] = vs.Probes[i]
|
||||
}
|
||||
}
|
||||
|
||||
// compileFallbacks 编译所有探测器的 fallback 数组
|
||||
|
||||
@@ -20,6 +20,7 @@ const MaxFallbacks = 20
|
||||
type Probe struct {
|
||||
Name string // 探测器名称
|
||||
Data string // 探测数据
|
||||
DecodedData []byte // 预解码的探测数据
|
||||
Protocol string // 协议
|
||||
Ports string // 端口范围
|
||||
SSLPorts string // SSL端口范围
|
||||
|
||||
+74
-12
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
|
||||
// ScanStrategy 定义扫描策略接口
|
||||
type ScanStrategy interface {
|
||||
Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup)
|
||||
Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup)
|
||||
GetPlugins(config *common.Config) ([]string, bool)
|
||||
IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool, config *common.Config) bool
|
||||
}
|
||||
@@ -73,7 +74,13 @@ func selectStrategy(config *common.Config, state *common.State, info common.Host
|
||||
}
|
||||
|
||||
// RunScan 执行整体扫描流程
|
||||
func RunScan(info common.HostInfo, config *common.Config, state *common.State) {
|
||||
func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSession) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
config := session.Config
|
||||
state := session.State
|
||||
|
||||
// 初始化HTTP客户端(静默,无需日志)
|
||||
if err := lib.Inithttp(config); err != nil {
|
||||
common.LogError(i18n.Tr("http_client_init_failed", err))
|
||||
@@ -88,7 +95,7 @@ func RunScan(info common.HostInfo, config *common.Config, state *common.State) {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
// 执行策略
|
||||
strategy.Execute(config, state, info, ch, &wg)
|
||||
strategy.Execute(ctx, session, info, ch, &wg)
|
||||
|
||||
// 等待所有扫描完成
|
||||
wg.Wait()
|
||||
@@ -106,11 +113,16 @@ func RunScan(info common.HostInfo, config *common.Config, state *common.State) {
|
||||
}
|
||||
common.LogInfo(i18n.GetText("press_ctrl_c_exit"))
|
||||
|
||||
// 优雅等待信号
|
||||
// 优雅等待信号或 context 取消(Web Stop)
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
common.LogInfo(i18n.GetText("received_exit_signal"))
|
||||
select {
|
||||
case <-sigChan:
|
||||
common.LogInfo(i18n.GetText("received_exit_signal"))
|
||||
case <-ctx.Done():
|
||||
}
|
||||
cancel()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 完成扫描
|
||||
@@ -134,7 +146,9 @@ func finishScan(config *common.Config, state *common.State) {
|
||||
}
|
||||
|
||||
// ExecuteScanTasks 任务执行通用框架
|
||||
func ExecuteScanTasks(config *common.Config, state *common.State, targets []common.HostInfo, strategy ScanStrategy, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func ExecuteScanTasks(ctx context.Context, session *common.ScanSession, targets []common.HostInfo, strategy ScanStrategy, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
config := session.Config
|
||||
|
||||
// 获取要执行的插件
|
||||
pluginsToRun, isCustomMode := strategy.GetPlugins(config)
|
||||
|
||||
@@ -149,6 +163,13 @@ func ExecuteScanTasks(config *common.Config, state *common.State, targets []comm
|
||||
|
||||
// 流式执行任务,避免预构建大量任务对象
|
||||
for _, target := range targets {
|
||||
// 检查取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
targetPort := target.Port
|
||||
|
||||
for _, pluginName := range pluginsToRun {
|
||||
@@ -159,7 +180,7 @@ func ExecuteScanTasks(config *common.Config, state *common.State, targets []comm
|
||||
|
||||
// 检查插件是否适用于当前目标
|
||||
if strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode, config) {
|
||||
executeScanTask(config, state, pluginName, target, ch, wg)
|
||||
executeScanTask(ctx, session, pluginName, target, ch, wg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,10 +203,44 @@ func countApplicableTasks(targets []common.HostInfo, pluginsToRun []string, isCu
|
||||
return count
|
||||
}
|
||||
|
||||
// longRunningPlugins 长驻插件,不加入 scan WaitGroup,通过 ctx 取消退出
|
||||
var longRunningPlugins = map[string]bool{
|
||||
"forwardshell": true,
|
||||
"socks5proxy": true,
|
||||
"reverseshell": true,
|
||||
}
|
||||
|
||||
// executeScanTask 执行单个扫描任务
|
||||
func executeScanTask(config *common.Config, state *common.State, pluginName string, target common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func executeScanTask(ctx context.Context, session *common.ScanSession, pluginName string, target common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
state := session.State
|
||||
|
||||
// 检查取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 长驻插件不进 WaitGroup,通过 ctx 管理生命周期
|
||||
if longRunningPlugins[pluginName] {
|
||||
go func() {
|
||||
plugin := plugins.Get(pluginName)
|
||||
if plugin != nil {
|
||||
plugin.Scan(ctx, &target, session)
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
ch <- struct{}{} // 获取并发槽位
|
||||
|
||||
// 获取并发槽位,支持取消
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
// 开始监控插件任务
|
||||
@@ -210,7 +265,7 @@ func executeScanTask(config *common.Config, state *common.State, pluginName stri
|
||||
|
||||
plugin := plugins.Get(pluginName)
|
||||
if plugin != nil {
|
||||
result := plugin.Scan(context.Background(), &target, config, state)
|
||||
result := plugin.Scan(ctx, &target, session)
|
||||
if result != nil {
|
||||
if result.Success {
|
||||
// 保存成功的扫描结果到文件
|
||||
@@ -286,9 +341,16 @@ var resultSerializers = map[plugins.ResultType]resultSerializer{
|
||||
plugins.ResultTypeWeb: {
|
||||
outputType: output.TypeService,
|
||||
getStatus: func(_ *plugins.Result, _ *common.HostInfo) string { return "web" },
|
||||
fillDetail: func(_ *plugins.Result, info *common.HostInfo, d map[string]interface{}) {
|
||||
fillDetail: func(r *plugins.Result, info *common.HostInfo, d map[string]interface{}) {
|
||||
d["is_web"] = true
|
||||
d["port"] = info.Port
|
||||
if r.Output == "" {
|
||||
return
|
||||
}
|
||||
d["url"] = r.Output
|
||||
if parsed, err := url.Parse(r.Output); err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https") {
|
||||
d["protocol"] = parsed.Scheme
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+23
-1
@@ -1,11 +1,13 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -34,6 +36,26 @@ scanner_test.go - Scanner核心逻辑测试
|
||||
// 核心逻辑测试:策略选择
|
||||
// =============================================================================
|
||||
|
||||
func TestWebResultSerializerPreservesDetectedProtocol(t *testing.T) {
|
||||
serializer := resultSerializers[plugins.ResultTypeWeb]
|
||||
details := map[string]interface{}{}
|
||||
result := &plugins.Result{
|
||||
Type: plugins.ResultTypeWeb,
|
||||
Success: true,
|
||||
Output: "https://192.168.1.1:8443",
|
||||
}
|
||||
info := &common.HostInfo{Host: "192.168.1.1", Port: 8443}
|
||||
|
||||
serializer.fillDetail(result, info, details)
|
||||
|
||||
if details["protocol"] != "https" {
|
||||
t.Fatalf("protocol = %v, 期望 https", details["protocol"])
|
||||
}
|
||||
if details["url"] != "https://192.168.1.1:8443" {
|
||||
t.Fatalf("url = %v, 期望检测出的URL", details["url"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelectStrategy 测试策略选择逻辑
|
||||
func TestSelectStrategy(t *testing.T) {
|
||||
// 保存原始配置
|
||||
@@ -214,7 +236,7 @@ type mockStrategy struct {
|
||||
applicablePlugins map[string]bool // pluginName -> isApplicable
|
||||
}
|
||||
|
||||
func (m *mockStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func (m *mockStrategy) Execute(_ context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
}
|
||||
|
||||
func (m *mockStrategy) GetPlugins() ([]string, bool) {
|
||||
|
||||
+52
-21
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -15,7 +16,7 @@ import (
|
||||
|
||||
// 默认超时时间常量
|
||||
const (
|
||||
defaultTotalWaitMS = 6000 // Nmap 默认等待时间
|
||||
defaultTotalWaitMS = 3000 // 服务探测默认等待时间
|
||||
defaultIntensity = 7 // 默认探测强度 (1-9)
|
||||
)
|
||||
|
||||
@@ -70,13 +71,16 @@ type Service struct {
|
||||
|
||||
// Info 定义单个端口探测的上下文信息
|
||||
type Info struct {
|
||||
Address string // 目标IP地址
|
||||
Port int // 目标端口
|
||||
Conn net.Conn // 网络连接
|
||||
Result Result // 探测结果
|
||||
Found bool // 是否成功识别服务
|
||||
config *common.Config // 配置引用
|
||||
readTimeoutMS int // 当前读取超时时间(毫秒)
|
||||
Address string // 目标IP地址
|
||||
Port int // 目标端口
|
||||
Conn net.Conn // 网络连接
|
||||
Result Result // 探测结果
|
||||
Found bool // 是否成功识别服务
|
||||
ctx context.Context // 扫描级 context
|
||||
config *common.Config // 配置引用
|
||||
session *common.ScanSession // 会话引用
|
||||
readTimeoutMS int // 当前读取超时时间(毫秒)
|
||||
maxReadTimeoutMS int // RTT 自适应上限(毫秒),0 表示不限制
|
||||
}
|
||||
|
||||
// SmartPortInfoScanner 智能服务识别器:保持nmap准确性,优化网络交互
|
||||
@@ -86,24 +90,28 @@ type SmartPortInfoScanner struct {
|
||||
Conn net.Conn
|
||||
Timeout time.Duration
|
||||
info *Info
|
||||
config *common.Config // 配置引用
|
||||
config *common.Config // 配置引用
|
||||
session *common.ScanSession // 会话引用
|
||||
}
|
||||
|
||||
// 预定义的基础探测器已在PortFinger.go中定义,这里不再重复定义
|
||||
|
||||
// NewSmartPortInfoScanner 创建智能服务识别器
|
||||
func NewSmartPortInfoScanner(addr string, port int, conn net.Conn, timeout time.Duration, config *common.Config) *SmartPortInfoScanner {
|
||||
func NewSmartPortInfoScanner(ctx context.Context, addr string, port int, conn net.Conn, timeout time.Duration, config *common.Config, session *common.ScanSession) *SmartPortInfoScanner {
|
||||
return &SmartPortInfoScanner{
|
||||
Address: addr,
|
||||
Port: port,
|
||||
Conn: conn,
|
||||
Timeout: timeout,
|
||||
config: config,
|
||||
session: session,
|
||||
info: &Info{
|
||||
Address: addr,
|
||||
Port: port,
|
||||
Conn: conn,
|
||||
ctx: ctx,
|
||||
config: config,
|
||||
session: session,
|
||||
Result: Result{
|
||||
Service: Service{},
|
||||
},
|
||||
@@ -211,9 +219,14 @@ func (s *SmartPortInfoScanner) tryProbeList(probes []*Probe, usedProbes map[stri
|
||||
}
|
||||
usedProbes[probe.Name] = struct{}{}
|
||||
|
||||
probeData, err := DecodeData(probe.Data)
|
||||
if err != nil {
|
||||
continue
|
||||
// 优先使用预解码数据
|
||||
probeData := probe.DecodedData
|
||||
if probeData == nil {
|
||||
var err error
|
||||
probeData, err = DecodeData(probe.Data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 TotalWaitMS 设置动态超时
|
||||
@@ -251,7 +264,7 @@ func (s *SmartPortInfoScanner) reconnectIfNeeded() {
|
||||
}
|
||||
|
||||
// 重新建立连接
|
||||
newConn, err := common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%d", s.Address, s.Port), s.Timeout)
|
||||
newConn, err := s.session.DialTCP(s.info.ctx, "tcp", fmt.Sprintf("%s:%d", s.Address, s.Port), s.Timeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -274,8 +287,15 @@ func (s *SmartPortInfoScanner) performSSLSecondStage(serviceInfo *ServiceInfo) *
|
||||
continue
|
||||
}
|
||||
|
||||
probeData, err := DecodeData(probe.Data)
|
||||
if err != nil || len(probeData) == 0 {
|
||||
probeData := probe.DecodedData
|
||||
if probeData == nil {
|
||||
var decErr error
|
||||
probeData, decErr = DecodeData(probe.Data)
|
||||
if decErr != nil || len(probeData) == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(probeData) == 0 {
|
||||
continue
|
||||
}
|
||||
response := s.info.Connect(probeData)
|
||||
@@ -309,8 +329,15 @@ func (s *SmartPortInfoScanner) tryHTTPSProbe() *ServiceInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
probeData, err := DecodeData(probe.Data)
|
||||
if err != nil || len(probeData) == 0 {
|
||||
probeData := probe.DecodedData
|
||||
if probeData == nil {
|
||||
var decErr error
|
||||
probeData, decErr = DecodeData(probe.Data)
|
||||
if decErr != nil || len(probeData) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if len(probeData) == 0 {
|
||||
return nil
|
||||
}
|
||||
response := s.info.Connect(probeData)
|
||||
@@ -481,10 +508,14 @@ func (i *Info) setReadTimeout(ms int) {
|
||||
|
||||
// getReadTimeout 获取当前读取超时时间
|
||||
func (i *Info) getReadTimeout() time.Duration {
|
||||
ms := defaultReadTimeoutMS
|
||||
if i.readTimeoutMS > 0 {
|
||||
return time.Duration(i.readTimeoutMS) * time.Millisecond
|
||||
ms = i.readTimeoutMS
|
||||
}
|
||||
return time.Duration(defaultReadTimeoutMS) * time.Millisecond
|
||||
if i.maxReadTimeoutMS > 0 && ms > i.maxReadTimeoutMS {
|
||||
ms = i.maxReadTimeoutMS
|
||||
}
|
||||
return time.Duration(ms) * time.Millisecond
|
||||
}
|
||||
|
||||
// WrTimeout 默认读写超时时间(秒)
|
||||
@@ -511,7 +542,7 @@ func (i *Info) Write(msg []byte) error {
|
||||
_ = oldConn.Close()
|
||||
|
||||
// 尝试重新连接 - 支持SOCKS5代理
|
||||
newConn, retryErr := common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%d", i.Address, i.Port), time.Duration(6)*time.Second)
|
||||
newConn, retryErr := i.session.DialTCP(i.ctx, "tcp", fmt.Sprintf("%s:%d", i.Address, i.Port), time.Duration(6)*time.Second)
|
||||
if retryErr != nil {
|
||||
return retryErr
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ service_probe_strategy_test.go - SmartProbeStrategy 策略逻辑测试
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -155,7 +156,7 @@ func TestSmartPortInfoScanner_Creation(t *testing.T) {
|
||||
}
|
||||
|
||||
// 使用 nil 连接(实际测试中会使用真实连接)
|
||||
scanner := NewSmartPortInfoScanner("127.0.0.1", 80, nil, 3*time.Second, config)
|
||||
scanner := NewSmartPortInfoScanner(context.Background(), "127.0.0.1", 80, nil, 3*time.Second, config, nil)
|
||||
|
||||
if scanner == nil {
|
||||
t.Fatal("Scanner 创建失败")
|
||||
@@ -175,8 +176,8 @@ func TestSmartPortInfoScanner_Creation(t *testing.T) {
|
||||
// TestDefaultConstants 验证默认常量值
|
||||
func TestDefaultConstants(t *testing.T) {
|
||||
// 验证默认等待时间
|
||||
if defaultTotalWaitMS != 6000 {
|
||||
t.Errorf("defaultTotalWaitMS 应该是 6000,实际是 %d", defaultTotalWaitMS)
|
||||
if defaultTotalWaitMS != 3000 {
|
||||
t.Errorf("defaultTotalWaitMS 应该是 3000,实际是 %d", defaultTotalWaitMS)
|
||||
}
|
||||
|
||||
// 验证默认 intensity
|
||||
|
||||
+112
-31
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -27,7 +28,7 @@ func NewServiceScanStrategy() *ServiceScanStrategy {
|
||||
func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config) {
|
||||
// 需要从命令行参数获取端口信息来进行过滤
|
||||
// 如果没有指定端口,使用默认端口进行过滤显示
|
||||
ports := common.GetFlagVars().Ports
|
||||
ports := config.Target.Ports
|
||||
if ports == "" || ports == "all" {
|
||||
// 默认端口扫描:显示所有插件
|
||||
s.BaseScanStrategy.LogPluginInfo(config)
|
||||
@@ -42,7 +43,7 @@ func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config
|
||||
allPlugins, isCustomMode := s.GetPlugins(config)
|
||||
|
||||
// 解析端口
|
||||
ports := s.parsePortList(common.GetFlagVars().Ports)
|
||||
ports := s.parsePortList(config.Target.Ports)
|
||||
if len(ports) == 0 {
|
||||
s.BaseScanStrategy.LogPluginInfo(config)
|
||||
return
|
||||
@@ -112,10 +113,11 @@ func (s *ServiceScanStrategy) Description() string {
|
||||
}
|
||||
|
||||
// Execute 执行服务扫描策略
|
||||
func (s *ServiceScanStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func (s *ServiceScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
config := session.Config
|
||||
|
||||
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
|
||||
fv := common.GetFlagVars()
|
||||
if info.Host == "" && fv.HostsFile == "" {
|
||||
if info.Host == "" && session.Params.HostsFile == "" {
|
||||
common.LogError(i18n.GetText("parse_error_target_empty"))
|
||||
return
|
||||
}
|
||||
@@ -133,28 +135,88 @@ func (s *ServiceScanStrategy) Execute(config *common.Config, state *common.State
|
||||
s.LogPluginInfo(config)
|
||||
|
||||
// 执行主机扫描流程
|
||||
s.performHostScan(config, state, info, ch, wg)
|
||||
s.performHostScan(ctx, session, info, ch, wg)
|
||||
}
|
||||
|
||||
// performHostScan 执行主机扫描的完整流程
|
||||
func (s *ServiceScanStrategy) performHostScan(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
// 发现目标主机和端口
|
||||
targetInfos, err := s.discoverTargets(info.Host, info, config, state)
|
||||
// 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
|
||||
|
||||
// 解析目标主机
|
||||
hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
|
||||
if err != nil {
|
||||
common.LogError(err.Error())
|
||||
common.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err))
|
||||
return
|
||||
}
|
||||
|
||||
// 执行漏洞扫描
|
||||
if len(targetInfos) > 0 {
|
||||
ExecuteScanTasks(config, state, targetInfos, s, ch, wg)
|
||||
// 主机存活检测
|
||||
if s.shouldPerformLivenessCheck(hosts, config) {
|
||||
hosts = CheckLive(ctx, hosts, false, session)
|
||||
common.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts)))
|
||||
}
|
||||
|
||||
if len(hosts) == 0 && len(state.GetHostPorts()) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 流式 channel:端口扫描发现开放端口后立即通知插件执行
|
||||
stream := make(chan string, 64)
|
||||
|
||||
// 启动端口扫描 goroutine
|
||||
go func() {
|
||||
if len(hosts) > 0 {
|
||||
EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, stream)
|
||||
} else {
|
||||
close(stream)
|
||||
}
|
||||
}()
|
||||
|
||||
// pipeline 消费:边收开放端口边执行插件
|
||||
pluginsToRun, isCustomMode := s.GetPlugins(config)
|
||||
cancelled := false
|
||||
for addr := range stream {
|
||||
if cancelled {
|
||||
continue // ctx 已取消,排空 stream 防止写端阻塞
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 合并预设的 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()
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareTargets 准备目标信息
|
||||
func (s *ServiceScanStrategy) PrepareTargets(info common.HostInfo, config *common.Config, state *common.State) []common.HostInfo {
|
||||
func (s *ServiceScanStrategy) PrepareTargets(info common.HostInfo, session *common.ScanSession) []common.HostInfo {
|
||||
// 发现目标主机和端口
|
||||
targetInfos, err := s.discoverTargets(info.Host, info, config, state)
|
||||
targetInfos, err := s.discoverTargets(context.Background(), info.Host, info, session)
|
||||
if err != nil {
|
||||
common.LogError(err.Error())
|
||||
return nil
|
||||
@@ -213,10 +275,11 @@ func (s *ServiceScanStrategy) LogVulnerabilityPluginInfo(targets []common.HostIn
|
||||
// =============================================================================
|
||||
|
||||
// discoverTargets 发现目标主机和端口
|
||||
func (s *ServiceScanStrategy) discoverTargets(hostInput string, baseInfo common.HostInfo, config *common.Config, state *common.State) ([]common.HostInfo, error) {
|
||||
func (s *ServiceScanStrategy) discoverTargets(ctx context.Context, hostInput string, baseInfo common.HostInfo, session *common.ScanSession) ([]common.HostInfo, error) {
|
||||
config := session.Config
|
||||
state := session.State
|
||||
// 标准流程:解析目标主机
|
||||
fv := common.GetFlagVars()
|
||||
hosts, err := parsers.ParseIP(hostInput, fv.HostsFile, fv.ExcludeHosts)
|
||||
hosts, err := parsers.ParseIP(hostInput, session.Params.HostsFile, session.Params.ExcludeHosts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", i18n.GetText("parse_target_failed"), err)
|
||||
}
|
||||
@@ -227,12 +290,12 @@ func (s *ServiceScanStrategy) discoverTargets(hostInput string, baseInfo common.
|
||||
if len(hosts) > 0 || len(state.GetHostPorts()) > 0 {
|
||||
// 主机存活检测
|
||||
if s.shouldPerformLivenessCheck(hosts, config) {
|
||||
hosts = CheckLive(hosts, false, config, state)
|
||||
hosts = CheckLive(ctx, hosts, false, session)
|
||||
common.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts)))
|
||||
}
|
||||
|
||||
// 端口扫描
|
||||
alivePorts := s.discoverAlivePorts(hosts, config, state)
|
||||
alivePorts := s.discoverAlivePorts(ctx, hosts, session)
|
||||
if len(alivePorts) > 0 {
|
||||
targetInfos = s.convertToTargetInfos(alivePorts, baseInfo)
|
||||
}
|
||||
@@ -247,26 +310,44 @@ func (s *ServiceScanStrategy) shouldPerformLivenessCheck(hosts []string, config
|
||||
}
|
||||
|
||||
// discoverAlivePorts 发现存活的端口
|
||||
func (s *ServiceScanStrategy) discoverAlivePorts(hosts []string, config *common.Config, state *common.State) []string {
|
||||
// 执行正常端口扫描后,合并预设的 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
|
||||
|
||||
// 如果已经有明确指定的host:port,直接使用(让后续SmartIdentify统一验证和识别)
|
||||
hostPorts := state.GetHostPorts()
|
||||
if len(hostPorts) > 0 {
|
||||
alivePorts = hostPorts
|
||||
common.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts)))
|
||||
state.ClearHostPorts()
|
||||
return alivePorts
|
||||
// 正常端口扫描
|
||||
if len(hosts) > 0 {
|
||||
alivePorts = EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, nil)
|
||||
}
|
||||
|
||||
// 根据扫描模式选择端口扫描方式
|
||||
if len(hosts) > 0 {
|
||||
alivePorts = EnhancedPortScan(hosts, config.Target.Ports, int64(config.Timeout.Seconds()), config, state)
|
||||
// 合并预设的 host:port(项目缓存 / CLI 注入)
|
||||
hostPorts := state.GetHostPorts()
|
||||
if len(hostPorts) > 0 {
|
||||
alivePorts = mergeHostPorts(alivePorts, hostPorts)
|
||||
common.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
|
||||
|
||||
+32
-44
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -18,6 +19,25 @@ import (
|
||||
// Web服务检测
|
||||
// ===============================
|
||||
|
||||
// 全局共享 HTTP Client,复用连接池减少 TLS 握手和 TCP 建连开销
|
||||
var (
|
||||
sharedHTTPClientOnce sync.Once
|
||||
sharedHTTPClient *http.Client
|
||||
)
|
||||
|
||||
func getSharedHTTPClient(config *common.Config) *http.Client {
|
||||
sharedHTTPClientOnce.Do(func() {
|
||||
sharedHTTPClient = createHTTPClient(config)
|
||||
// 启用 keep-alive 复用连接
|
||||
if t, ok := sharedHTTPClient.Transport.(*http.Transport); ok {
|
||||
t.DisableKeepAlives = false
|
||||
t.MaxIdleConns = 100
|
||||
t.MaxIdleConnsPerHost = 2
|
||||
}
|
||||
})
|
||||
return sharedHTTPClient
|
||||
}
|
||||
|
||||
// WebPortDetector 简化的Web检测器 - 保持API兼容
|
||||
type WebPortDetector struct{}
|
||||
|
||||
@@ -29,9 +49,9 @@ func GetWebPortDetector() *WebPortDetector {
|
||||
// DetectHTTPScheme 智能检测HTTP/HTTPS协议
|
||||
// 策略:TLS握手优先(快速且准确),失败后尝试HTTP
|
||||
// 返回: "https", "http", 或 "" (都不是Web服务)
|
||||
func DetectHTTPScheme(host string, port int, config *common.Config) string {
|
||||
func DetectHTTPScheme(host string, port int, config *common.Config, session *common.ScanSession) string {
|
||||
// 优化:先快速检测 TCP 连通性
|
||||
if !isPortReachable(host, port, config) {
|
||||
if !isPortReachable(host, port, config, session) {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -58,15 +78,7 @@ func DetectHTTPScheme(host string, port int, config *common.Config) string {
|
||||
// TLS握手失败,记录原因
|
||||
|
||||
// 第二步:尝试HTTP请求(回退检测HTTP)
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse // 不跟随重定向
|
||||
},
|
||||
}
|
||||
client := getSharedHTTPClient(config)
|
||||
|
||||
// 使用HEAD请求(更轻量)
|
||||
httpURL := fmt.Sprintf("http://%s", addr)
|
||||
@@ -124,14 +136,14 @@ func createHTTPClient(config *common.Config) *http.Client {
|
||||
}
|
||||
|
||||
// DetectHTTPServiceOnly HTTP协议检测 - 保持API兼容,简化实现
|
||||
func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *common.Config) bool {
|
||||
func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *common.Config, session *common.ScanSession) bool {
|
||||
// 优化:先快速检测 TCP 连通性,避免在不可达端口上浪费双倍超时时间
|
||||
// 对于不存在的端口,这可以将检测时间从 2×timeout 减少到 1×timeout
|
||||
if !isPortReachable(host, port, config) {
|
||||
if !isPortReachable(host, port, config, session) {
|
||||
return false
|
||||
}
|
||||
|
||||
client := createHTTPClient(config)
|
||||
client := getSharedHTTPClient(config)
|
||||
|
||||
// 尝试HTTP
|
||||
if w.tryHTTP(client, host, port, "http") {
|
||||
@@ -148,11 +160,11 @@ func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *c
|
||||
|
||||
// isPortReachable 快速检测端口是否可达(TCP 连接测试)
|
||||
// 用于在 HTTP/HTTPS 检测前过滤不可达端口,避免双重超时
|
||||
func isPortReachable(host string, port int, config *common.Config) bool {
|
||||
func isPortReachable(host string, port int, config *common.Config, session *common.ScanSession) bool {
|
||||
timeout := config.Network.WebTimeout
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
|
||||
conn, err := net.DialTimeout("tcp", addr, timeout)
|
||||
conn, err := session.DialTCP(context.Background(), "tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -276,30 +288,6 @@ func IsMarkedWebService(host string, port int) bool {
|
||||
return exists
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 指纹缓存
|
||||
// ===============================
|
||||
|
||||
// 指纹缓存 - 存储 host:port → 指纹列表的映射
|
||||
var (
|
||||
fingerprintCache = make(map[string][]string)
|
||||
fingerprintCacheMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// SetFingerprints 存储目标的指纹信息
|
||||
func SetFingerprints(host string, port int, fingerprints []string) {
|
||||
if len(fingerprints) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
fingerprintCacheMutex.Lock()
|
||||
defer fingerprintCacheMutex.Unlock()
|
||||
|
||||
fingerprintCache[cacheKey] = fingerprints
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// Web扫描策略
|
||||
// ===============================
|
||||
@@ -327,7 +315,7 @@ func (s *WebScanStrategy) Description() string {
|
||||
}
|
||||
|
||||
// Execute 执行Web扫描策略
|
||||
func (s *WebScanStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func (s *WebScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
// 输出扫描开始信息
|
||||
s.LogScanStart()
|
||||
|
||||
@@ -338,13 +326,13 @@ func (s *WebScanStrategy) Execute(config *common.Config, state *common.State, in
|
||||
}
|
||||
|
||||
// 准备URL目标
|
||||
targets := s.PrepareTargets(info, state)
|
||||
targets := s.PrepareTargets(info, session.State)
|
||||
|
||||
// 输出插件信息
|
||||
s.LogPluginInfo(config)
|
||||
s.LogPluginInfo(session.Config)
|
||||
|
||||
// 执行扫描任务
|
||||
ExecuteScanTasks(config, state, targets, s, ch, wg)
|
||||
ExecuteScanTasks(ctx, session, targets, s, ch, wg)
|
||||
}
|
||||
|
||||
// PrepareTargets 准备URL目标列表
|
||||
|
||||
@@ -662,6 +662,8 @@ func TestDetectHTTPScheme(t *testing.T) {
|
||||
cfg.Network.WebTimeout = 2 * time.Second
|
||||
defer func() { cfg.Network.WebTimeout = oldTimeout }()
|
||||
|
||||
session := common.NewScanSession(cfg, common.NewState(), common.GetFlagVars())
|
||||
|
||||
t.Run("HTTPS服务器检测", func(t *testing.T) {
|
||||
// 创建HTTPS测试服务器
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -677,7 +679,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
|
||||
// 测试检测
|
||||
result := DetectHTTPScheme(host, port, cfg)
|
||||
result := DetectHTTPScheme(host, port, cfg, session)
|
||||
if result != "https" {
|
||||
t.Errorf("DetectHTTPScheme() = %q, 期望 'https'", result)
|
||||
}
|
||||
@@ -698,7 +700,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
|
||||
// 测试检测
|
||||
result := DetectHTTPScheme(host, port, cfg)
|
||||
result := DetectHTTPScheme(host, port, cfg, session)
|
||||
if result != "http" {
|
||||
t.Errorf("DetectHTTPScheme() = %q, 期望 'http'", result)
|
||||
}
|
||||
@@ -706,7 +708,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
||||
|
||||
t.Run("不存在的服务", func(t *testing.T) {
|
||||
// 使用127.0.0.1的一个未使用端口
|
||||
result := DetectHTTPScheme("127.0.0.1", 65534, cfg)
|
||||
result := DetectHTTPScheme("127.0.0.1", 65534, cfg, session)
|
||||
if result != "" {
|
||||
t.Errorf("不存在的服务应返回空字符串, 实际 %q", result)
|
||||
}
|
||||
@@ -736,7 +738,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
|
||||
// 测试检测
|
||||
result := DetectHTTPScheme("127.0.0.1", port, cfg)
|
||||
result := DetectHTTPScheme("127.0.0.1", port, cfg, session)
|
||||
if result != "" {
|
||||
t.Logf("非Web服务检测返回: %q (预期空字符串,但立即关闭连接可能被误判)", result)
|
||||
}
|
||||
@@ -757,7 +759,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
||||
host, portStr, _ := net.SplitHostPort(server.Listener.Addr().String())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
|
||||
result := DetectHTTPScheme(host, port, cfg)
|
||||
result := DetectHTTPScheme(host, port, cfg, session)
|
||||
if result != "https" {
|
||||
t.Errorf("TLS 1.0服务器应被检测为https, 实际 %q", result)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user