feat: 统一服务缓存 + 指纹驱动插件匹配

将 webServiceCache 扩展为通用 serviceCache,所有指纹识别结果
统一缓存,插件匹配时端口不命中则回退到服务名称匹配。

删除多余的 service_cache.go,复用已有的 ServiceInfo 体系。
补充 nil 防御、Explicit 标记、大量单元/集成/回归测试。
This commit is contained in:
ZacharyZcR
2026-06-14 22:23:47 +08:00
parent 2ab7c4d9b2
commit 5ad914a1bb
44 changed files with 1533 additions and 142 deletions
+3 -3
View File
@@ -137,11 +137,11 @@ func (b *BaseScanStrategy) isPluginApplicableToPortWithHost(pluginName string, t
}
}
// 端口不匹配时,按服务识别结果匹配
// 端口不匹配时,按指纹识别结果匹配
// 例:8881 端口上识别到 ssh 服务 → ssh 插件应该执行
if targetHost != "" && targetPort > 0 {
if svcName, ok := GetServiceName(targetHost, targetPort); ok {
if strings.EqualFold(svcName, pluginName) {
if info, ok := GetCachedServiceInfo(targetHost, targetPort); ok && info != nil {
if strings.EqualFold(info.Name, pluginName) {
return true
}
}
+6 -2
View File
@@ -2,10 +2,10 @@ package core
import (
"context"
"fmt"
"math"
"net"
"sort"
"strconv"
"sync"
"time"
@@ -97,6 +97,10 @@ func (p *NetworkProfile) RecommendConcurrency(userThreadNum int, explicit bool)
// probePorts 探测用的端口列表(高响应率的常见端口)
var probePorts = []int{80, 443, 22}
func networkProbeAddress(host string, port int) string {
return net.JoinHostPort(host, strconv.Itoa(port))
}
// ProbeNetwork 探测目标网络环境
// 从 hosts 中抽样,用低并发 TCP 连接测量 RTT 和丢包率
// 整个过程控制在数秒内完成
@@ -140,7 +144,7 @@ func ProbeNetwork(ctx context.Context, hosts []string, session *common.ScanSessi
go func(h string, p int) {
defer func() { <-sem; wg.Done() }()
addr := fmt.Sprintf("%s:%d", h, p)
addr := networkProbeAddress(h, p)
start := time.Now()
conn, err := session.DialTCP(ctx, "tcp", addr, probeTimeout)
rtt := time.Since(start)
+28 -10
View File
@@ -60,7 +60,7 @@ func TestClassifyNetwork(t *testing.T) {
t.Run("公网 RTT 分布(低丢包)", func(t *testing.T) {
rtts := makeDurations([]int{60, 70, 80, 90, 100, 110, 120, 150, 200, 300}) // ms
p := classifyNetwork(rtts, 0, 10) // 无丢包
p := classifyNetwork(rtts, 0, 10) // 无丢包
if p.Env != EnvInternet {
t.Errorf("env = %v, want Internet", p.Env)
@@ -72,7 +72,7 @@ func TestClassifyNetwork(t *testing.T) {
t.Run("高丢包归类为慢速", func(t *testing.T) {
rtts := makeDurations([]int{60, 70, 80, 90, 100}) // ms, 5 responded
p := classifyNetwork(rtts, 5, 10) // 50% loss
p := classifyNetwork(rtts, 5, 10) // 50% loss
if p.Env != EnvSlow {
t.Errorf("env = %v, want Slow (高丢包)", p.Env)
@@ -96,14 +96,14 @@ func TestClassifyNetwork(t *testing.T) {
func TestRecommendConcurrency(t *testing.T) {
tests := []struct {
env NetworkEnv
lossRate float64
userT int
explicit bool
wantTMin int
wantTMax int
wantCeil int
desc string
env NetworkEnv
lossRate float64
userT int
explicit bool
wantTMin int
wantTMax int
wantCeil int
desc string
}{
{EnvLAN, 0.0, 600, false, 800, 1000, -1, "内网自动: ×1.5"},
{EnvWAN, 0.0, 600, false, 550, 650, -1, "局域网自动: ×1.0"},
@@ -156,6 +156,24 @@ func TestPickSamples(t *testing.T) {
}
}
func TestNetworkProbeAddressUsesJoinHostPort(t *testing.T) {
tests := []struct {
host string
port int
want string
}{
{"127.0.0.1", 80, "127.0.0.1:80"},
{"::1", 443, "[::1]:443"},
{"2001:db8::1", 22, "[2001:db8::1]:22"},
}
for _, tt := range tests {
if got := networkProbeAddress(tt.host, tt.port); got != tt.want {
t.Fatalf("networkProbeAddress(%q, %d) = %q, want %q", tt.host, tt.port, got, tt.want)
}
}
}
// =============================================================================
// 辅助
// =============================================================================
+2 -3
View File
@@ -707,8 +707,8 @@ func processServiceResult(ctx context.Context, host string, port int, addr strin
return
}
// 缓存服务名称,供插件按服务类型匹配(解决非标准端口问题)
MarkServiceName(host, port, serviceInfo.Name)
// 缓存指纹识别结果,供插件按服务类型匹配(解决非标准端口问题)
CacheServiceInfo(host, port, serviceInfo)
// 保存并输出服务信息
details := buildServiceDetails(port, serviceInfo)
@@ -716,7 +716,6 @@ func processServiceResult(ctx context.Context, host string, port int, addr strin
if isWeb {
details["is_web"] = true
MarkAsWebService(host, port, serviceInfo)
}
_ = session.SaveResult(&output.ScanResult{
+42
View File
@@ -212,6 +212,48 @@ func TestFormatAddress(t *testing.T) {
}
}
func TestBuildWebServiceURLIPv6(t *testing.T) {
tests := []struct {
name string
addr string
serviceInfo *ServiceInfo
want string
}{
{
name: "http default port",
addr: "[2001:db8::1]:80",
serviceInfo: &ServiceInfo{
Name: "http",
},
want: "http://[2001:db8::1]",
},
{
name: "https default port",
addr: "[2001:db8::1]:443",
serviceInfo: &ServiceInfo{
Name: "https",
},
want: "https://[2001:db8::1]",
},
{
name: "http non-default port",
addr: "[2001:db8::1]:8080",
serviceInfo: &ServiceInfo{
Name: "http",
},
want: "http://[2001:db8::1]:8080",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := buildWebServiceURL(tt.addr, tt.serviceInfo); got != tt.want {
t.Fatalf("buildWebServiceURL(%q) = %q, want %q", tt.addr, got, tt.want)
}
})
}
}
// =============================================================================
// 排除端口逻辑测试(从EnhancedPortScan:28-32行提取)
// =============================================================================
-36
View File
@@ -1,36 +0,0 @@
package core
import (
"net"
"strconv"
"strings"
"sync"
)
// 服务识别缓存:host:port → 服务名称
// 端口扫描阶段写入,插件匹配阶段读取
// 解决非标准端口上的服务无法匹配对应插件的问题
var (
serviceNameCache = make(map[string]string)
serviceCacheMu sync.RWMutex
)
// MarkServiceName 记录端口上识别到的服务名称
func MarkServiceName(host string, port int, serviceName string) {
if serviceName == "" || serviceName == "unknown" {
return
}
key := net.JoinHostPort(host, strconv.Itoa(port))
serviceCacheMu.Lock()
serviceNameCache[key] = strings.ToLower(serviceName)
serviceCacheMu.Unlock()
}
// GetServiceName 查询端口上的服务名称
func GetServiceName(host string, port int) (string, bool) {
key := net.JoinHostPort(host, strconv.Itoa(port))
serviceCacheMu.RLock()
name, ok := serviceNameCache[key]
serviceCacheMu.RUnlock()
return name, ok
}
+354
View File
@@ -0,0 +1,354 @@
package core
import (
"sync"
"testing"
"github.com/shadow1ng/fscan/plugins"
)
// registerTestPlugins 注册测试用插件(名字和服务识别结果一致)
func registerTestPlugins(t *testing.T) {
t.Helper()
plugins.RegisterWithOptions("ssh", func() plugins.Plugin { return nil }, []int{22, 2222}, nil, true)
plugins.RegisterWithOptions("mysql", func() plugins.Plugin { return nil }, []int{3306}, nil, true)
plugins.RegisterWithOptions("ftp", func() plugins.Plugin { return nil }, []int{21}, nil, true)
plugins.RegisterWithOptions("redis", func() plugins.Plugin { return nil }, []int{6379}, nil, true)
plugins.RegisterWithOptions("postgresql", func() plugins.Plugin { return nil }, []int{5432}, nil, true)
plugins.RegisterWithOptions("telnet", func() plugins.Plugin { return nil }, []int{23}, nil, true)
plugins.RegisterWithOptions("mssql", func() plugins.Plugin { return nil }, []int{1433}, nil, true)
plugins.RegisterWithOptions("vnc", func() plugins.Plugin { return nil }, []int{5900}, nil, true)
plugins.RegisterWithOptions("webtitle", func() plugins.Plugin { return nil }, []int{}, []string{plugins.PluginTypeWeb}, true)
}
func clearServiceCache() {
serviceCacheMutex.Lock()
serviceCache = make(map[string]*ServiceInfo)
serviceCacheMutex.Unlock()
}
// =============================================================================
// 单元测试:CacheServiceInfo / GetCachedServiceInfo
// =============================================================================
func TestCacheServiceInfo_BasicCRUD(t *testing.T) {
clearServiceCache()
t.Run("缓存后可读取", func(t *testing.T) {
CacheServiceInfo("10.0.0.1", 22, &ServiceInfo{Name: "ssh", Version: "OpenSSH_8.9"})
info, ok := GetCachedServiceInfo("10.0.0.1", 22)
if !ok {
t.Fatal("缓存未命中")
}
if info.Name != "ssh" || info.Version != "OpenSSH_8.9" {
t.Errorf("got Name=%q Version=%q", info.Name, info.Version)
}
})
t.Run("不同端口独立", func(t *testing.T) {
CacheServiceInfo("10.0.0.1", 3306, &ServiceInfo{Name: "mysql"})
CacheServiceInfo("10.0.0.1", 5432, &ServiceInfo{Name: "postgresql"})
i1, _ := GetCachedServiceInfo("10.0.0.1", 3306)
i2, _ := GetCachedServiceInfo("10.0.0.1", 5432)
if i1.Name != "mysql" || i2.Name != "postgresql" {
t.Errorf("端口混淆: 3306=%q 5432=%q", i1.Name, i2.Name)
}
})
t.Run("不同主机独立", func(t *testing.T) {
CacheServiceInfo("10.0.0.1", 22, &ServiceInfo{Name: "ssh"})
CacheServiceInfo("10.0.0.2", 22, &ServiceInfo{Name: "telnet"})
i1, _ := GetCachedServiceInfo("10.0.0.1", 22)
i2, _ := GetCachedServiceInfo("10.0.0.2", 22)
if i1.Name != "ssh" || i2.Name != "telnet" {
t.Errorf("主机混淆: .1=%q .2=%q", i1.Name, i2.Name)
}
})
t.Run("覆盖写入", func(t *testing.T) {
CacheServiceInfo("10.0.0.5", 80, &ServiceInfo{Name: "unknown"})
CacheServiceInfo("10.0.0.5", 80, &ServiceInfo{Name: "http"})
info, _ := GetCachedServiceInfo("10.0.0.5", 80)
if info.Name != "http" {
t.Errorf("覆盖失败: %q", info.Name)
}
})
t.Run("未缓存返回 false", func(t *testing.T) {
if _, ok := GetCachedServiceInfo("192.168.99.99", 12345); ok {
t.Error("应返回 false")
}
})
}
// =============================================================================
// 单元测试:Web 服务过滤
// =============================================================================
func TestWebServiceFiltering(t *testing.T) {
clearServiceCache()
webNames := []string{"http", "https", "ssl", "tls", "nginx", "apache", "iis", "tomcat"}
nonWebNames := []string{"ssh", "mysql", "postgresql", "redis", "mongodb", "ftp", "smtp", "telnet", "vnc", "rdp"}
for _, name := range webNames {
clearServiceCache()
CacheServiceInfo("10.0.0.1", 443, &ServiceInfo{Name: name})
if !IsMarkedWebService("10.0.0.1", 443) {
t.Errorf("%q 应被识别为 Web 服务", name)
}
}
for _, name := range nonWebNames {
clearServiceCache()
CacheServiceInfo("10.0.0.1", 9999, &ServiceInfo{Name: name})
if IsMarkedWebService("10.0.0.1", 9999) {
t.Errorf("%q 不应被识别为 Web 服务", name)
}
}
t.Run("GetWebServiceInfo 过滤非 Web", func(t *testing.T) {
clearServiceCache()
CacheServiceInfo("10.0.0.1", 3306, &ServiceInfo{Name: "mysql"})
if _, ok := GetWebServiceInfo("10.0.0.1", 3306); ok {
t.Error("mysql 不应通过 GetWebServiceInfo")
}
})
t.Run("GetWebServiceInfo 返回 Web", func(t *testing.T) {
clearServiceCache()
CacheServiceInfo("10.0.0.1", 8080, &ServiceInfo{Name: "nginx"})
info, ok := GetWebServiceInfo("10.0.0.1", 8080)
if !ok || info.Name != "nginx" {
t.Error("nginx 应通过 GetWebServiceInfo")
}
})
}
// =============================================================================
// 集成测试:指纹驱动插件匹配
// =============================================================================
func TestIntegration_FingerprintDrivenPluginMatch(t *testing.T) {
clearServiceCache()
registerTestPlugins(t)
CacheServiceInfo("10.0.0.1", 22, &ServiceInfo{Name: "ssh"})
CacheServiceInfo("10.0.0.1", 8881, &ServiceInfo{Name: "ssh"})
CacheServiceInfo("10.0.0.1", 13306, &ServiceInfo{Name: "mysql"})
CacheServiceInfo("10.0.0.1", 80, &ServiceInfo{Name: "http"})
CacheServiceInfo("10.0.0.1", 9443, &ServiceInfo{Name: "https"})
CacheServiceInfo("10.0.0.1", 2121, &ServiceInfo{Name: "ftp"})
CacheServiceInfo("10.0.0.1", 6380, &ServiceInfo{Name: "redis"})
strategy := NewServiceScanStrategy()
tests := []struct {
plugin, host string
port int
want bool
desc string
}{
{"ssh", "10.0.0.1", 22, true, "SSH 标准端口"},
{"ssh", "10.0.0.1", 8881, true, "SSH 非标准端口(指纹匹配)"},
{"mysql", "10.0.0.1", 13306, true, "MySQL 非标准端口"},
{"ftp", "10.0.0.1", 2121, true, "FTP 非标准端口"},
{"redis", "10.0.0.1", 6380, true, "Redis 非标准端口"},
{"ssh", "10.0.0.1", 13306, false, "SSH 不匹配 MySQL 端口"},
{"mysql", "10.0.0.1", 8881, false, "MySQL 不匹配 SSH 端口"},
{"redis", "10.0.0.1", 22, false, "Redis 不匹配 SSH 标准端口"},
{"ssh", "10.0.0.1", 65000, false, "SSH 不匹配未识别端口"},
{"webtitle", "10.0.0.1", 80, true, "Web 匹配 http"},
{"webtitle", "10.0.0.1", 9443, true, "Web 匹配 https 非标准"},
{"webtitle", "10.0.0.1", 22, false, "Web 不匹配 SSH"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got := strategy.isPluginApplicableToPortWithHost(tt.plugin, tt.host, tt.port)
if got != tt.want {
t.Errorf("plugin=%q port=%d: got %v, want %v", tt.plugin, tt.port, got, tt.want)
}
})
}
}
// =============================================================================
// 集成测试:非标准端口完整流程
// =============================================================================
func TestIntegration_NonStandardPortScanFlow(t *testing.T) {
clearServiceCache()
registerTestPlugins(t)
host := "172.16.0.100"
CacheServiceInfo(host, 8881, &ServiceInfo{
Name: "ssh", Version: "OpenSSH_8.2p1",
Banner: "SSH-2.0-OpenSSH_8.2p1", Extras: map[string]string{"os": "Linux"},
})
strategy := NewServiceScanStrategy()
if !strategy.isPluginApplicableToPortWithHost("ssh", host, 8881) {
t.Error("SSH 应匹配 8881")
}
if strategy.isPluginApplicableToPortWithHost("mysql", host, 8881) {
t.Error("MySQL 不应匹配 8881 上的 SSH")
}
if IsMarkedWebService(host, 8881) {
t.Error("SSH 不应标记为 Web")
}
}
// =============================================================================
// 集成测试:同一主机多服务
// =============================================================================
func TestIntegration_MultiServiceSameHost(t *testing.T) {
clearServiceCache()
registerTestPlugins(t)
host := "192.168.1.100"
CacheServiceInfo(host, 2222, &ServiceInfo{Name: "ssh"})
CacheServiceInfo(host, 33060, &ServiceInfo{Name: "mysql"})
CacheServiceInfo(host, 8080, &ServiceInfo{Name: "http"})
CacheServiceInfo(host, 63790, &ServiceInfo{Name: "redis"})
strategy := NewServiceScanStrategy()
checks := []struct {
plugin string
port int
want bool
}{
{"ssh", 2222, true}, {"ssh", 33060, false}, {"ssh", 8080, false},
{"mysql", 33060, true}, {"mysql", 2222, false},
{"redis", 63790, true}, {"redis", 2222, false},
{"webtitle", 8080, true}, {"webtitle", 2222, false},
}
for _, c := range checks {
got := strategy.isPluginApplicableToPortWithHost(c.plugin, host, c.port)
if got != c.want {
t.Errorf("plugin=%q port=%d: got %v, want %v", c.plugin, c.port, got, c.want)
}
}
}
// =============================================================================
// 边界测试
// =============================================================================
func TestServiceCache_EdgeCases(t *testing.T) {
clearServiceCache()
registerTestPlugins(t)
strategy := NewServiceScanStrategy()
t.Run("空服务名不匹配", func(t *testing.T) {
CacheServiceInfo("10.0.0.1", 9999, &ServiceInfo{Name: ""})
if strategy.isPluginApplicableToPortWithHost("ssh", "10.0.0.1", 9999) {
t.Error("空服务名不应匹配")
}
})
t.Run("unknown 不匹配", func(t *testing.T) {
CacheServiceInfo("10.0.0.1", 8888, &ServiceInfo{Name: "unknown"})
if strategy.isPluginApplicableToPortWithHost("ssh", "10.0.0.1", 8888) {
t.Error("unknown 不应匹配")
}
})
t.Run("大小写不敏感", func(t *testing.T) {
clearServiceCache()
CacheServiceInfo("10.0.0.1", 5555, &ServiceInfo{Name: "SSH"})
if !strategy.isPluginApplicableToPortWithHost("ssh", "10.0.0.1", 5555) {
t.Error("SSH 大写应匹配 ssh 插件")
}
})
t.Run("host 为空不查缓存", func(t *testing.T) {
CacheServiceInfo("10.0.0.1", 8881, &ServiceInfo{Name: "ssh"})
if strategy.isPluginApplicableToPortWithHost("ssh", "", 8881) {
t.Error("host 为空不应匹配")
}
})
t.Run("nil ServiceInfo 不 panic", func(t *testing.T) {
CacheServiceInfo("10.0.0.1", 7777, nil)
got := strategy.isPluginApplicableToPortWithHost("ssh", "10.0.0.1", 7777)
if got {
t.Error("nil ServiceInfo 不应匹配")
}
})
t.Run("IPv6", func(t *testing.T) {
clearServiceCache()
CacheServiceInfo("::1", 22, &ServiceInfo{Name: "ssh"})
if _, ok := GetCachedServiceInfo("::1", 22); !ok {
t.Error("IPv6 缓存失败")
}
})
}
// =============================================================================
// 并发安全
// =============================================================================
func TestServiceCache_ConcurrentSafety(t *testing.T) {
clearServiceCache()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(3)
go func(p int) { defer wg.Done(); CacheServiceInfo("10.0.0.1", p, &ServiceInfo{Name: "ssh"}) }(i)
go func(p int) { defer wg.Done(); GetCachedServiceInfo("10.0.0.1", p) }(i)
go func(p int) { defer wg.Done(); IsMarkedWebService("10.0.0.1", p) }(i)
}
wg.Wait()
for i := 0; i < 100; i++ {
if _, ok := GetCachedServiceInfo("10.0.0.1", i); !ok {
t.Errorf("并发写入丢失: port=%d", i)
}
}
}
// =============================================================================
// 回归测试:#588
// =============================================================================
func TestRegression_Issue588(t *testing.T) {
clearServiceCache()
registerTestPlugins(t)
CacheServiceInfo("192.168.1.50", 8881, &ServiceInfo{Name: "ssh", Version: "OpenSSH_7.4"})
strategy := NewServiceScanStrategy()
if !strategy.isPluginApplicableToPortWithHost("ssh", "192.168.1.50", 8881) {
t.Fatal("#588: SSH 应匹配 8881")
}
for _, p := range []string{"mysql", "ftp", "redis", "postgresql", "telnet", "vnc", "mssql"} {
if strategy.isPluginApplicableToPortWithHost(p, "192.168.1.50", 8881) {
t.Errorf("#588: %q 不应匹配 8881 上的 SSH", p)
}
}
}
// =============================================================================
// 端口匹配优先于缓存
// =============================================================================
func TestIntegration_PortMatchPrecedence(t *testing.T) {
clearServiceCache()
registerTestPlugins(t)
CacheServiceInfo("10.0.0.1", 22, &ServiceInfo{Name: "http"})
strategy := NewServiceScanStrategy()
if !strategy.isPluginApplicableToPortWithHost("ssh", "10.0.0.1", 22) {
t.Error("SSH 应通过端口匹配命中 22(即使缓存是 http)")
}
if !IsMarkedWebService("10.0.0.1", 22) {
t.Error("缓存是 http,应标记为 Web")
}
}
+30 -7
View File
@@ -65,6 +65,16 @@ func TestParsePortList_BasicParsing(t *testing.T) {
input: "22,80,443,3306",
expected: []int{22, 80, 443, 3306},
},
{
name: "端口范围",
input: "80-82",
expected: []int{80, 81, 82},
},
{
name: "端口和范围混合",
input: "22,80-81",
expected: []int{22, 80, 81},
},
{
name: "空字符串",
input: "",
@@ -281,7 +291,7 @@ func TestParsePortList_ProductionScenarios(t *testing.T) {
t.Run("数据库端口", func(t *testing.T) {
input := "3306,5432,1433,27017"
expected := []int{3306, 5432, 1433, 27017}
expected := []int{1433, 3306, 5432, 27017}
result := s.parsePortList(input)
if !intSlicesEqual(result, expected) {
t.Errorf("应该正确解析常见数据库端口")
@@ -317,6 +327,13 @@ func TestParsePortList_ProductionScenarios(t *testing.T) {
t.Errorf("应该正确解析高端口号")
}
})
t.Run("端口组", func(t *testing.T) {
result := s.parsePortList("web")
if !sliceContains(result, 80) || !sliceContains(result, 443) {
t.Errorf("web端口组应该包含80和443, 实际 %v", result)
}
})
}
// TestParsePortList_ReturnValue 测试返回值特性
@@ -330,14 +347,11 @@ func TestParsePortList_ReturnValue(t *testing.T) {
}
})
t.Run("端口不重复-但不保证去重", func(t *testing.T) {
// 注意:当前实现不去重,如果用户输入 "22,22",会返回 [22, 22]
// 这是可以接受的,因为上层逻辑会处理重复
t.Run("重复端口会去重", func(t *testing.T) {
input := "22,22"
result := s.parsePortList(input)
// 这里我们只测试解析是否正确,不测试去重
if len(result) != 2 || result[0] != 22 || result[1] != 22 {
t.Errorf("当前实现不去重,应该返回两个22")
if len(result) != 1 || result[0] != 22 {
t.Errorf("重复端口应该去重, 实际 %v", result)
}
})
}
@@ -355,6 +369,15 @@ func intSlicesEqual(a, b []int) bool {
return true
}
func sliceContains(values []int, target int) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
// =============================================================================
// 存活检测判断测试
// =============================================================================
+82
View File
@@ -0,0 +1,82 @@
package core
import (
"flag"
"os"
"testing"
"time"
"github.com/shadow1ng/fscan/common"
)
func TestCLIExplicitDefaultTuningFlagsSurviveTuneConfig(t *testing.T) {
oldArgs := os.Args
oldFlagSet := flag.CommandLine
oldFlagVars := *common.GetFlagVars()
defer func() {
os.Args = oldArgs
flag.CommandLine = oldFlagSet
*common.GetFlagVars() = oldFlagVars
}()
*common.GetFlagVars() = common.FlagVars{}
flag.CommandLine = flag.NewFlagSet("fscan-test", flag.ContinueOnError)
os.Args = []string{
"fscan-test",
"-silent",
"-h", "127.0.0.1",
"-time", "3",
"-mt", "20",
"-retry", "3",
"-icmp-rate", "0.1",
"-num", "20",
}
info := &common.HostInfo{}
if err := common.Flag(info); err != nil {
t.Fatalf("Flag error = %v", err)
}
cfg, _, err := common.BuildConfig(common.GetFlagVars(), info)
if err != nil {
t.Fatalf("BuildConfig error = %v", err)
}
if !cfg.TimeoutExplicit || !cfg.ModuleThreadNumExplicit ||
!cfg.MaxRetriesExplicit || !cfg.Network.ICMPRateExplicit ||
!cfg.POC.NumExplicit {
t.Fatalf("explicit flags not propagated: timeout=%v mt=%v retry=%v icmp=%v num=%v",
cfg.TimeoutExplicit,
cfg.ModuleThreadNumExplicit,
cfg.MaxRetriesExplicit,
cfg.Network.ICMPRateExplicit,
cfg.POC.NumExplicit)
}
ep := &EnvironmentProfile{
Net: NetworkProfile{
Env: EnvLAN,
RTTMedian: time.Millisecond,
RTTStddev: 200 * time.Microsecond,
LossRate: 0,
Samples: 30,
},
System: SystemProfile{FDLimit: 65536, NumCPU: 8},
}
ep.TuneConfig(cfg, makeTestSession(cfg))
if cfg.Timeout != 3*time.Second {
t.Fatalf("Timeout = %v, want explicit default 3s", cfg.Timeout)
}
if cfg.ModuleThreadNum != 20 {
t.Fatalf("ModuleThreadNum = %d, want explicit default 20", cfg.ModuleThreadNum)
}
if cfg.MaxRetries != 3 {
t.Fatalf("MaxRetries = %d, want explicit default 3", cfg.MaxRetries)
}
if cfg.Network.ICMPRate != 0.1 {
t.Fatalf("ICMPRate = %.2f, want explicit default 0.10", cfg.Network.ICMPRate)
}
if cfg.POC.Num != 20 {
t.Fatalf("POC.Num = %d, want explicit default 20", cfg.POC.Num)
}
}
+32 -14
View File
@@ -204,10 +204,11 @@ func (w *WebPortDetector) tryHTTP(ctx context.Context, client *http.Client, sess
// 基于服务指纹的Web服务识别
// ===============================
// Web服务缓存 - 简化的全局缓存
// 服务识别缓存 - 存储所有识别到的服务(不仅限于 Web)
// 端口扫描阶段写入,插件匹配阶段读取
var (
webServiceCache = make(map[string]*ServiceInfo)
webCacheMutex sync.RWMutex
serviceCache = make(map[string]*ServiceInfo)
serviceCacheMutex sync.RWMutex
)
// IsWebServiceByFingerprint 基于服务指纹判断Web服务 - 保持API兼容
@@ -259,28 +260,45 @@ func IsWebServiceByFingerprint(serviceInfo *ServiceInfo) bool {
return false
}
// MarkAsWebService 标记Web服务 - 保持API兼容
func MarkAsWebService(host string, port int, serviceInfo *ServiceInfo) {
// CacheServiceInfo 缓存识别到的服务信息
func CacheServiceInfo(host string, port int, serviceInfo *ServiceInfo) {
cacheKey := net.JoinHostPort(host, strconv.Itoa(port))
webCacheMutex.Lock()
defer webCacheMutex.Unlock()
serviceCacheMutex.Lock()
defer serviceCacheMutex.Unlock()
webServiceCache[cacheKey] = serviceInfo
serviceCache[cacheKey] = serviceInfo
}
// GetWebServiceInfo 获取Web服务信息
func GetWebServiceInfo(host string, port int) (*ServiceInfo, bool) {
// MarkAsWebService 标记 Web 服务(兼容旧调用)
func MarkAsWebService(host string, port int, serviceInfo *ServiceInfo) {
CacheServiceInfo(host, port, serviceInfo)
}
// GetCachedServiceInfo 获取缓存的服务信息
func GetCachedServiceInfo(host string, port int) (*ServiceInfo, bool) {
cacheKey := net.JoinHostPort(host, strconv.Itoa(port))
webCacheMutex.RLock()
defer webCacheMutex.RUnlock()
serviceCacheMutex.RLock()
defer serviceCacheMutex.RUnlock()
serviceInfo, exists := webServiceCache[cacheKey]
serviceInfo, exists := serviceCache[cacheKey]
return serviceInfo, exists
}
// IsMarkedWebService 检查是否已标记为Web服务
// GetWebServiceInfo 获取 Web 服务信息(兼容旧调用)
func GetWebServiceInfo(host string, port int) (*ServiceInfo, bool) {
info, exists := GetCachedServiceInfo(host, port)
if !exists {
return nil, false
}
if !IsWebServiceByFingerprint(info) {
return nil, false
}
return info, true
}
// IsMarkedWebService 检查是否为 Web 服务
func IsMarkedWebService(host string, port int) bool {
_, exists := GetWebServiceInfo(host, port)
return exists
+6 -6
View File
@@ -440,9 +440,9 @@ func TestCreateTargetFromURL(t *testing.T) {
// TestWebServiceCache 测试Web服务缓存操作
func TestWebServiceCache(t *testing.T) {
// 清空缓存
webCacheMutex.Lock()
webServiceCache = make(map[string]*ServiceInfo)
webCacheMutex.Unlock()
serviceCacheMutex.Lock()
serviceCache = make(map[string]*ServiceInfo)
serviceCacheMutex.Unlock()
t.Run("存储和读取", func(t *testing.T) {
serviceInfo := &ServiceInfo{
@@ -517,9 +517,9 @@ func TestWebServiceCache(t *testing.T) {
// TestWebServiceCache_Concurrent 测试并发安全性
func TestWebServiceCache_Concurrent(t *testing.T) {
// 清空缓存
webCacheMutex.Lock()
webServiceCache = make(map[string]*ServiceInfo)
webCacheMutex.Unlock()
serviceCacheMutex.Lock()
serviceCache = make(map[string]*ServiceInfo)
serviceCacheMutex.Unlock()
t.Run("不同key并发写入", func(t *testing.T) {
var wg sync.WaitGroup