fix(proxy): 修复代理模式下服务识别错误和端口漏扫问题

- port_scan.go: 验证通过后重建干净连接,避免HTTP GET探测污染服务识别
- port_scan.go: 优化验证策略,用轻量CRLF探测替代HTTP GET,超时从2.2s降至0.6s
- manager.go: 修正ProbeProxyBehavior判断逻辑,超时应视为代理正常转发
This commit is contained in:
ZacharyZcR
2026-01-21 17:57:09 +08:00
parent 98641d49b8
commit 927bdbab92
2 changed files with 122 additions and 33 deletions

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"net"
"net/url"
"strings"
"sync"
"sync/atomic"
"time"
@@ -368,6 +367,13 @@ func (s *socks5Dialer) updateAverageConnectTime(duration time.Duration) {
// ProbeProxyBehavior 探测代理是否存在"全回显"问题
// 通过连接一个几乎肯定不可达的地址,并尝试发送数据来判断代理行为
// 返回 true 表示代理可靠false 表示代理存在全回显问题
//
// 判断标准:
// - 连接失败 → 可靠(代理正确拒绝不可达目标)
// - 写入失败 → 可靠(代理在数据传输时报告错误)
// - 读取超时 → 可靠(代理转发了请求,目标没响应是正常的)
// - 读取错误 → 可靠(代理正确报告了目标不可达)
// - 收到数据 → 不可靠(代理伪造了响应)
func ProbeProxyBehavior(dialer Dialer, timeout time.Duration) bool {
// 使用 RFC 5737 保留的测试 IP (TEST-NET-1) + 高端口
// 192.0.2.1 是文档专用地址,保证不会路由到真实主机
@@ -402,14 +408,8 @@ func ProbeProxyBehavior(dialer Dialer, timeout time.Duration) bool {
n, readErr := conn.Read(buf)
if readErr != nil {
// 读取超时或错误 = 目标不可达,代理行为正常
// 说明代理没有伪造响应
errStr := readErr.Error()
if strings.Contains(errStr, "timeout") {
// 超时可能意味着代理接受了连接但没有伪造响应
// 这是边界情况,暂时认为不可靠
return false
}
// 读取超时或错误 = 目标不可达,代理行为正常
// 超时说明代理正确转发了请求,目标没有响应是正常的
// 其他错误reset, refused等说明代理正确报告了目标不可达
return true
}
@@ -419,6 +419,6 @@ func ProbeProxyBehavior(dialer Dialer, timeout time.Duration) bool {
return false
}
// 代理接受连接且不报错也不响应 = 全回显行为
return false
// 无错误且无数据 = EOF说明连接被正常关闭代理行为正常
return true
}

View File

@@ -109,6 +109,63 @@ func (f *failedPortCollector) Count() int {
return count
}
// preResolveDomains 预解析域名列表
// 将域名解析为 IP解析失败的域名保留原样
// 这样可以避免扫描过程中大量并发 DNS 查询导致系统资源耗尽
func preResolveDomains(hosts []string, timeout time.Duration) []string {
if len(hosts) == 0 {
return hosts
}
// 检查是否有需要解析的域名
needResolve := false
for _, host := range hosts {
if net.ParseIP(host) == nil {
needResolve = true
break
}
}
if !needResolve {
return hosts
}
common.LogDebug(fmt.Sprintf("[DNS] 开始预解析 %d 个主机", len(hosts)))
result := make([]string, 0, len(hosts))
resolved := 0
failed := 0
for _, host := range hosts {
// 如果已经是 IP直接添加
if net.ParseIP(host) != nil {
result = append(result, host)
continue
}
// 尝试解析域名
ips, err := net.LookupHost(host)
if err != nil || len(ips) == 0 {
// 解析失败,保留原域名(后续连接时会再次尝试解析)
common.LogDebug(fmt.Sprintf("[DNS] 解析失败: %s - %v", host, err))
result = append(result, host)
failed++
continue
}
// 使用第一个解析结果
result = append(result, ips[0])
resolved++
common.LogDebug(fmt.Sprintf("[DNS] %s -> %s", host, ips[0]))
}
if resolved > 0 || failed > 0 {
common.LogDebug(fmt.Sprintf("[DNS] 预解析完成: 成功=%d, 失败=%d", resolved, failed))
}
return result
}
// estimateScanTime 估算扫描时间
// 参数: totalTasks - 总任务数, threads - 线程数, timeout - 超时时间(秒)
// 返回: 估算的扫描时间(秒)
@@ -134,12 +191,18 @@ func estimateScanTime(totalTasks int, threads int, timeout int64) int64 {
// EnhancedPortScan 高性能端口扫描函数
// 使用滑动窗口调度 + 自适应线程池 + 流式迭代器
func EnhancedPortScan(hosts []string, ports string, timeout int64, config *common.Config, state *common.State) []string {
common.LogDebug(fmt.Sprintf("[PortScan] 开始: %d个主机, 线程数=%d", len(hosts), config.ThreadNum))
// 预解析域名,避免扫描过程中大量 DNS 查询导致问题
hosts = preResolveDomains(hosts, time.Duration(timeout)*time.Second)
// 解析端口和排除端口
portList := parsers.ParsePort(ports)
if len(portList) == 0 {
common.LogError(i18n.Tr("invalid_port", ports))
return nil
}
common.LogDebug(fmt.Sprintf("[PortScan] 端口解析完成: %d个端口", len(portList)))
// 使用config中的排除端口配置
excludePorts := parsers.ParsePort(config.Target.ExcludePorts)
@@ -156,16 +219,28 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
// 创建流式迭代器O(1) 内存,端口喷洒策略)
iter := NewSocketIterator(hosts, portList, exclude)
totalTasks := iter.Total()
common.LogDebug(fmt.Sprintf("[PortScan] 总任务数: %d", totalTasks))
// 使用传入的配置
threadNum := config.ThreadNum
// 大规模扫描警告和线程数自动调整
if totalTasks > 100000 {
common.LogBase(fmt.Sprintf("[*] 大规模扫描: %d 个目标 (%d主机 × %d端口)", totalTasks, len(hosts), len(portList)))
// 如果任务数超过100万且线程数大于300自动降低线程数
if totalTasks > 1000000 && threadNum > 300 {
oldThreadNum := threadNum
threadNum = 300
common.LogBase(fmt.Sprintf("[*] 自动调整线程数: %d -> %d (大规模扫描优化)", oldThreadNum, threadNum))
}
}
// 初始化端口扫描进度条
if totalTasks > 0 && config.Output.ShowProgress {
description := fmt.Sprintf("端口扫描中(%d线程", threadNum)
common.InitProgressBar(int64(totalTasks), description)
}
common.LogDebug("[PortScan] 进度条初始化完成")
// 初始化并发控制
to := time.Duration(timeout) * time.Second
@@ -174,6 +249,7 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
failedCollector := &failedPortCollector{}
var wg sync.WaitGroup
common.LogDebug(fmt.Sprintf("[PortScan] 开始创建线程池, size=%d", threadNum))
// 创建自适应线程池(支持动态调整)
pool, err := NewAdaptivePool(threadNum, func(task interface{}) {
taskInfo, ok := task.(portScanTask)
@@ -193,10 +269,13 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
common.LogError(i18n.Tr("thread_pool_create_failed", err))
return nil
}
common.LogDebug("[PortScan] 线程池创建成功")
defer pool.Release()
common.LogDebug("[PortScan] 开始滑动窗口调度")
// 滑动窗口调度:维护固定数量的"飞行中"任务
slidingWindowSchedule(iter, pool, &wg, threadNum)
common.LogDebug("[PortScan] 滑动窗口调度完成")
// 收集结果
aliveAddrs := collector.GetAll()
@@ -361,6 +440,18 @@ func scanSinglePort(host string, port int, addr string, timeout time.Duration, c
return
}
// 步骤1.6:如果使用了代理且进行了数据交互,需要重建连接
// 因为验证阶段可能读取了Banner或发送了HTTP GET探测污染了连接状态
if common.IsProxyEnabled() && verifyMethod != "direct" {
_ = conn.Close()
// 重新建立干净的连接用于服务识别
conn, err = connectWithRetry(addr, timeout, 3, state)
if err != nil {
handleConnectionFailure(err, host, port, addr, failedCollector)
return
}
}
// 步骤2记录开放端口
atomic.AddInt64(count, 1)
collector.Add(addr)
@@ -387,62 +478,60 @@ func isTimeoutError(err error) bool {
return err != nil && strings.Contains(err.Error(), "i/o timeout")
}
// verifyProxyConnectionDeep 深度验证代理连接是否真正可用4阶段验证
// verifyProxyConnectionDeep 深度验证代理连接是否真正可用
// 防止透明代理/全回显代理的假连接问题
// 返回: (是否有效, 验证方式)
//
// 优化策略:
// 1. 快速 Banner 检测 (100ms) - 大部分服务会主动发送数据
// 2. 轻量探测 (发送 \r\n) - 触发某些服务响应,同时不污染协议状态
// 3. 短超时等待 (500ms) - 平衡准确性和性能
func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
// 如果没有使用代理,跳过验证
if !common.IsProxyEnabled() {
return true, "direct"
}
// 阶段1: 读取 Banner (200ms)
// 某些服务会主动发送欢迎消息
_ = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
buf := make([]byte, 256)
// 阶段1: 快速读取 Banner (100ms)
// 大部分服务SSH、FTP、SMTP、MySQL等会主动发送欢迎消息
_ = conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
n, _ := conn.Read(buf)
_ = conn.SetReadDeadline(time.Time{}) // 重置 deadline
_ = conn.SetReadDeadline(time.Time{})
if n > 0 {
// 收到数据,检查是否为代理错误响应
if isProxyErrorResponse(buf[:n]) {
common.LogDebug(fmt.Sprintf("代理返回错误响应 %s", addr))
return false, "proxy_error"
}
// 收到有效 Banner确认端口开放
return true, "banner"
}
// 阶段2: 发送探测数据 (HTTP GET with Host header)
// 使用完整的 HTTP 请求以获得服务器响应
// 从 addr 提取主机名作为 Host 头HTTP 服务器需要正确的 Host 才会响应)
host := addr
if colonIdx := strings.LastIndex(addr, ":"); colonIdx > 0 {
host = addr[:colonIdx]
}
probeReq := fmt.Sprintf("GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", host)
// 阶段2: 轻量探测 - 发送 CRLF 触发响应
// 使用 \r\n 而非 HTTP GET因为
// - 不会污染大部分协议状态
// - 某些服务(如 HTTP会返回 400 Bad Request
// - 速度快,只需极短超时
_ = conn.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))
_, writeErr := conn.Write([]byte(probeReq))
_ = conn.SetWriteDeadline(time.Time{}) // 重置 deadline
_, writeErr := conn.Write([]byte("\r\n"))
_ = conn.SetWriteDeadline(time.Time{})
if writeErr != nil && isConnectionClosed(writeErr) {
// 写入失败broken pipe/reset= 连接已被拒绝
common.LogDebug(fmt.Sprintf("探测写入失败 %s: %v", addr, writeErr))
return false, "write_failed"
}
// 阶段3: 等待探测响应 (2s - 需要足够时间让 HTTP 服务器响应)
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
// 阶段3: 等待探测响应 (500ms)
_ = conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
n, readErr := conn.Read(buf)
_ = conn.SetReadDeadline(time.Time{})
if n > 0 {
// 收到响应,检查是否为代理错误
if isProxyErrorResponse(buf[:n]) {
common.LogDebug(fmt.Sprintf("代理探测返回错误 %s", addr))
return false, "proxy_error"
}
// 收到有效响应,确认端口开放
return true, "probe"
}