feat: 统一服务缓存 + 指纹驱动插件匹配
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled

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

删除多余的 service_cache.go,复用已有的 ServiceInfo 体系。
补充 nil 防御、Explicit 标记、大量单元/集成/回归测试。
This commit is contained in:
ZacharyZcR
2026-06-12 19:49:07 +08:00
parent 517133f72f
commit 1595c92aed
44 changed files with 1533 additions and 142 deletions
+76 -12
View File
@@ -7,6 +7,7 @@ import (
"io"
"net"
"sync"
"sync/atomic"
"time"
"github.com/shadow1ng/fscan/common"
@@ -61,7 +62,11 @@ type AuthFunc func(ctx context.Context, cred Credential) *AuthResult
// ErrorClassifier 错误分类函数
type ErrorClassifier func(err error) ErrorType
var authCleanupWait = 2 * time.Second
var authCleanupWaitNanos int64 = int64(2 * time.Second)
func authCleanupWait() time.Duration {
return time.Duration(atomic.LoadInt64(&authCleanupWaitNanos))
}
// =============================================================================
// 单凭据测试(解决 goroutine 泄漏)
@@ -70,9 +75,36 @@ var authCleanupWait = 2 * time.Second
// TestSingleCredential 安全地测试单个凭据
// 正确处理 context 取消时的资源清理
func TestSingleCredential(ctx context.Context, cred Credential, authFn AuthFunc) *AuthResult {
if ctx == nil {
ctx = context.Background()
}
if authFn == nil {
return &AuthResult{
Success: false,
ErrorType: ErrorTypeUnknown,
Error: fmt.Errorf("auth function is nil"),
}
}
if err := ctx.Err(); err != nil {
return &AuthResult{
Success: false,
ErrorType: ErrorTypeNetwork,
Error: err,
}
}
resultChan := make(chan *AuthResult, 1)
go func() {
defer func() {
if r := recover(); r != nil {
resultChan <- &AuthResult{
Success: false,
ErrorType: ErrorTypeUnknown,
Error: fmt.Errorf("auth function panic: %v", r),
}
}
}()
result := authFn(ctx, cred)
resultChan <- result
}()
@@ -83,7 +115,7 @@ func TestSingleCredential(ctx context.Context, cred Credential, authFn AuthFunc)
case <-ctx.Done():
// context 被取消后只做有界等待,避免 authFn 卡死时清理 goroutine 也永久泄漏。
go func() {
timer := time.NewTimer(authCleanupWait)
timer := time.NewTimer(authCleanupWait())
defer timer.Stop()
select {
@@ -116,15 +148,35 @@ type ConcurrentTestConfig struct {
UseProxy bool // 代理模式下跳过直连 TCP 预检
}
func normalizeConcurrentTestConfig(testConfig ConcurrentTestConfig) ConcurrentTestConfig {
if testConfig.Concurrency <= 0 {
testConfig.Concurrency = 10
}
if testConfig.MaxRetries <= 0 {
testConfig.MaxRetries = 3
}
if testConfig.RetryDelay <= 0 {
testConfig.RetryDelay = time.Second
}
if testConfig.MaxConsecutiveNetErrors <= 0 {
testConfig.MaxConsecutiveNetErrors = 5
}
return testConfig
}
// DefaultConcurrentTestConfig 默认配置
func DefaultConcurrentTestConfig(config *common.Config) ConcurrentTestConfig {
concurrency := config.ModuleThreadNum
if concurrency <= 0 {
concurrency = 10
}
maxRetries := config.MaxRetries
if maxRetries <= 0 {
maxRetries = 3
}
return ConcurrentTestConfig{
Concurrency: concurrency,
MaxRetries: 3,
MaxRetries: maxRetries,
RetryDelay: time.Second,
MaxConsecutiveNetErrors: 5,
UseProxy: config.Network.Socks5Proxy != "" || config.Network.HTTPProxy != "",
@@ -147,6 +199,9 @@ func TestCredentialsConcurrently(
serviceName string,
testConfig ConcurrentTestConfig,
) *ScanResult {
if ctx == nil {
ctx = context.Background()
}
if len(credentials) == 0 {
return &ScanResult{
Success: false,
@@ -154,11 +209,16 @@ func TestCredentialsConcurrently(
Error: fmt.Errorf("%s", i18n.GetText("service_no_test_creds")),
}
}
testConfig = normalizeConcurrentTestConfig(testConfig)
// TCP 预检:快速验证目标可达,避免对不可达目标浪费全部凭据尝试
// 代理模式下跳过:net.DialTimeout 直连无法到达代理后的内网目标
if testConfig.TargetAddr != "" && !testConfig.UseProxy {
preConn, err := net.DialTimeout("tcp", testConfig.TargetAddr, 3*time.Second)
dialCtx, dialCancel := context.WithTimeout(ctx, 3*time.Second)
defer dialCancel()
var dialer net.Dialer
preConn, err := dialer.DialContext(dialCtx, "tcp", testConfig.TargetAddr)
if err != nil {
return &ScanResult{
Success: false,
@@ -240,10 +300,6 @@ func workerTestCredentials(
testConfig ConcurrentTestConfig,
) {
consecutiveNetErrors := 0
maxNetErrors := testConfig.MaxConsecutiveNetErrors
if maxNetErrors <= 0 {
maxNetErrors = 5
}
for cred := range credChan {
// 检查是否应该停止
@@ -254,7 +310,7 @@ func workerTestCredentials(
}
// 连续网络错误达到阈值,目标可能不可达,提前退出
if consecutiveNetErrors >= maxNetErrors {
if consecutiveNetErrors >= testConfig.MaxConsecutiveNetErrors {
return
}
@@ -292,10 +348,18 @@ func testCredentialWithRetry(
// 测试凭据
result := TestSingleCredential(ctx, cred, authFn)
if result == nil {
result = &AuthResult{
Success: false,
ErrorType: ErrorTypeUnknown,
Error: fmt.Errorf("auth function returned nil result"),
}
}
if result.Success && result.Conn != nil {
// 成功,关闭连接并返回
_ = result.Conn.Close()
if result.Success {
if result.Conn != nil {
_ = result.Conn.Close()
}
return &ScanResult{
Type: plugins.ResultTypeCredential,
Success: true,