Files
fscan/core/port_scan_test.go
T
ZacharyZcR 5ad914a1bb feat: 统一服务缓存 + 指纹驱动插件匹配
将 webServiceCache 扩展为通用 serviceCache,所有指纹识别结果
统一缓存,插件匹配时端口不命中则回退到服务名称匹配。

删除多余的 service_cache.go,复用已有的 ServiceInfo 体系。
补充 nil 防御、Explicit 标记、大量单元/集成/回归测试。
2026-06-14 22:23:47 +08:00

790 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package core
import (
"fmt"
"testing"
)
/*
port_scan_test.go - EnhancedPortScan 核心逻辑测试
注意:EnhancedPortScan 是一个228行的"上帝函数",耦合了:
- 网络IO (TCP连接)
- 并发控制 (errgroup, semaphore)
- 全局状态 (common.*全局变量)
- 进度条管理
- 服务识别
- 结果保存
这种设计无法进行真正的单元测试。本测试文件:
1. 验证核心算法逻辑的正确性(通过独立函数模拟)
2. 测试关键计算逻辑(任务数计算、排除端口)
3. 不测试网络IO和并发控制(需要集成测试)
"这函数需要重构,不是测试。200行代码做了太多事情。
但既然现在无法重构,我们至少验证算法逻辑是对的。"
*/
// =============================================================================
// 核心算法逻辑测试(从EnhancedPortScan提取)
// =============================================================================
// calculateTotalTasks 计算总扫描任务数(从EnhancedPortScan:34-42行提取)
// 这是纯函数,可以独立测试
func calculateTotalTasks(hosts []string, portList []int, exclude map[int]struct{}) int {
totalTasks := 0
for range hosts {
for _, port := range portList {
if _, excluded := exclude[port]; !excluded {
totalTasks++
}
}
}
return totalTasks
}
// TestCalculateTotalTasks 测试总任务数计算逻辑
func TestCalculateTotalTasks(t *testing.T) {
tests := []struct {
name string
hosts []string
portList []int
exclude map[int]struct{}
expected int
}{
{
name: "单主机单端口-无排除",
hosts: []string{"192.168.1.1"},
portList: []int{80},
exclude: map[int]struct{}{},
expected: 1,
},
{
name: "单主机多端口-无排除",
hosts: []string{"192.168.1.1"},
portList: []int{80, 443, 8080},
exclude: map[int]struct{}{},
expected: 3,
},
{
name: "多主机单端口-无排除",
hosts: []string{"192.168.1.1", "192.168.1.2", "192.168.1.3"},
portList: []int{80},
exclude: map[int]struct{}{},
expected: 3,
},
{
name: "多主机多端口-无排除",
hosts: []string{"192.168.1.1", "192.168.1.2"},
portList: []int{80, 443, 8080},
exclude: map[int]struct{}{},
expected: 6, // 2 hosts * 3 ports
},
{
name: "单主机多端口-排除一个",
hosts: []string{"192.168.1.1"},
portList: []int{80, 443, 8080},
exclude: map[int]struct{}{443: {}},
expected: 2, // 80, 8080
},
{
name: "多主机多端口-排除多个",
hosts: []string{"192.168.1.1", "192.168.1.2"},
portList: []int{80, 443, 8080, 3306},
exclude: map[int]struct{}{443: {}, 3306: {}},
expected: 4, // 2 hosts * 2 ports (80, 8080)
},
{
name: "空主机列表",
hosts: []string{},
portList: []int{80, 443},
exclude: map[int]struct{}{},
expected: 0,
},
{
name: "空端口列表",
hosts: []string{"192.168.1.1"},
portList: []int{},
exclude: map[int]struct{}{},
expected: 0,
},
{
name: "所有端口都被排除",
hosts: []string{"192.168.1.1"},
portList: []int{80, 443},
exclude: map[int]struct{}{80: {}, 443: {}},
expected: 0,
},
{
name: "大规模扫描",
hosts: []string{"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4", "192.168.1.5"},
portList: []int{21, 22, 23, 80, 443, 3306, 3389, 8080, 8443, 9090},
exclude: map[int]struct{}{},
expected: 50, // 5 hosts * 10 ports
},
{
name: "大规模扫描-部分排除",
hosts: []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"},
portList: []int{80, 443, 8080, 8443, 3000, 3001, 3002, 3003, 3004, 3005},
exclude: map[int]struct{}{8080: {}, 8443: {}},
expected: 24, // 3 hosts * 8 ports
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := calculateTotalTasks(tt.hosts, tt.portList, tt.exclude)
if result != tt.expected {
t.Errorf("calculateTotalTasks() = %d, 期望 %d", result, tt.expected)
}
})
}
}
// =============================================================================
// 地址格式化逻辑测试(从EnhancedPortScan:67行提取)
// =============================================================================
// formatAddress 格式化主机:端口地址(从EnhancedPortScan提取)
func formatAddress(host string, port int) string {
return fmt.Sprintf("%s:%d", host, port)
}
// TestFormatAddress 测试地址格式化
func TestFormatAddress(t *testing.T) {
tests := []struct {
name string
host string
port int
expected string
}{
{
name: "标准IPv4地址",
host: "192.168.1.1",
port: 80,
expected: "192.168.1.1:80",
},
{
name: "域名",
host: "example.com",
port: 443,
expected: "example.com:443",
},
{
name: "localhost",
host: "localhost",
port: 8080,
expected: "localhost:8080",
},
{
name: "高端口号",
host: "10.0.0.1",
port: 65535,
expected: "10.0.0.1:65535",
},
{
name: "低端口号",
host: "10.0.0.1",
port: 1,
expected: "10.0.0.1:1",
},
{
name: "常见HTTP端口",
host: "192.168.1.100",
port: 80,
expected: "192.168.1.100:80",
},
{
name: "常见HTTPS端口",
host: "192.168.1.100",
port: 443,
expected: "192.168.1.100:443",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatAddress(tt.host, tt.port)
if result != tt.expected {
t.Errorf("formatAddress() = %q, 期望 %q", result, tt.expected)
}
})
}
}
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行提取)
// =============================================================================
// buildExcludeMap 构建排除端口映射(从EnhancedPortScan提取)
func buildExcludeMap(excludePorts []int) map[int]struct{} {
exclude := make(map[int]struct{}, len(excludePorts))
for _, p := range excludePorts {
exclude[p] = struct{}{}
}
return exclude
}
// TestBuildExcludeMap 测试排除端口映射构建
func TestBuildExcludeMap(t *testing.T) {
tests := []struct {
name string
excludePorts []int
testPort int
shouldExclude bool
}{
{
name: "空排除列表",
excludePorts: []int{},
testPort: 80,
shouldExclude: false,
},
{
name: "单个排除端口-匹配",
excludePorts: []int{443},
testPort: 443,
shouldExclude: true,
},
{
name: "单个排除端口-不匹配",
excludePorts: []int{443},
testPort: 80,
shouldExclude: false,
},
{
name: "多个排除端口-匹配第一个",
excludePorts: []int{80, 443, 8080},
testPort: 80,
shouldExclude: true,
},
{
name: "多个排除端口-匹配中间",
excludePorts: []int{80, 443, 8080},
testPort: 443,
shouldExclude: true,
},
{
name: "多个排除端口-匹配最后",
excludePorts: []int{80, 443, 8080},
testPort: 8080,
shouldExclude: true,
},
{
name: "多个排除端口-不匹配",
excludePorts: []int{80, 443, 8080},
testPort: 3306,
shouldExclude: false,
},
{
name: "大量排除端口",
excludePorts: []int{21, 22, 23, 25, 53, 110, 143, 445, 3389},
testPort: 3389,
shouldExclude: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
excludeMap := buildExcludeMap(tt.excludePorts)
// 验证映射大小
if len(excludeMap) != len(tt.excludePorts) {
t.Errorf("excludeMap长度 = %d, 期望 %d", len(excludeMap), len(tt.excludePorts))
}
// 验证端口是否被正确排除
_, excluded := excludeMap[tt.testPort]
if excluded != tt.shouldExclude {
t.Errorf("端口 %d 排除状态 = %v, 期望 %v", tt.testPort, excluded, tt.shouldExclude)
}
})
}
}
// TestBuildExcludeMap_DuplicatePorts 测试重复端口处理
func TestBuildExcludeMap_DuplicatePorts(t *testing.T) {
excludePorts := []int{80, 443, 80, 443, 80}
excludeMap := buildExcludeMap(excludePorts)
// 重复端口应该被去重(map自动去重)
if len(excludeMap) != 2 {
t.Errorf("excludeMap应自动去重, 期望长度2, 实际 %d", len(excludeMap))
}
// 验证两个端口都存在
if _, ok := excludeMap[80]; !ok {
t.Error("端口80应在排除列表中")
}
if _, ok := excludeMap[443]; !ok {
t.Error("端口443应在排除列表中")
}
}
// =============================================================================
// 集成逻辑测试(任务数计算 + 排除端口)
// =============================================================================
// TestIntegratedTaskCalculation 测试任务计算与排除端口的集成
func TestIntegratedTaskCalculation(t *testing.T) {
tests := []struct {
name string
hosts []string
portList []int
excludePorts []int
expected int
}{
{
name: "无排除-小规模",
hosts: []string{"192.168.1.1", "192.168.1.2"},
portList: []int{80, 443, 8080},
excludePorts: []int{},
expected: 6, // 2*3
},
{
name: "有排除-小规模",
hosts: []string{"192.168.1.1", "192.168.1.2"},
portList: []int{80, 443, 8080},
excludePorts: []int{443},
expected: 4, // 2*2
},
{
name: "大规模C段扫描",
hosts: make([]string, 254), // 模拟254个主机
portList: []int{80, 443, 22, 3389, 3306},
excludePorts: []int{22}, // 排除SSH
expected: 1016, // 254 * 4
},
{
name: "端口全排除",
hosts: []string{"192.168.1.1"},
portList: []int{80, 443},
excludePorts: []int{80, 443},
expected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 填充大规模测试的hosts
if len(tt.hosts) == 254 && tt.hosts[0] == "" {
for i := range tt.hosts {
tt.hosts[i] = fmt.Sprintf("192.168.1.%d", i+1)
}
}
excludeMap := buildExcludeMap(tt.excludePorts)
result := calculateTotalTasks(tt.hosts, tt.portList, excludeMap)
if result != tt.expected {
t.Errorf("集成测试失败: calculateTotalTasks() = %d, 期望 %d", result, tt.expected)
}
})
}
}
// =============================================================================
// 边界情况和错误处理测试
// =============================================================================
// TestCalculateTotalTasks_EdgeCases 测试边界情况
func TestCalculateTotalTasks_EdgeCases(t *testing.T) {
t.Run("nil主机列表", func(t *testing.T) {
result := calculateTotalTasks(nil, []int{80}, map[int]struct{}{})
if result != 0 {
t.Errorf("nil主机列表应返回0, 实际 %d", result)
}
})
t.Run("nil端口列表", func(t *testing.T) {
result := calculateTotalTasks([]string{"192.168.1.1"}, nil, map[int]struct{}{})
if result != 0 {
t.Errorf("nil端口列表应返回0, 实际 %d", result)
}
})
t.Run("nil排除映射", func(t *testing.T) {
result := calculateTotalTasks([]string{"192.168.1.1"}, []int{80}, nil)
if result != 1 {
t.Errorf("nil排除映射应视为无排除, 期望1, 实际 %d", result)
}
})
t.Run("极大端口号", func(t *testing.T) {
excludeMap := buildExcludeMap([]int{65535})
if _, ok := excludeMap[65535]; !ok {
t.Error("应支持最大端口号65535")
}
})
t.Run("端口号0", func(t *testing.T) {
excludeMap := buildExcludeMap([]int{0})
if _, ok := excludeMap[0]; !ok {
t.Error("应支持端口号0")
}
})
}
// =============================================================================
// 性能基准测试
// =============================================================================
// BenchmarkCalculateTotalTasks 基准测试任务计算性能
func BenchmarkCalculateTotalTasks(b *testing.B) {
// 模拟C段扫描: 254个主机 * 10个端口
hosts := make([]string, 254)
for i := range hosts {
hosts[i] = fmt.Sprintf("192.168.1.%d", i+1)
}
portList := []int{21, 22, 80, 443, 3306, 3389, 8080, 8443, 9090, 9200}
exclude := map[int]struct{}{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
calculateTotalTasks(hosts, portList, exclude)
}
}
// BenchmarkBuildExcludeMap 基准测试排除映射构建性能
func BenchmarkBuildExcludeMap(b *testing.B) {
excludePorts := []int{21, 22, 23, 25, 53, 110, 143, 445, 3389, 1433}
b.ResetTimer()
for i := 0; i < b.N; i++ {
buildExcludeMap(excludePorts)
}
}
// =============================================================================
// 重构后函数的单元测试
// =============================================================================
// TestBuildServiceLogMessage 测试服务日志消息构建
// 新格式: "addr service version/banner"
func TestBuildServiceLogMessage(t *testing.T) {
tests := []struct {
name string
addr string
serviceInfo *ServiceInfo
isWeb bool
wantContain []string // 期望包含的字符串片段
}{
{
name: "基础HTTP服务",
addr: "192.168.1.1:80",
serviceInfo: &ServiceInfo{
Name: "http",
Version: "1.1",
Banner: "",
Extras: map[string]string{},
},
isWeb: true,
wantContain: []string{"http://192.168.1.1", "http", "1.1"},
},
{
name: "非标准端口HTTP服务显示URL",
addr: "192.168.1.1:8080",
serviceInfo: &ServiceInfo{
Name: "http",
Version: "1.1",
Banner: "",
Extras: map[string]string{},
},
isWeb: true,
wantContain: []string{"http://192.168.1.1:8080", "http", "1.1"},
},
{
name: "HTTPS服务显示HTTPS URL",
addr: "192.168.1.1:443",
serviceInfo: &ServiceInfo{
Name: "https",
Version: "1.1",
Banner: "",
Extras: map[string]string{},
},
isWeb: true,
wantContain: []string{"https://192.168.1.1", "https", "1.1"},
},
{
name: "带Banner的SSH服务",
addr: "10.0.0.1:22",
serviceInfo: &ServiceInfo{
Name: "ssh",
Version: "OpenSSH_8.0",
Banner: "SSH-2.0-OpenSSH_8.0",
Extras: map[string]string{},
},
isWeb: false,
wantContain: []string{"10.0.0.1:22", "ssh", "SSH-2.0-OpenSSH_8.0"}, // Banner优先于Version
},
{
name: "带扩展信息的服务",
addr: "172.16.0.1:3306",
serviceInfo: &ServiceInfo{
Name: "mysql",
Version: "5.7.30",
Banner: "",
Extras: map[string]string{
"vendor_product": "MySQL Community Server",
"os": "Linux",
"info": "utf8_general_ci",
},
},
isWeb: false,
wantContain: []string{"172.16.0.1:3306", "mysql", "5.7.30"}, // 简化格式不包含Extras
},
{
name: "未知服务",
addr: "192.168.1.1:8888",
serviceInfo: &ServiceInfo{
Name: "unknown",
Version: "",
Banner: "",
Extras: map[string]string{},
},
isWeb: false,
wantContain: []string{"192.168.1.1:8888"}, // unknown服务不显示名称
},
{
name: "过长Banner使用Version",
addr: "10.0.0.1:21",
serviceInfo: &ServiceInfo{
Name: "ftp",
Version: "2.0",
Banner: string(make([]byte, 200)), // 超过100字符的banner
Extras: map[string]string{},
},
isWeb: false,
wantContain: []string{"10.0.0.1:21", "ftp", "2.0"}, // Banner超长则用Version
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := buildServiceLogMessage(tt.addr, tt.serviceInfo, tt.isWeb)
// 验证所有期望的字符串片段都存在
for _, want := range tt.wantContain {
if !contains(result, want) {
t.Errorf("buildServiceLogMessage() 结果缺少期望内容\n期望包含: %q\n实际结果: %q", want, result)
}
}
})
}
}
// contains 检查字符串是否包含子串
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > 0 && len(substr) > 0 && indexOf(s, substr) >= 0))
}
// indexOf 查找子串位置
func indexOf(s, substr string) int {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}
// =============================================================================
// 资源耗尽错误检测测试
// =============================================================================
/*
资源耗尽错误检测 - isResourceExhaustedError 函数测试
测试价值:资源耗尽检测是生产环境的关键逻辑,错误分类影响重试策略
"这是真正的业务逻辑。错误分类错了,扫描就会失败或死循环。
这种函数必须测试,而且要测真实的错误场景。"
*/
// TestIsResourceExhaustedError_ActualErrors 测试真实的资源耗尽错误
func TestIsResourceExhaustedError_ActualErrors(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{
name: "文件描述符耗尽-Linux",
err: fmt.Errorf("socket: too many open files"),
expected: true,
},
{
name: "文件描述符耗尽-直接错误",
err: fmt.Errorf("too many open files"),
expected: true,
},
{
name: "缓冲区耗尽",
err: fmt.Errorf("write: no buffer space available"),
expected: true,
},
{
name: "本地端口耗尽",
err: fmt.Errorf("dial tcp: cannot assign requested address"),
expected: true,
},
{
name: "连接重置-高并发",
err: fmt.Errorf("read tcp 192.168.1.1:1234->10.0.0.1:80: connection reset by peer"),
expected: true,
},
{
name: "自定义发包限制",
err: fmt.Errorf("发包受限"),
expected: true,
},
{
name: "nil错误",
err: nil,
expected: false,
},
{
name: "普通网络错误-超时",
err: fmt.Errorf("dial tcp: i/o timeout"),
expected: false,
},
{
name: "普通网络错误-拒绝连接",
err: fmt.Errorf("connection refused"),
expected: false,
},
{
name: "认证错误",
err: fmt.Errorf("authentication failed"),
expected: false,
},
{
name: "空字符串错误",
err: fmt.Errorf(""),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isResourceExhaustedError(tt.err)
if result != tt.expected {
t.Errorf("isResourceExhaustedError() = %v, want %v (error: %v)",
result, tt.expected, tt.err)
}
})
}
}
// TestIsResourceExhaustedError_EdgeCases 测试边界情况
func TestIsResourceExhaustedError_EdgeCases(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{
name: "大小写混合",
err: fmt.Errorf("Too Many Open Files"),
expected: true, // containsFold 不区分大小写
},
{
name: "错误信息包含但不完全匹配",
err: fmt.Errorf("some error with no buffer space available suffix"),
expected: true, // strings.Contains会匹配完整短语
},
{
name: "多个错误特征-只需匹配一个",
err: fmt.Errorf("too many open files and no buffer space available"),
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isResourceExhaustedError(tt.err)
if result != tt.expected {
t.Errorf("isResourceExhaustedError() = %v, want %v (error: %v)",
result, tt.expected, tt.err)
}
})
}
}
// TestIsResourceExhaustedError_ProductionScenarios 测试生产环境真实场景
func TestIsResourceExhaustedError_ProductionScenarios(t *testing.T) {
// 场景1ulimit设置太低
t.Run("ulimit限制触发", func(t *testing.T) {
err := fmt.Errorf("dial tcp 10.0.0.1:22: socket: too many open files")
if !isResourceExhaustedError(err) {
t.Error("应该识别出ulimit限制错误")
}
})
// 场景2Windows端口耗尽
t.Run("Windows端口耗尽", func(t *testing.T) {
err := fmt.Errorf("dial tcp :0: bind: cannot assign requested address")
if !isResourceExhaustedError(err) {
t.Error("应该识别出端口耗尽错误")
}
})
// 场景3:并发扫描导致的连接重置
t.Run("高并发连接重置", func(t *testing.T) {
err := fmt.Errorf("read tcp: connection reset by peer")
if !isResourceExhaustedError(err) {
t.Error("应该识别出高并发导致的连接重置")
}
})
// 场景4:正常的认证失败不应被识别为资源耗尽
t.Run("认证失败-不是资源问题", func(t *testing.T) {
err := fmt.Errorf("ssh: handshake failed: ssh: unable to authenticate")
if isResourceExhaustedError(err) {
t.Error("认证失败不应被识别为资源耗尽")
}
})
}