Files
fscan/core/icmp.go
ZacharyZcR 849c28ede2 perf(scan): 实现启发式优化提升扫描体验
1. 端口优先级排序:高价值端口(80,443,22,3389等)优先扫描
   - 用户能更快看到有意义的结果
   - 不影响端口喷洒策略

2. TCP 补充探测:ICMP 响应率<10%时自动启用
   - 对未响应主机用 TCP 80/443/22/445 补充探测
   - 解决防火墙过滤 ICMP 导致漏检的问题
2026-01-21 19:17:49 +08:00

768 lines
20 KiB
Go
Raw Permalink 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 (
"bytes"
"errors"
"fmt"
"net"
"os/exec"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/common/output"
"golang.org/x/net/icmp"
)
// pingForbiddenChars 命令注入防护 - 禁止的字符
var pingForbiddenChars = []string{";", "&", "|", "`", "$", "\\", "'", "%", "\"", "\n"}
// pingErrorKeywords ping 失败的关键词(跨平台)
var pingErrorKeywords = []string{
// Windows
"TTL expired",
"Destination host unreachable",
"Destination net unreachable",
"Request timed out",
"General failure",
"transmit failed",
// Linux/macOS
"Time to live exceeded",
"100% packet loss",
"Network is unreachable",
"No route to host",
}
// CheckLive 检测主机存活状态
// 支持 ICMP/Ping 探测,并在响应率过低时自动启用 TCP 补充探测
func CheckLive(hostslist []string, Ping bool, config *common.Config, state *common.State) []string {
// 创建局部WaitGroup
var livewg sync.WaitGroup
// 创建局部存活主机列表,预分配容量避免频繁扩容
aliveHosts := make([]string, 0, len(hostslist))
var aliveHostsMu sync.Mutex // 保护aliveHosts并发访问
existHosts := make(map[string]struct{}, len(hostslist))
// 创建主机通道
chanHosts := make(chan string, len(hostslist))
// 处理存活主机
go handleAliveHosts(chanHosts, hostslist, Ping, &aliveHosts, &aliveHostsMu, existHosts, config, &livewg)
// 根据Ping参数选择检测方式
if Ping {
// 使用ping方式探测
RunPing(hostslist, chanHosts, &livewg)
} else {
probeWithICMP(hostslist, chanHosts, &aliveHosts, &aliveHostsMu, config, state, &livewg)
}
// 等待所有检测完成
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 {
if eachItem == item {
return true
}
}
return false
}
func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool, aliveHosts *[]string, aliveHostsMu *sync.Mutex, existHosts map[string]struct{}, config *common.Config, livewg *sync.WaitGroup) {
for ip := range chanHosts {
if _, ok := existHosts[ip]; !ok && IsContain(hostslist, ip) {
existHosts[ip] = struct{}{}
// 加锁保护aliveHosts并发写入
aliveHostsMu.Lock()
*aliveHosts = append(*aliveHosts, ip)
aliveHostsMu.Unlock()
// 使用Output系统保存存活主机信息
protocol := "ICMP"
if isPing {
protocol = "PING"
}
result := &output.ScanResult{
Time: time.Now(),
Type: output.TypeHost,
Target: ip,
Status: "alive",
Details: map[string]interface{}{
"protocol": protocol,
},
}
_ = common.SaveResult(result)
// 保留原有的控制台输出
if !config.Output.Silent {
common.LogInfo(i18n.Tr("host_alive", ip, protocol))
}
}
livewg.Done()
}
}
// probeWithICMP 使用ICMP方式探测
func probeWithICMP(hostslist []string, chanHosts chan string, aliveHosts *[]string, aliveHostsMu *sync.Mutex, config *common.Config, state *common.State, livewg *sync.WaitGroup) {
// 代理模式下自动禁用ICMP直接降级为Ping
// ICMP在代理环境无法正常工作
if shouldDisableICMP() {
if !config.Output.Silent {
common.LogInfo(i18n.GetText("proxy_mode_disable_icmp"))
}
RunPing(hostslist, chanHosts, livewg)
return
}
// 尝试监听本地ICMP
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err == nil {
RunIcmp1(hostslist, conn, chanHosts, aliveHosts, aliveHostsMu, config, state, livewg)
return
}
common.LogError(i18n.Tr("icmp_listen_failed", err))
common.LogInfo(i18n.GetText("trying_no_listen_icmp"))
// 尝试无监听ICMP探测
conn2, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second)
if err == nil {
defer func() { _ = conn2.Close() }()
RunIcmp2(hostslist, chanHosts, config, state, livewg)
return
}
common.LogError(i18n.Tr("icmp_connect_failed", err))
common.LogError(i18n.GetText("insufficient_privileges"))
common.LogInfo(i18n.GetText("switching_to_ping"))
// 降级使用ping探测
RunPing(hostslist, chanHosts, livewg)
}
// shouldDisableICMP 检查是否应该禁用ICMP
// 这是一个内部辅助函数,用于检查代理状态
func shouldDisableICMP() bool {
// 尝试导入proxy包的状态检查避免循环依赖
// 实际实现中会通过全局配置检查
// 这里暂时返回false实际集成时会正确处理
return false
}
// getOptimalTopCount 根据扫描规模智能决定显示数量
func getOptimalTopCount(totalHosts int) int {
switch {
case totalHosts > 50000: // 超大规模扫描
return 20
case totalHosts > 10000: // 大规模扫描
return 15
case totalHosts > 1000: // 中等规模扫描
return 10
case totalHosts > 256: // 小规模扫描
return 5
default:
return 3
}
}
// printAliveStats 打印存活统计信息
func printAliveStats(aliveHosts []string, hostslist []string) {
// 智能计算显示数量
topCount := getOptimalTopCount(len(hostslist))
// 大规模扫描时输出 /16 网段统计
if len(hostslist) > 1000 {
arrTop, arrLen := ArrayCountValueTop(aliveHosts, topCount, true)
for i := 0; i < len(arrTop); i++ {
common.LogInfo(i18n.Tr("segment_16_alive", arrTop[i], arrLen[i]))
}
}
// 输出 /24 网段统计
if len(hostslist) > 256 {
arrTop, arrLen := ArrayCountValueTop(aliveHosts, topCount, false)
for i := 0; i < len(arrTop); i++ {
common.LogInfo(i18n.Tr("segment_24_alive", arrTop[i], arrLen[i]))
}
}
}
// ICMP 自适应等待参数
const (
icmpCheckInterval = 100 * time.Millisecond // 检查间隔,避免 CPU 空转
icmpMinWaitTime = 1 * time.Second // 最小等待时间,确保基础响应收集
icmpStableThreshold = 500 * time.Millisecond // 无新响应稳定阈值,超过此时间无新响应则提前结束
)
// waitAdaptive 自适应等待 ICMP 响应
// 算法:监控响应增量,连续一段时间无新响应则提前结束
// 保守原则:
// - 必须等待最小时间 (1s),确保基础响应收集
// - 只有"连续 500ms 无新响应"才提前结束
// - 保留原有最大等待时间作为兜底
func waitAdaptive(hostslist []string, aliveHosts *[]string, aliveHostsMu *sync.Mutex) {
totalHosts := len(hostslist)
// 根据主机数量设置最大超时时间(保持原有逻辑作为兜底)
maxWait := 6 * time.Second
if totalHosts <= 256 {
maxWait = 3 * time.Second
}
start := time.Now()
lastAliveCount := 0
lastChangeTime := start
for {
time.Sleep(icmpCheckInterval) // 避免 CPU 空转
// 读取当前存活数
aliveHostsMu.Lock()
aliveCount := len(*aliveHosts)
aliveHostsMu.Unlock()
elapsed := time.Since(start)
// 条件1所有主机都已响应立即结束
if aliveCount >= totalHosts {
common.LogDebug(fmt.Sprintf("[ICMP] 全部响应,耗时 %v", elapsed.Round(time.Millisecond)))
break
}
// 条件2超过最大等待时间兜底结束
if elapsed >= maxWait {
common.LogDebug(fmt.Sprintf("[ICMP] 达到最大等待时间 %v存活 %d/%d", maxWait, aliveCount, totalHosts))
break
}
// 条件3自适应提前结束
// 必须满足:已过最小等待时间 + 连续一段时间没有新响应
if elapsed >= icmpMinWaitTime {
if aliveCount > lastAliveCount {
// 有新响应,更新状态
lastChangeTime = time.Now()
lastAliveCount = aliveCount
} else if time.Since(lastChangeTime) >= icmpStableThreshold {
// 连续 500ms 没有新响应,认为响应已稳定,提前结束
common.LogDebug(fmt.Sprintf("[ICMP] 响应稳定,提前结束,耗时 %v存活 %d/%d",
elapsed.Round(time.Millisecond), aliveCount, totalHosts))
break
}
} else {
// 最小等待期内,持续更新状态
if aliveCount > lastAliveCount {
lastChangeTime = time.Now()
lastAliveCount = aliveCount
}
}
}
}
// RunIcmp1 使用ICMP批量探测主机存活(监听模式)
func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string, aliveHosts *[]string, aliveHostsMu *sync.Mutex, config *common.Config, state *common.State, livewg *sync.WaitGroup) {
// 使用atomic.Bool保证并发安全
var endflag atomic.Bool
var listenerWg sync.WaitGroup
// 创建布隆过滤器用于去重(自动根据主机数量调整大小)
bloomFilter := NewBloomFilter(len(hostslist), 0.01)
// 启动监听协程
listenerWg.Add(1)
go func() {
defer listenerWg.Done()
defer func() {
if r := recover(); r != nil {
common.LogError(i18n.Tr("icmp_listener_panic", r))
}
}()
for {
if endflag.Load() {
return
}
// 设置读取超时避免无限期阻塞
_ = conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
// 接收ICMP响应
msg := make([]byte, 100)
_, sourceIP, err := conn.ReadFrom(msg)
if err != nil {
// 超时错误正常,其他错误则退出
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
continue
}
return
}
if sourceIP != nil && !endflag.Load() {
ipStr := sourceIP.String()
// 使用布隆过滤器去重过滤重复的ICMP响应和杂包
if bloomFilter.Contains(ipStr) {
continue
}
bloomFilter.Add(ipStr)
livewg.Add(1)
select {
case chanHosts <- ipStr:
// 发送成功
default:
// channel已满回退计数器
livewg.Done()
}
}
}
}()
// 发送ICMP请求应用令牌桶限速
limiter := state.GetICMPLimiter(config.Network.ICMPRate)
for _, host := range hostslist {
limiter.Wait(1) // 等待令牌,控制发包速率
dst, _ := net.ResolveIPAddr("ip", host)
IcmpByte := makemsg(host)
_, _ = conn.WriteTo(IcmpByte, dst)
}
// 自适应等待响应
// 算法:监控响应增量,连续一段时间无新响应则提前结束
// 保守原则:保留最大等待时间兜底,确保不漏掉慢响应主机
waitAdaptive(hostslist, aliveHosts, aliveHostsMu)
endflag.Store(true)
_ = conn.Close()
listenerWg.Wait()
}
// RunIcmp2 使用ICMP并发探测主机存活(无监听模式)
func RunIcmp2(hostslist []string, chanHosts chan string, config *common.Config, state *common.State, livewg *sync.WaitGroup) {
// 控制并发数
num := 1000
if len(hostslist) < num {
num = len(hostslist)
}
var wg sync.WaitGroup
limiter := make(chan struct{}, num)
rateLimiter := state.GetICMPLimiter(config.Network.ICMPRate) // 获取速率限制器
// 并发探测
for _, host := range hostslist {
wg.Add(1)
limiter <- struct{}{}
go func(host string) {
defer func() {
<-limiter
wg.Done()
}()
rateLimiter.Wait(1) // 等待令牌,控制发包速率
if icmpalive(host) {
livewg.Add(1)
select {
case chanHosts <- host:
// 发送成功
default:
// channel已满回退计数器
livewg.Done()
}
}
}(host)
}
wg.Wait()
close(limiter)
}
// icmpalive 检测主机ICMP是否存活
func icmpalive(host string) bool {
startTime := time.Now()
// 建立ICMP连接
conn, err := net.DialTimeout("ip4:icmp", host, 6*time.Second)
if err != nil {
return false
}
defer func() { _ = conn.Close() }()
// 设置超时时间
if err := conn.SetDeadline(startTime.Add(6 * time.Second)); err != nil {
return false
}
// 构造并发送ICMP请求
msg := makemsg(host)
if _, err := conn.Write(msg); err != nil {
return false
}
// 接收ICMP响应
receive := make([]byte, 60)
if _, err := conn.Read(receive); err != nil {
return false
}
return true
}
// RunPing 使用系统Ping命令并发探测主机存活
func RunPing(hostslist []string, chanHosts chan string, livewg *sync.WaitGroup) {
var wg sync.WaitGroup
// 限制并发数为50
limiter := make(chan struct{}, 50)
// 并发探测
for _, host := range hostslist {
wg.Add(1)
limiter <- struct{}{}
go func(host string) {
defer func() {
<-limiter
wg.Done()
}()
if ExecCommandPing(host) {
livewg.Add(1)
select {
case chanHosts <- host:
// 发送成功
default:
// channel已满回退计数器
livewg.Done()
}
}
}(host)
}
wg.Wait()
}
// containsPingError 检查 ping 输出是否包含错误关键词
func containsPingError(output string) bool {
outputLower := strings.ToLower(output)
for _, keyword := range pingErrorKeywords {
if strings.Contains(outputLower, strings.ToLower(keyword)) {
return true
}
}
return false
}
// ExecCommandPing 执行系统Ping命令检测主机存活
func ExecCommandPing(ip string) bool {
// 过滤黑名单字符(命令注入防护)
for _, char := range pingForbiddenChars {
if strings.Contains(ip, char) {
return false
}
}
var command *exec.Cmd
// 根据操作系统选择不同的ping命令
switch runtime.GOOS {
case "windows":
command = exec.Command("cmd", "/c", "ping -n 1 -w 1 "+ip+" && echo true || echo false")
case "darwin":
command = exec.Command("/bin/bash", "-c", "ping -c 1 -W 1 "+ip+" && echo true || echo false")
default: // linux
command = exec.Command("/bin/bash", "-c", "ping -c 1 -w 1 "+ip+" && echo true || echo false")
}
// 捕获命令输出
var outinfo bytes.Buffer
command.Stdout = &outinfo
// 执行命令
if err := command.Start(); err != nil {
return false
}
if err := command.Wait(); err != nil {
return false
}
// 分析输出结果
output := outinfo.String()
return strings.Contains(output, "true") && strings.Count(output, ip) > 2 && !containsPingError(output)
}
// makemsg 构造ICMP echo请求消息
func makemsg(host string) []byte {
msg := make([]byte, 40)
// 获取标识符
id0, id1 := genIdentifier(host)
// 设置ICMP头部
msg[0] = 8 // Type: Echo Request
msg[1] = 0 // Code: 0
msg[2] = 0 // Checksum高位(待计算)
msg[3] = 0 // Checksum低位(待计算)
msg[4], msg[5] = id0, id1 // Identifier
msg[6], msg[7] = genSequence(1) // Sequence Number
// 计算校验和
check := checkSum(msg[0:40])
msg[2] = byte(check >> 8) // 设置校验和高位
msg[3] = byte(check & 255) // 设置校验和低位
return msg
}
// checkSum 计算ICMP校验和
func checkSum(msg []byte) uint16 {
sum := 0
length := len(msg)
// 按16位累加
for i := 0; i < length-1; i += 2 {
sum += int(msg[i])*256 + int(msg[i+1])
}
// 处理奇数长度情况
if length%2 == 1 {
sum += int(msg[length-1]) * 256
}
// 将高16位加到低16位
sum = (sum >> 16) + (sum & 0xffff)
sum = sum + (sum >> 16)
// 取反得到校验和
return uint16(^sum)
}
// genSequence 生成ICMP序列号
func genSequence(v int16) (byte, byte) {
ret1 := byte(v >> 8) // 高8位
ret2 := byte(v & 255) // 低8位
return ret1, ret2
}
// genIdentifier 根据主机地址生成标识符
func genIdentifier(host string) (byte, byte) {
if len(host) < 2 {
return 0, 0
}
return host[0], host[1]
}
// ArrayCountValueTop 统计IP地址段存活数量并返回TOP N结果
func ArrayCountValueTop(arrInit []string, length int, flag bool) (arrTop []string, arrLen []int) {
if len(arrInit) == 0 {
return
}
// 统计各网段出现次数,预分配容量
segmentCounts := make(map[string]int, len(arrInit)/4)
for _, ip := range arrInit {
segments := strings.Split(ip, ".")
if len(segments) != 4 {
continue
}
// 根据flag确定统计B段还是C段
var segment string
if flag {
segment = fmt.Sprintf("%s.%s", segments[0], segments[1]) // B段
} else {
segment = fmt.Sprintf("%s.%s.%s", segments[0], segments[1], segments[2]) // C段
}
segmentCounts[segment]++
}
// 创建副本用于排序
sortMap := make(map[string]int)
for k, v := range segmentCounts {
sortMap[k] = v
}
// 获取TOP N结果
for i := 0; i < length && len(sortMap) > 0; i++ {
maxSegment := ""
maxCount := 0
// 查找当前最大值
for segment, count := range sortMap {
if count > maxCount {
maxCount = count
maxSegment = segment
}
}
// 添加到结果集
arrTop = append(arrTop, maxSegment)
arrLen = append(arrLen, maxCount)
// 从待处理map中删除已处理项
delete(sortMap, maxSegment)
}
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
}