mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-02-09 02:09:17 +08:00
perf(scan): 实现启发式优化提升扫描体验
1. 端口优先级排序:高价值端口(80,443,22,3389等)优先扫描 - 用户能更快看到有意义的结果 - 不影响端口喷洒策略 2. TCP 补充探测:ICMP 响应率<10%时自动启用 - 对未响应主机用 TCP 80/443/22/445 补充探测 - 解决防火墙过滤 ICMP 导致漏检的问题
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
143
core/icmp.go
143
core/icmp.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user