mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 11:20:41 +08:00
## 架构重构
- 全局变量消除,迁移至 Config/State 对象
- SMB 插件融合(smb/smb2/smbghost/smbinfo)
- 服务探测重构,实现 Nmap 风格 fallback 机制
- 输出系统重构,TXT 实时刷盘 + 双写机制
- i18n 框架升级至 go-i18n
## 性能优化
- 正则表达式预编译
- 内存优化 map[string]struct{}
- 并发指纹匹配
- SOCKS5 连接复用
- 滑动窗口调度 + 自适应线程池
## 新功能
- Web 管理界面
- 多格式 POC 适配(xray/afrog)
- 增强指纹库(3139条)
- Favicon hash 指纹识别
- 插件选择性编译(Build Tags)
- fscan-lab 靶场环境
- 默认端口扩展(62→133)
## 构建系统
- 添加 no_local tag 支持排除本地插件
- 多版本构建:fscan/fscan-nolocal/fscan-web
- CI 添加 snapshot 模式支持仅测试构建
## Bug 修复
- 修复 120+ 个问题,包括 RDP panic、批量扫描漏报、
JSON 输出格式、Redis 检测、Context 超时等
## 测试增强
- 单元测试覆盖率 74-100%
- 并发安全测试
- 集成测试(Web/端口/服务/SSH/ICMP)
176 lines
5.2 KiB
Go
176 lines
5.2 KiB
Go
package common
|
|
|
|
/*
|
|
network.go - 统一网络操作包装器
|
|
|
|
提供便捷的网络连接API,自动处理发包限制检查、代理和统计。
|
|
*/
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/shadow1ng/fscan/common/proxy"
|
|
)
|
|
|
|
// =============================================================================
|
|
// 全局代理管理器(复用连接,避免重复创建)
|
|
// =============================================================================
|
|
|
|
var (
|
|
globalProxyOnce sync.Once
|
|
globalProxyDialer proxy.Dialer
|
|
globalProxyInitErr error
|
|
)
|
|
|
|
// getGlobalDialer 获取全局拨号器(线程安全,只初始化一次)
|
|
func getGlobalDialer(timeout time.Duration) (proxy.Dialer, error) {
|
|
globalProxyOnce.Do(func() {
|
|
// 创建代理配置
|
|
config := createProxyConfig(timeout)
|
|
|
|
// 创建代理管理器
|
|
manager := proxy.NewProxyManager(config)
|
|
|
|
// 创建拨号器
|
|
globalProxyDialer, globalProxyInitErr = manager.GetDialer()
|
|
})
|
|
|
|
return globalProxyDialer, globalProxyInitErr
|
|
}
|
|
|
|
// =============================================================================
|
|
// 代理配置
|
|
// =============================================================================
|
|
|
|
// parseProxyURL 解析代理URL,提取地址和认证信息
|
|
func parseProxyURL(proxyURL, fallback string) (host, username, password string) {
|
|
parsedURL, err := url.Parse(proxyURL)
|
|
if err != nil {
|
|
return fallback, "", ""
|
|
}
|
|
host = parsedURL.Host
|
|
if parsedURL.User != nil {
|
|
username = parsedURL.User.Username()
|
|
password, _ = parsedURL.User.Password()
|
|
}
|
|
return
|
|
}
|
|
|
|
// createProxyConfig 根据全局设置创建代理配置
|
|
func createProxyConfig(timeout time.Duration) *proxy.ProxyConfig {
|
|
fv := GetFlagVars()
|
|
config := proxy.DefaultProxyConfig()
|
|
config.Timeout = timeout
|
|
config.LocalAddr = fv.Iface // 设置本地网卡IP地址
|
|
|
|
// 优先使用SOCKS5代理
|
|
if fv.Socks5Proxy != "" {
|
|
config.Type = proxy.ProxyTypeSOCKS5
|
|
// 确保有协议前缀以便解析
|
|
socks5URL := fv.Socks5Proxy
|
|
if !strings.HasPrefix(socks5URL, "socks5://") {
|
|
socks5URL = "socks5://" + socks5URL
|
|
}
|
|
config.Address, config.Username, config.Password = parseProxyURL(socks5URL, fv.Socks5Proxy)
|
|
return config
|
|
}
|
|
|
|
// 其次使用HTTP代理
|
|
if fv.HTTPProxy != "" {
|
|
if strings.HasPrefix(fv.HTTPProxy, "https://") {
|
|
config.Type = proxy.ProxyTypeHTTPS
|
|
} else {
|
|
config.Type = proxy.ProxyTypeHTTP
|
|
}
|
|
config.Address, config.Username, config.Password = parseProxyURL(fv.HTTPProxy, fv.HTTPProxy)
|
|
return config
|
|
}
|
|
|
|
// 无代理配置,使用直连
|
|
config.Type = proxy.ProxyTypeNone
|
|
return config
|
|
}
|
|
|
|
// =============================================================================
|
|
// TCP 连接
|
|
// =============================================================================
|
|
|
|
// WrapperTcpWithTimeout TCP连接包装器,带超时
|
|
// 支持通过代理管理器进行SOCKS5和HTTP代理连接,并集成发包控制
|
|
// 使用全局拨号器复用连接,避免重复创建代理握手开销
|
|
//
|
|
//nolint:revive // 保持向后兼容性,避免破坏大量现有代码
|
|
func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
|
|
// 检查发包限制 - 在代理连接前进行控制
|
|
if canSend, reason := CanSendPacket(); !canSend {
|
|
LogError(fmt.Sprintf("TCP连接 %s 受限: %s", address, reason))
|
|
return nil, fmt.Errorf("发包受限: %s", reason)
|
|
}
|
|
|
|
// 获取全局拨号器(复用,避免重复创建)
|
|
dialer, err := getGlobalDialer(timeout)
|
|
if err != nil {
|
|
LogError(fmt.Sprintf("获取代理拨号器失败: %v", err))
|
|
GetGlobalState().IncrementTCPFailedPacketCount()
|
|
return nil, err
|
|
}
|
|
|
|
// 使用代理拨号器连接
|
|
conn, err := dialer.DialContext(context.Background(), network, address)
|
|
|
|
// 统计TCP包数量 - 无论是否使用代理都要计数
|
|
if err != nil {
|
|
GetGlobalState().IncrementTCPFailedPacketCount()
|
|
LogDebug(fmt.Sprintf("连接 %s 失败: %v", address, err))
|
|
return nil, err
|
|
}
|
|
|
|
// 连接成功,统计成功包
|
|
GetGlobalState().IncrementTCPSuccessPacketCount()
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
// SafeTCPDial TCP连接的便捷封装
|
|
// 直接调用WrapperTcpWithTimeout,自动处理发包限制、代理和统计
|
|
func SafeTCPDial(address string, timeout time.Duration) (net.Conn, error) {
|
|
return WrapperTcpWithTimeout("tcp", address, timeout)
|
|
}
|
|
|
|
// =============================================================================
|
|
// HTTP 请求
|
|
// =============================================================================
|
|
|
|
// IsProxyEnabled 检查是否启用了代理(封装proxy包的函数)
|
|
func IsProxyEnabled() bool {
|
|
return proxy.IsProxyEnabled()
|
|
}
|
|
|
|
// SafeHTTPDo 带发包控制的HTTP请求
|
|
func SafeHTTPDo(client *http.Client, req *http.Request) (*http.Response, error) {
|
|
// 检查发包限制
|
|
if canSend, reason := CanSendPacket(); !canSend {
|
|
LogError(fmt.Sprintf("HTTP请求 %s 受限: %s", req.URL.String(), reason))
|
|
return nil, fmt.Errorf("发包受限: %s", reason)
|
|
}
|
|
|
|
// 执行HTTP请求
|
|
resp, err := client.Do(req)
|
|
|
|
// 统计TCP包数量 (HTTP本质上是TCP)
|
|
if err != nil {
|
|
GetGlobalState().IncrementTCPFailedPacketCount()
|
|
} else {
|
|
GetGlobalState().IncrementTCPSuccessPacketCount()
|
|
}
|
|
|
|
return resp, err
|
|
}
|