perf(scan): 实现启发式优化提升扫描体验

1. 端口优先级排序:高价值端口(80,443,22,3389等)优先扫描
   - 用户能更快看到有意义的结果
   - 不影响端口喷洒策略

2. TCP 补充探测:ICMP 响应率<10%时自动启用
   - 对未响应主机用 TCP 80/443/22/445 补充探测
   - 解决防火墙过滤 ICMP 导致漏检的问题
This commit is contained in:
ZacharyZcR
2026-01-21 19:17:49 +08:00
parent 5ecd3cfe4d
commit 849c28ede2
5 changed files with 252 additions and 5 deletions

View File

@@ -269,6 +269,10 @@ segment_16_alive:
other: "{{.Arg1}}.0.0/16 segment alive: {{.Arg2}}"
segment_24_alive:
other: "{{.Arg1}}.0/24 segment alive: {{.Arg2}}"
tcp_probe_low_icmp_rate:
other: "Low ICMP response rate ({{.Arg1}}), enabling TCP supplementary probe ({{.Arg2}} hosts)"
tcp_probe_found:
other: "TCP probe found {{.Arg1}} alive hosts"
# ========================= Alive Scan Stats Messages =========================
parse_target_failed:

View File

@@ -269,6 +269,10 @@ segment_16_alive:
other: "{{.Arg1}}.0.0/16 网段存活: {{.Arg2}}"
segment_24_alive:
other: "{{.Arg1}}.0/24 网段存活: {{.Arg2}}"
tcp_probe_low_icmp_rate:
other: "ICMP响应率过低({{.Arg1}})启用TCP补充探测({{.Arg2}}个主机)"
tcp_probe_found:
other: "TCP补充探测发现 {{.Arg1}} 个存活主机"
# ========================= 存活扫描统计消息 =========================
parse_target_failed:

View File

