Files
fscan/core/icmp.go
T
ZacharyZcRandr00t 2c2ca6ace3 v2.1.3 Release (#572)
* add CVE-2026-24061 detect logic  (#562)

* add CVE-2026-24061 detect logic

* fix(telnet): 修复 errcheck 警告,统一错误处理风格

---------

Co-authored-by: ZacharyZcR <[email protected]>

* fix: 修复 Hub 广播 data race 和端口扫描潜在死锁,清理死代码

- hub.go: broadcast 路径 RLock 改 Lock,修复并发 delete/close 竞争
- port_scan.go: pool.Invoke 失败时释放 wg 和 semaphore,防止死锁
- web_scanner.go: 删除只写不读的 fingerprintCache
- webtitle.go: 移除对已删除 SetFingerprints 的调用
- keylogger.go: 删除未使用的 stopChan 和 isRunning 字段

* refactor: context 穿透扫描生命周期,修复长驻插件阻塞和 Web Stop 无效

- RunScan 接受 context.Context,创建可取消上下文并穿透到所有策略和插件
- 长驻插件(forwardshell/socks5proxy/reverseshell)不再进入 scan WaitGroup,
  通过 ctx.Done() 管理生命周期,解除 wg.Wait() 死锁
- Web Stop API 从 stopChan 改为 context.CancelFunc,取消信号真正传播到扫描链路
- ExecuteScanTasks 和 executeScanTask 支持 context 取消检查,停止分发新任务
- CLI 模式传 context.Background(),行为完全不变

* fix: 修复 Web Stop 信号等待阻塞和 SMB 响应解析越界 panic

- scanner.go: 长驻插件等待信号时同时监听 ctx.Done(),Web Stop 可正常返回
- smb_protocol.go: 响应长度检查修正为 47,远端偏移量全部做边界校验

* fix: POC 扫描接入调用方 context,修复 cachedPocPath 竞争和 ProxyStats data race

- webscan/web_scan.go: WebScan 接受 ctx 参数,替换 context.Background();
  sync.Once 改为 sync.Mutex 保护 POC 加载,消除 cachedPocPath 并发写竞争
- webtitle.go: ctx 从 Scan 穿透到 identifyFingerprintsMulti → triggerPocScan → WebScan
- webpoc.go: 传递 ctx 到 WebScan
- proxy/types.go: ProxyStats 增加 sync.Mutex
- proxy/manager.go: LastConnectTime/LastError/AverageConnectTime 读写加锁

* fix: 修复 ProxyStats 含 mutex 导致的 copylocks 告警

Stats() 方法改为手动构造副本,避免值拷贝复制 sync.Mutex

* fix: 补全 HTTP/TLS proxy stats 加锁,修复 RPC/SMB 解析越界和 POC 加载逻辑

- httpdialer.go/tlsdialer.go: LastError/LastConnectTime/AverageConnectTime 加 mutex
- findnet.go: RPC 响应结束标记位置 < 4 时跳过截断,防止负数切片 panic
- ms17010.go: SMB 会话响应最小长度改为 45,sessionSetupResponse 加长度校验
- web_scan.go: POC 加载失败时不标记 pocLoaded,允许后续重试
- Eval.go: DNSLog 配置去掉 sync.Once,允许多次扫描更新配置

* fix: Web 全局状态同步、字典文件错误提示、长驻插件连接可取消

- scan.go: Web API 构建 config/state 后同步到全局实例
- config_builder.go: 用户名/密码/URL 文件读取失败时输出错误日志
- reverseshell.go: 读命令设 1s 超时,超时后检查 ctx 实现可取消
- forwardshell.go: handleClient 接受 ctx,取消时关闭连接解除阻塞
- socks5proxy.go: handleClient 接受 ctx,取消时关闭连接解除 IO 阻塞

* refactor: 引入 ScanSession,替代全局状态穿透扫描管道 (Phase 1-3)

- 新增 common/session.go: ScanSession 结构体封装 Config/State/Params/Dialer
- RunScan/Strategy/ExecuteScanTasks/executeScanTask 全部接收 session
- Plugin 接口从 Scan(ctx, info, config, state) 改为 Scan(ctx, info, session)
- 48 个插件实现统一更新签名
- Web API 构建 ScanSession 传给 RunScan
- CLI 模式通过 Initialize() 创建 session

* refactor: 全量替换 WrapperTcpWithTimeout 为 session.DialTCP (Phase 4)

- core/port_scan.go: EnhancedPortScan/connectWithRetry/scanSinglePort 接入 session
- core/service_probe.go: SmartPortInfoScanner 持有 session,重连走 session.DialTCP
- core/icmp.go: CheckLive/tcpProbeAlive 接入 session
- 17 个 service 插件: 内部 helper 函数全部穿透 ctx+session
- 移除插件中冗余的手动 TCP 计数(DialTCP 内部已处理)
- plugins/core 下已无 WrapperTcpWithTimeout/SafeTCPDial 调用残留

* refactor: 清除 core/plugins 全局状态依赖,ProgressManager 缓存引用 (Phase 5)

- core/alive_scanner.go: GetFlagVars() → session.Params
- core/service_scanner.go: GetFlagVars() → session.Params 和 config.Target.Ports
- common/progress_manager.go: 缓存 State 和 NoColor 到字段,不再运行时读全局
- common/output_api.go: SaveResult 改用 GetGlobalConfig().Output.DisableSave
- common/network.go: WrapperTcpWithTimeout 标记 Deprecated
- core/ 和 plugins/ 下已无全局状态调用残留

* fix: 修复 dialer timeout 锁死、CVE 检测绕过 session 和误报问题

* fix: 修复 pocDNSLog data race,穿透 ctx 到全链路,消除残余 net.DialTimeout 绕过

* perf: CVE-2026-24061 检测改并发执行,消除硬 sleep 用 deadline 替代

* feat: 项目缓存系统,跨扫描合并资产,缓存 host:port 避免漏报

* perf: 三阶段性能优化,ICMP 并发提升+TCP 并行探测,端口扫描退避调整,服务探测超时减半

* fix: 修复凭据测试清理 goroutine 无限阻塞导致的 goroutine 泄漏

* fix: 凭据测试连续网络错误短路、resultChan 缓冲防阻塞、timer 泄漏修复

* perf: 大规模扫描网段预筛,按 /24 探活跳过空子网,B 段扫描从 2h+ 降至 2min

* fix: 网段预筛从抽样改全覆盖,每台主机发 1 个探测包,消除漏报

* perf: 网段预筛增加网关启发式,.1/.254 多端口优先探测,命中即跳过逐主机兜底

* fix: MSSQL 连接加 encrypt=disable 修复无 TLS 环境扫描失败,Web API 参数校验负数

* feat: Release 增加 armv5 架构支持

* chore: bump version to 2.1.3

* fix: 锁定 golangci-lint 版本为 v2.12.1 修复 CI checksum 校验失败

* fix: golangci-lint 改用 go install 安装,绕过上游安装脚本 checksum 校验问题

* feat: -silent 模式输出 NDJSON 到 stdout,支持 AI agent 管道消费

- 新增 StdoutNDJSONWriter,silent 模式下每条扫描结果实时输出一行 JSON
- LogWithProgress 层拦截人类可读日志,绕过 logger sync.Once 初始化时序问题
- 支持 fscan -h xxx -silent | jq 管道用法

* fix rdp invalid random panic (#573)

* restore ms17010 legacy detection and exploit (#574)

* fix ms17010 legacy packet decoding (#574)

* fix csv web title output (#575)

* fix web result protocol output (#577)

* feat: add -ntp flag to disable TCP supplementary probe

* fix: skip TCP supplementary probe in icmp mode

* feat: add -debug flag with file logging to fscan_debug.log

* fix: resolve golangci-lint errcheck and staticcheck warnings

* fix: skip proxy deep verification for SOCKS5 connections (#579)

SOCKS5 protocol validates connection reachability at protocol level,
deep verification was incorrectly rejecting non-banner services like
SMB(445), RPC(139) and Kerberos(88).

* fix: exclude timeout from scan failure rate calculation (#578)

Timeout is a normal scan result when firewalls drop packets, not a
scan failure. Only resource exhaustion errors count toward failure rate.

* feat: flatten NDJSON output for AI agent consumption and add SKILL.md

* perf: 端口扫描自适应超时,基于 RTT 采样动态调整连接超时

* perf: 四项扫描性能优化

- SO_LINGER=0 快速释放连接,减少 TIME_WAIT 堆积
- 服务探测超时自适应,RTT 采样约束读超时上限
- 端口扫描结果流式传递,pipeline 并行端口扫描和插件执行
- ICMP 批量预构建包和地址,减少发送循环开销

* perf: 六项性能优化

- DNS 解析缓存:sync.Map 缓存避免重复系统调用
- 凭据测试 TCP 预检:不可达目标直接跳过全部凭据
- Web 探测 HTTP Client 复用:全局共享连接池
- 端口扫描 Bloom Filter 去重:替代 map 降低内存
- 进度条 atomic 累加 + 50ms 节流渲染:消除锁竞争
- 服务探针预解码:Init 时预编译,运行时零解码开销

* refactor: replace bloom filter with map for deduplication

Bloom filter has false positive risk which can silently drop valid
scan results. Map provides exact deduplication with negligible memory
overhead at the scale of open ports (typically thousands, not millions).

* fix: credential TCP precheck bypass proxy and pipeline goroutine leak

- Skip TCP precheck when proxy is enabled, net.DialTimeout cannot
  reach targets behind SOCKS5/HTTP proxy
- Drain stream channel on ctx cancellation to prevent EnhancedPortScan
  goroutine from blocking on a full channel

* fix: stream channel 提前返回未关闭导致 goroutine 泄漏,服务探测超时下限 500ms

* fix: resolve golangci-lint errcheck and staticcheck warnings

---------

Co-authored-by: r00t <[email protected]>
2026-05-13 14:41:23 +08:00

802 lines
21 KiB
Go

package core
import (
"bytes"
"context"
"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(ctx context.Context, hostslist []string, Ping bool, session *common.ScanSession) []string {
config := session.Config
state := session.State
// 创建局部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(ctx, hostslist, aliveHosts, session)
// 输出存活统计信息
printAliveStats(aliveHosts, hostslist)
return aliveHosts
}
// tcpSupplementaryProbe TCP 补充探测
// 当 ICMP 响应率过低时(<10%),对未响应主机进行 TCP 探测
func tcpSupplementaryProbe(ctx context.Context, allHosts []string, aliveHosts []string, session *common.ScanSession) []string {
if session.Config.DisableTcpProbe || session.Config.Mode == "icmp" {
return aliveHosts
}
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(ctx, unrespondedHosts, session)
// 合并结果
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
// 去重集合:过滤重复的ICMP响应
seen := make(map[string]struct{}, len(hostslist))
// 启动监听协程
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()
if _, dup := seen[ipStr]; dup {
continue
}
seen[ipStr] = struct{}{}
livewg.Add(1)
select {
case chanHosts <- ipStr:
// 发送成功
default:
// channel已满,回退计数器
livewg.Done()
}
}
}
}()
// 发送ICMP请求(批量预构建 + 令牌桶限速)
// 预构建所有 ICMP 包和目标地址,减少发送循环中的开销
type icmpPacket struct {
data []byte
dst net.Addr
}
packets := make([]icmpPacket, 0, len(hostslist))
for _, host := range hostslist {
dst, _ := common.DNSCache.ResolveIP(host)
packets = append(packets, icmpPacket{data: makemsg(host), dst: dst})
}
limiter := state.GetICMPLimiter(config.Network.ICMPRate)
for i := range packets {
limiter.Wait(1)
_, _ = conn.WriteTo(packets[i].data, packets[i].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
// 并发数根据主机数动态调整,上限 200
concurrency := len(hostslist)
if concurrency > 200 {
concurrency = 200
}
limiter := make(chan struct{}, concurrency)
// 并发探测
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 = 1 * time.Second
// tcpProbeThreshold TCP 补充探测触发阈值
// 当 ICMP 响应率低于此值时,自动启用 TCP 补充探测
const tcpProbeThreshold = 0.1 // 10%
// tcpProbeAlive 使用 TCP 并行探测主机是否存活
// 同时连接所有常用端口,任一响应即返回
func tcpProbeAlive(ctx context.Context, session *common.ScanSession, host string) bool {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
result := make(chan bool, len(tcpProbeCommonPorts))
for _, port := range tcpProbeCommonPorts {
go func(p int) {
addr := fmt.Sprintf("%s:%d", host, p)
conn, err := session.DialTCP(ctx, "tcp", addr, tcpProbeTimeout)
if err == nil {
_ = conn.Close()
result <- true
return
}
result <- false
}(port)
}
for range tcpProbeCommonPorts {
if <-result {
return true
}
}
return false
}
// runTcpProbeForHosts 对指定主机列表进行 TCP 补充探测
// 返回存活的主机列表
func runTcpProbeForHosts(ctx context.Context, hosts []string, session *common.ScanSession) []string {
config := session.Config
if len(hosts) == 0 {
return nil
}
var wg sync.WaitGroup
var mu sync.Mutex
aliveHosts := make([]string, 0)
// 并发控制,根据主机数动态调整,上限 200
concurrency := len(hosts)
if concurrency > 200 {
concurrency = 200
}
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(ctx, session, 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
}