@@ -38,6 +38,7 @@ var pingErrorKeywords = []string{
}
// CheckLive 检测主机存活状态
// 支持 ICMP/Ping 探测,并在响应率过低时自动启用 TCP 补充探测
func CheckLive(hostslist []string, Ping bool, config *common.Config, state *common.State) []string {
// 创建局部WaitGroup
var livewg sync.WaitGroup
@@ -65,12 +66,53 @@ func CheckLive(hostslist []string, Ping bool, config *common.Config, state *comm
livewg.Wait()
close(chanHosts)
// TCP 补充探测:当 ICMP/Ping 响应率过低时自动启用
// 这对防火墙过滤 ICMP 的环境特别有用
aliveHosts = tcpSupplementaryProbe(hostslist, aliveHosts, config)
// 输出存活统计信息
printAliveStats(aliveHosts, hostslist)
return aliveHosts
}
// tcpSupplementaryProbe TCP 补充探测
// 当 ICMP 响应率过低时(<10%),对未响应主机进行 TCP 探测
func tcpSupplementaryProbe(allHosts []string, aliveHosts []string, config *common.Config) []string {
totalHosts := len(allHosts)
if totalHosts == 0 {
return aliveHosts
}
// 计算 ICMP 响应率
responseRate := float64(len(aliveHosts)) / float64(totalHosts)
// 响应率高于阈值,无需补充探测
if responseRate >= tcpProbeThreshold {
return aliveHosts
}
// 获取未响应的主机
unrespondedHosts := getUnrespondedHosts(allHosts, aliveHosts)
if len(unrespondedHosts) == 0 {
return aliveHosts
}
// 提示用户正在进行 TCP 补充探测
common.LogInfo(i18n.Tr("tcp_probe_low_icmp_rate", fmt.Sprintf("%.1f%%", responseRate*100), len(unrespondedHosts)))
// 执行 TCP 补充探测
tcpAliveHosts := runTcpProbeForHosts(unrespondedHosts, config)
// 合并结果
if len(tcpAliveHosts) > 0 {
aliveHosts = append(aliveHosts, tcpAliveHosts...)
common.LogInfo(i18n.Tr("tcp_probe_found", len(tcpAliveHosts)))
}
return aliveHosts
}
// IsContain 检查切片中是否包含指定元素
func IsContain(items []string, item string) bool {
for _, eachItem := range items {
@@ -622,3 +664,104 @@ func ArrayCountValueTop(arrInit []string, length int, flag bool) (arrTop []strin
return
}
// =============================================================================
// TCP 补充探测 - 当 ICMP 响应率过低时自动启用
// =============================================================================
// tcpProbeCommonPorts TCP 探测使用的常用端口
// 这些端口在大多数服务器上至少有一个开放
var tcpProbeCommonPorts = []int{80, 443, 22, 445}
// tcpProbeTimeout TCP 探测超时时间(较短,只做存活判断)
const tcpProbeTimeout = 2 * time.Second
// tcpProbeThreshold TCP 补充探测触发阈值
// 当 ICMP 响应率低于此值时,自动启用 TCP 补充探测
const tcpProbeThreshold = 0.1 // 10%
// tcpProbeAlive 使用 TCP 探测主机是否存活
// 尝试连接常用端口,任一端口响应即认为存活
func tcpProbeAlive(host string) bool {
for _, port := range tcpProbeCommonPorts {
addr := fmt.Sprintf("%s:%d", host, port)
conn, err := common.WrapperTcpWithTimeout("tcp", addr, tcpProbeTimeout)
if err == nil {
_ = conn.Close()
return true
}
}
return false
}
// runTcpProbeForHosts 对指定主机列表进行 TCP 补充探测
// 返回存活的主机列表
func runTcpProbeForHosts(hosts []string, config *common.Config) []string {
if len(hosts) == 0 {
return nil
}
var wg sync.WaitGroup
var mu sync.Mutex
aliveHosts := make([]string, 0)
// 并发控制,避免资源耗尽
concurrency := 50
if len(hosts) < concurrency {
concurrency = len(hosts)
}
limiter := make(chan struct{}, concurrency)
for _, host := range hosts {
wg.Add(1)
limiter <- struct{}{}
go func(h string) {
defer func() {
<-limiter
wg.Done()
}()
if tcpProbeAlive(h) {
mu.Lock()
aliveHosts = append(aliveHosts, h)
mu.Unlock()
// 保存结果
result := &output.ScanResult{
Time: time.Now(),
Type: output.TypeHost,
Target: h,
Status: "alive",
Details: map[string]interface{}{
"protocol": "TCP",
},
}
_ = common.SaveResult(result)
if !config.Output.Silent {
common.LogInfo(i18n.Tr("host_alive", h, "TCP"))
}
}
}(host)
}
wg.Wait()
return aliveHosts
}
// getUnrespondedHosts 获取未响应的主机列表
func getUnrespondedHosts(allHosts []string, aliveHosts []string) []string {
aliveSet := make(map[string]struct{}, len(aliveHosts))
for _, h := range aliveHosts {
aliveSet[h] = struct{}{}
}
unresponded := make([]string, 0, len(allHosts)-len(aliveHosts))
for _, h := range allHosts {
if _, alive := aliveSet[h]; !alive {
unresponded = append(unresponded, h)
}
}
return unresponded
}

View File

@@ -1,9 +1,35 @@
package core
import (
"sort"
"sync"
)
// highPriorityPorts 高价值端口优先级表
// 数字越小优先级越高,用户最关心这些服务能快速出结果
var highPriorityPorts = map[int]int{
80: 1, // HTTP
443: 2, // HTTPS
22: 3, // SSH
3389: 4, // RDP
445: 5, // SMB
3306: 6, // MySQL
1433: 7, // MSSQL
6379: 8, // Redis
21: 9, // FTP
23: 10, // Telnet
8080: 11, // HTTP-Alt
8443: 12, // HTTPS-Alt
5432: 13, // PostgreSQL
27017: 14, // MongoDB
1521: 15, // Oracle
5900: 16, // VNC
25: 17, // SMTP
110: 18, // POP3
143: 19, // IMAP
53: 20, // DNS
}
// SocketIterator 流式生成 host:port 组合
// 设计原则O(1) 内存,按需生成
// 使用端口喷洒策略Port1全IP -> Port2全IP -> ...
@@ -18,15 +44,50 @@ type SocketIterator struct {
}
// NewSocketIterator 创建流式迭代器
// 自动对端口进行智能排序:高价值端口优先,让用户更快看到有意义的结果
func NewSocketIterator(hosts []string, ports []int, exclude map[int]struct{}) *SocketIterator {
validPorts := filterExcludedPorts(ports, exclude)
sortedPorts := sortPortsByPriority(validPorts)
return &SocketIterator{
hosts: hosts,
ports: validPorts,
total: len(hosts) * len(validPorts),
ports: sortedPorts,
total: len(hosts) * len(sortedPorts),
}
}
// sortPortsByPriority 智能排序端口
// 策略:高价值端口优先,其余按数字升序
func sortPortsByPriority(ports []int) []int {
if len(ports) <= 1 {
return ports
}
result := make([]int, len(ports))
copy(result, ports)
sort.Slice(result, func(i, j int) bool {
pi, pj := result[i], result[j]
priI, okI := highPriorityPorts[pi]
priJ, okJ := highPriorityPorts[pj]
// 都有优先级:按优先级排序
if okI && okJ {
return priI < priJ
}
// 只有一个有优先级:有优先级的排前面
if okI {
return true
}
if okJ {
return false
}
// 都没有优先级:按端口号升序
return pi < pj
})
return result
}
// Next 返回下一个 host:port 组合ok=false 表示迭代结束
// 端口喷洒顺序先遍历所有IP的同一端口再换下一个端口
func (it *SocketIterator) Next() (string, int, bool) {

View File

@@ -127,7 +127,7 @@ func TestSocketIterator_ExcludePorts(t *testing.T) {
it := NewSocketIterator(hosts, ports, exclude)
// 应该只有22和443
// 应该只有22和443按优先级排序443(优先级2) 在 22(优先级3) 之前
var gotPorts []int
for {
_, port, ok := it.Next()
@@ -140,8 +140,9 @@ func TestSocketIterator_ExcludePorts(t *testing.T) {
if len(gotPorts) != 2 {
t.Fatalf("期望2个端口, 实际 %d", len(gotPorts))
}
if gotPorts[0] != 22 || gotPorts[1] != 443 {
t.Errorf("期望 [22, 443], 实际 %v", gotPorts)
// 443优先级高于22所以443在前
if gotPorts[0] != 443 || gotPorts[1] != 22 {
t.Errorf("期望 [443, 22] (按优先级排序), 实际 %v", gotPorts)
}
// 验证Total也正确
@@ -181,6 +182,40 @@ func TestSocketIterator_EmptyInputs(t *testing.T) {
})
}
// TestSocketIterator_PortPrioritySort 验证端口优先级排序
// 高价值端口80, 443, 22等应该排在前面
func TestSocketIterator_PortPrioritySort(t *testing.T) {
hosts := []string{"192.168.1.1"}
// 故意乱序输入,包含高优先级和普通端口
ports := []int{9999, 22, 8888, 80, 7777, 443, 3389, 1234}
it := NewSocketIterator(hosts, ports, nil)
var gotPorts []int
for {
_, port, ok := it.Next()
if !ok {
break
}
gotPorts = append(gotPorts, port)
}
// 期望顺序:高优先级端口按优先级排序,然后是普通端口按数字升序
// 80(优先级1), 443(2), 22(3), 3389(4), 然后 1234, 7777, 8888, 9999
expected := []int{80, 443, 22, 3389, 1234, 7777, 8888, 9999}
if len(gotPorts) != len(expected) {
t.Fatalf("端口数量不匹配: 期望 %d, 实际 %d", len(expected), len(gotPorts))
}
for i, exp := range expected {
if gotPorts[i] != exp {
t.Errorf("第%d个端口: 期望 %d, 实际 %d\n完整结果: %v", i, exp, gotPorts[i], gotPorts)
break
}
}
}
// TestSocketIterator_SingleElements 验证单元素情况
func TestSocketIterator_SingleElements(t *testing.T) {
t.Run("单IP单端口", func(t *testing.T) {