mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 11:20:41 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72042111c6 | ||
|
|
08a87b3128 | ||
|
|
cc1af631f0 | ||
|
|
5078fbc975 | ||
|
|
354ad2e24b | ||
|
|
942f0fa6c3 | ||
|
|
37b77f9c86 | ||
|
|
a02eed0518 | ||
|
|
019d13c9d4 | ||
|
|
c43982f834 | ||
|
|
d6d323854a | ||
|
|
569d21a8bc | ||
|
|
8ec96bfe6d | ||
|
|
8b558b4f12 | ||
|
|
3e4e2db722 | ||
|
|
ac755a9b4a | ||
|
|
ea17b5b1bc | ||
|
|
bfc0e0fb52 | ||
|
|
40b0eee654 | ||
|
|
8d30ee334c | ||
|
|
73cbe803c4 | ||
|
|
9ed6cc95b6 | ||
|
|
a0f0379c6c | ||
|
|
3c7823355d | ||
|
|
1a714f6a0c | ||
|
|
0046817c2e | ||
|
|
e0080555b4 | ||
|
|
bc2a8f653a | ||
|
|
a92df59dfe | ||
|
|
0a28db7371 | ||
|
|
778ba019d5 | ||
|
|
f43e3e3d0a | ||
|
|
a1588a321f | ||
|
|
eb4fa38fea | ||
|
|
c15aa3488b | ||
|
|
218811c38d | ||
|
|
5a884ca6ad | ||
|
|
5942d3bbcb | ||
|
|
c0a9cfd8f5 | ||
|
|
c16aa04e28 | ||
|
|
856eeccd78 | ||
|
|
13f7997d16 | ||
|
|
adb3ac5b74 | ||
|
|
8de7570268 | ||
|
|
3dde0c6a8e | ||
|
|
d4ed0867c9 | ||
|
|
6605c93dd9 | ||
|
|
6bfa05cb45 | ||
|
|
bce0a718ea | ||
|
|
40c3f1423e | ||
|
|
bfd64f78ed | ||
|
|
7063027acf | ||
|
|
639298b7c8 | ||
|
|
f427f04d35 | ||
|
|
05e383d8bf | ||
|
|
a560531e70 | ||
|
|
77827bef66 | ||
|
|
c266912dcb | ||
|
|
2f2b30763c | ||
|
|
72403ebb32 | ||
|
|
4fa8fdc30e | ||
|
|
bf210dd94d | ||
|
|
cb1079598d | ||
|
|
ec10097e76 | ||
|
|
231563e82b | ||
|
|
5af8682d22 | ||
|
|
a42f074b00 | ||
|
|
1ebcd6dc79 | ||
|
|
150e48ba85 | ||
|
|
76a4331fae | ||
|
|
ca4e1ffad3 | ||
|
|
a0fc7881f2 | ||
|
|
bc28df2ed1 | ||
|
|
7459da2b44 | ||
|
|
9d191889d0 | ||
|
|
2e277a51ed | ||
|
|
73b0b0b822 | ||
|
|
d412786228 | ||
|
|
b2e91d9fc0 | ||
|
|
3436d6ad02 | ||
|
|
a0f323a891 | ||
|
|
46e50a021f | ||
|
|
b32ce5dec8 | ||
|
|
2c2ca6ace3 |
@@ -1,6 +1,6 @@
|
||||
name: 🐛 Bug 报告
|
||||
description: 报告扫描异常、崩溃或错误行为
|
||||
title: "[Bug] 简要描述问题"
|
||||
title: "[Bug] "
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
@@ -8,6 +8,7 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
感谢您提交 Bug 报告!请尽可能详细地填写以下信息,这将帮助我们更快定位和修复问题。
|
||||
**请勿直接提交未修改的模板**,未填写有效信息的 Issue 会被直接关闭。
|
||||
|
||||
- type: dropdown
|
||||
id: module
|
||||
@@ -21,8 +22,10 @@ body:
|
||||
- 弱口令爆破 (Brute Force)
|
||||
- POC/漏洞扫描 (POC Scan)
|
||||
- Web指纹识别 (Web Fingerprint)
|
||||
- 代理功能 (Proxy)
|
||||
- 输出/日志 (Output/Logging)
|
||||
- 命令行参数 (CLI Arguments)
|
||||
- 嵌入式 SDK (Embedded SDK)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
@@ -32,7 +35,7 @@ body:
|
||||
attributes:
|
||||
label: 严重程度
|
||||
options:
|
||||
- 崩溃/无法使用 (Crash)
|
||||
- 崩溃/Panic (Crash)
|
||||
- 功能异常 (Malfunction)
|
||||
- 结果不准确 (Inaccurate)
|
||||
- 性能问题 (Performance)
|
||||
@@ -44,10 +47,7 @@ body:
|
||||
id: description
|
||||
attributes:
|
||||
label: 问题描述
|
||||
description: 清晰描述遇到的问题
|
||||
placeholder: |
|
||||
发生了什么?
|
||||
预期的行为是什么?
|
||||
description: 清晰描述遇到的问题,包括预期行为和实际行为
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -55,10 +55,7 @@ body:
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 提供可以复现问题的命令或步骤
|
||||
placeholder: |
|
||||
1. 执行命令: fscan -h xxx -p xxx
|
||||
2. 观察到...
|
||||
description: 提供可以复现问题的完整命令(请脱敏 IP/密码)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
@@ -75,10 +72,12 @@ body:
|
||||
attributes:
|
||||
label: fscan 版本
|
||||
options:
|
||||
- 2.2.0-rc (dev)
|
||||
- 2.1.3
|
||||
- 2.1.2
|
||||
- 2.1.0
|
||||
- 2.0.1
|
||||
- 2.0.0
|
||||
- 1.8.4
|
||||
- 2.0.x
|
||||
- 1.8.x
|
||||
- 其他/自编译
|
||||
validations:
|
||||
required: true
|
||||
@@ -92,14 +91,13 @@ body:
|
||||
- Windows 10
|
||||
- Windows Server 2022
|
||||
- Windows Server 2019
|
||||
- Ubuntu 24.04
|
||||
- Ubuntu 22.04
|
||||
- Ubuntu 20.04
|
||||
- CentOS 7
|
||||
- CentOS 8/Stream
|
||||
- CentOS 7/8/Stream
|
||||
- Debian 11/12
|
||||
- Kali Linux
|
||||
- macOS 15 (Sequoia)
|
||||
- macOS 14 (Sonoma)
|
||||
- macOS 13 (Ventura)
|
||||
- 其他 Linux
|
||||
- 其他
|
||||
validations:
|
||||
@@ -114,6 +112,7 @@ body:
|
||||
- arm64 (aarch64)
|
||||
- 386 (x86)
|
||||
- arm
|
||||
- mips/mipsle
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -121,4 +120,4 @@ body:
|
||||
id: additional
|
||||
attributes:
|
||||
label: 补充信息
|
||||
description: 其他可能有助于排查问题的信息(如自编译请注明 Go 版本)
|
||||
description: 其他有助于排查的信息(网络环境、是否使用代理、自编译请注明 Go 版本等)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# Issue 模板配置
|
||||
# 禁止空白 issue,强制用户选择模板
|
||||
blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: 📖 使用文档
|
||||
- name: 📖 使用文档 / README
|
||||
url: https://github.com/shadow1ng/fscan/blob/main/README.md
|
||||
about: 提交 Issue 前请先查阅文档
|
||||
- name: 💬 讨论区
|
||||
about: 提交 Issue 前请先查阅文档,确认不是用法问题
|
||||
- name: 💬 讨论区 / Discussions
|
||||
url: https://github.com/shadow1ng/fscan/discussions
|
||||
about: 一般性问题和讨论请使用 Discussions
|
||||
about: 使用疑问、经验分享、一般性讨论请使用 Discussions
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
name: 🎯 误报/漏报
|
||||
description: 报告扫描结果不准确的问题
|
||||
title: "[Accuracy] 服务名 - 误报/漏报描述"
|
||||
title: "[Accuracy] "
|
||||
labels: ["accuracy"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您帮助提高 fscan 的准确性!误报和漏报都是需要优化的问题。
|
||||
感谢您帮助提高 fscan 的准确性!
|
||||
**请提供对比验证结果**(如 nmap、手动连接等),仅描述"感觉不对"的报告无法处理。
|
||||
|
||||
- type: dropdown
|
||||
id: type
|
||||
@@ -34,11 +35,20 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: command
|
||||
attributes:
|
||||
label: 使用的命令
|
||||
description: 执行的 fscan 命令(请脱敏真实 IP/密码)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: fscan-output
|
||||
attributes:
|
||||
label: fscan 输出结果
|
||||
description: 粘贴相关的扫描输出(请脱敏敏感信息如真实IP、密码等)
|
||||
description: 粘贴相关的扫描输出(请脱敏敏感信息)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
@@ -46,11 +56,8 @@ body:
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: 实际情况
|
||||
description: 描述目标的真实状态
|
||||
placeholder: |
|
||||
实际上这个端口是关闭的 / 服务版本是 xxx / 密码不是 xxx...
|
||||
验证方式: 通过 nmap/手动连接/其他工具 确认...
|
||||
label: 实际情况及验证方式
|
||||
description: 描述目标的真实状态,以及你通过什么方式验证的(nmap、手动连接、其他工具等)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -59,15 +66,13 @@ body:
|
||||
attributes:
|
||||
label: 目标操作系统
|
||||
options:
|
||||
- Windows Server 2022
|
||||
- Windows Server 2019
|
||||
- Windows Server 2016
|
||||
- Windows 10/11
|
||||
- Ubuntu
|
||||
- Windows Server
|
||||
- Windows Desktop
|
||||
- Ubuntu/Debian
|
||||
- CentOS/RHEL
|
||||
- Debian
|
||||
- 其他 Linux
|
||||
- 网络设备
|
||||
- 容器/Docker
|
||||
- 未知
|
||||
validations:
|
||||
required: true
|
||||
@@ -77,20 +82,10 @@ body:
|
||||
attributes:
|
||||
label: 网络环境
|
||||
options:
|
||||
- 直连
|
||||
- 通过代理
|
||||
- 直连(同网段)
|
||||
- 跨网段/路由
|
||||
- 通过代理 (SOCKS5/HTTP)
|
||||
- VPN
|
||||
- 跨网段
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: command
|
||||
attributes:
|
||||
label: 使用的命令
|
||||
description: 执行的 fscan 命令
|
||||
placeholder: "fscan -h x.x.x.x -p 1-65535 -pwdf pass.txt"
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -99,10 +94,12 @@ body:
|
||||
attributes:
|
||||
label: fscan 版本
|
||||
options:
|
||||
- 2.2.0-rc (dev)
|
||||
- 2.1.3
|
||||
- 2.1.2
|
||||
- 2.1.0
|
||||
- 2.0.1
|
||||
- 2.0.0
|
||||
- 1.8.4
|
||||
- 2.0.x
|
||||
- 1.8.x
|
||||
- 其他/自编译
|
||||
validations:
|
||||
required: true
|
||||
@@ -110,8 +107,5 @@ body:
|
||||
- type: textarea
|
||||
id: suggestion
|
||||
attributes:
|
||||
label: 改进建议
|
||||
description: 如果您有改进的想法,请分享
|
||||
placeholder: |
|
||||
建议增加 xxx 判断条件...
|
||||
或者调整 xxx 检测逻辑...
|
||||
label: 改进建议(可选)
|
||||
description: 如果您有改进判断逻辑的想法
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
name: ✨ 功能请求
|
||||
description: 提议新功能或改进现有功能
|
||||
title: "[Feature] 一句话描述功能"
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的功能建议!请详细描述您的需求,这将帮助我们评估和实现。
|
||||
感谢您的功能建议!请描述清楚使用场景和期望行为。
|
||||
纯 "+1" 类请求请用 Reaction 表达,不要开新 Issue。
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
@@ -19,6 +20,7 @@ body:
|
||||
- 用户体验 (UX/CLI)
|
||||
- 输出格式 (Output Format)
|
||||
- 配置选项 (Configuration)
|
||||
- 嵌入式 SDK (Embedded SDK)
|
||||
- 集成/API (Integration/API)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
@@ -28,10 +30,7 @@ body:
|
||||
id: problem
|
||||
attributes:
|
||||
label: 解决什么问题?
|
||||
description: 描述您遇到的痛点或使用场景
|
||||
placeholder: |
|
||||
在进行 xxx 操作时,我希望能够...
|
||||
目前的问题是...
|
||||
description: 描述您的实际使用场景和痛点
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -39,20 +38,15 @@ body:
|
||||
id: solution
|
||||
attributes:
|
||||
label: 期望的解决方案
|
||||
description: 描述您希望的功能或行为
|
||||
placeholder: |
|
||||
希望能够通过 -xxx 参数来...
|
||||
或者增加一个新的模块来...
|
||||
description: 描述您希望的功能行为,最好包含预期的命令行用法示例
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 替代方案
|
||||
description: 您考虑过的其他解决方案或变通方法
|
||||
placeholder: |
|
||||
目前我通过 xxx 方式来解决,但是...
|
||||
label: 替代方案(可选)
|
||||
description: 您目前的变通方法,或考虑过的其他方案
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
@@ -60,7 +54,7 @@ body:
|
||||
label: 优先级建议
|
||||
description: 您认为这个功能的重要程度
|
||||
options:
|
||||
- 高 - 核心功能缺失
|
||||
- 高 - 核心场景缺失,严重影响使用
|
||||
- 中 - 明显改善体验
|
||||
- 低 - 锦上添花
|
||||
validations:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: 🔌 新插件/协议支持
|
||||
description: 请求支持新的服务、协议或漏洞检测
|
||||
title: "[Plugin] 协议/服务名称"
|
||||
title: "[Plugin] "
|
||||
labels: ["plugin", "enhancement"]
|
||||
|
||||
body:
|
||||
@@ -8,6 +8,7 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的插件请求!fscan 持续扩展对各种服务和协议的支持。
|
||||
提交前请先确认该服务尚未被支持(查看 README 的支持列表)。
|
||||
|
||||
- type: dropdown
|
||||
id: type
|
||||
@@ -25,7 +26,7 @@ body:
|
||||
id: service
|
||||
attributes:
|
||||
label: 服务/协议名称
|
||||
placeholder: "如: Kafka, ClickHouse, etcd, Consul"
|
||||
description: 例如 ClickHouse、etcd、Consul、Zookeeper
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -33,49 +34,31 @@ body:
|
||||
id: port
|
||||
attributes:
|
||||
label: 默认端口
|
||||
placeholder: "如: 9092, 8123, 2379"
|
||||
description: 例如 9000、2379、8500
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 服务描述
|
||||
description: 简要介绍这个服务/协议
|
||||
placeholder: |
|
||||
这是一个用于 xxx 的服务...
|
||||
在内网环境中常见于...
|
||||
label: 服务描述与检测思路
|
||||
description: 简要介绍服务用途、在内网中的常见程度,以及如何识别(Banner 特征、认证方式等)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: detection
|
||||
attributes:
|
||||
label: 识别方法
|
||||
description: 如何识别/检测这个服务(如有了解)
|
||||
placeholder: |
|
||||
Banner 特征: xxx
|
||||
默认响应: xxx
|
||||
认证方式: xxx
|
||||
|
||||
- type: textarea
|
||||
id: reference
|
||||
attributes:
|
||||
label: 参考资料
|
||||
description: 相关文档、其他工具实现、漏洞详情等
|
||||
placeholder: |
|
||||
- 官方文档: https://...
|
||||
- 其他工具实现: https://...
|
||||
- CVE编号: CVE-xxxx-xxxx
|
||||
label: 参考资料(可选)
|
||||
description: 官方文档链接、协议规范、其他工具实现、CVE 编号等
|
||||
|
||||
- type: dropdown
|
||||
id: prevalence
|
||||
attributes:
|
||||
label: 使用普遍程度
|
||||
description: 这个服务在目标环境中的常见程度
|
||||
label: 内网常见程度
|
||||
options:
|
||||
- 非常常见 (企业环境标配)
|
||||
- 较为常见 (经常遇到)
|
||||
- 非常常见(企业环境标配)
|
||||
- 较为常见(经常遇到)
|
||||
- 偶尔遇到
|
||||
- 较少见但重要
|
||||
- 较少见但高价值
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# 发版流程
|
||||
|
||||
## 预检查
|
||||
|
||||
```bash
|
||||
# 1. 确认 CI 通过
|
||||
gh run list --branch dev --limit 3
|
||||
|
||||
# 2. 全平台 dry-run(手动触发 snapshot 模式)
|
||||
gh workflow run release.yml -f snapshot=true
|
||||
|
||||
# 3. 确认版本号一致
|
||||
grep "version" common/globals.go
|
||||
grep "版本" README.md
|
||||
```
|
||||
|
||||
## 发版
|
||||
|
||||
```bash
|
||||
# 1. 确认 release notes 已就绪
|
||||
cat .github/release-notes/v<VERSION>.md
|
||||
|
||||
# 2. 打 tag(在 dev 分支打 RC,在 main 分支打正式版)
|
||||
git tag v<VERSION>
|
||||
git push origin v<VERSION>
|
||||
|
||||
# CI 自动执行:
|
||||
# - goreleaser 全平台构建 + UPX 压缩
|
||||
# - 创建 GitHub Release(RC 自动标记 pre-release)
|
||||
# - 用 .github/release-notes/ 下的文件覆盖 release body
|
||||
```
|
||||
|
||||
## 版本号规范
|
||||
|
||||
| 场景 | 格式 | 分支 | 示例 |
|
||||
|------|------|------|------|
|
||||
| 正式版 | `vX.Y.Z` | main | `v2.2.0` |
|
||||
| 预发布 | `vX.Y.Z-rc` | dev | `v2.2.0-rc` |
|
||||
| 热修复 | `vX.Y.Z` | main | `v2.2.1` |
|
||||
|
||||
## Release Notes 模板
|
||||
|
||||
放在 `.github/release-notes/<tag>.md`,格式参考 `v2.2.0-rc.md`。
|
||||
|
||||
如果文件不存在,goreleaser 会自动生成基于 commit 的 changelog。
|
||||
|
||||
## 正式版发布(RC → 正式)
|
||||
|
||||
```bash
|
||||
# 1. 合并 dev 到 main
|
||||
git checkout main
|
||||
git merge dev
|
||||
git push
|
||||
|
||||
# 2. 更新版本号去掉 -rc
|
||||
# common/globals.go, README.md, README_EN.md
|
||||
|
||||
# 3. 准备正式版 release notes
|
||||
# .github/release-notes/v2.2.0.md
|
||||
|
||||
# 4. 打 tag
|
||||
git tag v2.2.0
|
||||
git push origin v2.2.0
|
||||
```
|
||||
@@ -24,15 +24,12 @@ runs:
|
||||
steps:
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
cache: true
|
||||
|
||||
- name: 安装 C 编译工具
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y gcc make mingw-w64 gcc-multilib g++-multilib
|
||||
|
||||
- name: 下载依赖
|
||||
shell: bash
|
||||
@@ -59,12 +56,12 @@ runs:
|
||||
|
||||
- name: 上传产物
|
||||
uses: actions/upload-artifact@v4
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
if: always()
|
||||
with:
|
||||
name: build-${{ inputs.mode }}-${{ github.run_id }}
|
||||
path: |
|
||||
dist/
|
||||
dist-lite/
|
||||
path: dist/
|
||||
retention-days: ${{ inputs.retention-days }}
|
||||
|
||||
- name: 生成报告
|
||||
@@ -83,7 +80,6 @@ runs:
|
||||
|
||||
## 构建产物
|
||||
|
||||
### fscan (Go 版本)
|
||||
$(if [ -d "dist" ]; then
|
||||
echo "- 文件数: $(find dist -type f 2>/dev/null | wc -l)"
|
||||
echo "- 大小: $(du -sh dist 2>/dev/null | cut -f1)"
|
||||
@@ -91,13 +87,5 @@ runs:
|
||||
echo "- 无产物"
|
||||
fi)
|
||||
|
||||
### fscan-lite (C 版本)
|
||||
$(if [ -d "dist-lite" ]; then
|
||||
echo "- 文件数: $(find dist-lite -type f 2>/dev/null | wc -l)"
|
||||
echo "- 大小: $(du -sh dist-lite 2>/dev/null | cut -f1)"
|
||||
else
|
||||
echo "- 无产物"
|
||||
fi)
|
||||
|
||||
[查看产物](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
EOF
|
||||
|
||||
@@ -4,8 +4,6 @@ before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go mod download
|
||||
- chmod +x .github/scripts/build-lite.sh
|
||||
- bash .github/scripts/build-lite.sh {{ .Version }}
|
||||
|
||||
builds:
|
||||
# 标准版 - 全部插件(全架构)
|
||||
@@ -16,7 +14,7 @@ builds:
|
||||
- CGO_ENABLED=0
|
||||
goos: [windows, linux, darwin, freebsd, solaris]
|
||||
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
|
||||
goarm: ["6", "7"]
|
||||
goarm: ["5", "6", "7"]
|
||||
gomips: [softfloat]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
@@ -69,7 +67,7 @@ builds:
|
||||
- CGO_ENABLED=0
|
||||
goos: [windows, linux, darwin, freebsd, solaris]
|
||||
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
|
||||
goarm: ["6", "7"]
|
||||
goarm: ["5", "6", "7"]
|
||||
gomips: [softfloat]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
@@ -231,15 +229,13 @@ release:
|
||||
|
||||
| 平台 | 架构 |
|
||||
|------|------|
|
||||
| Linux | x64, x32, arm64, armv6, armv7, mips, mips64, mipsle |
|
||||
| Linux | x64, x32, arm64, armv5, armv6, armv7, mips, mips64, mipsle |
|
||||
| Windows | x64, x32 |
|
||||
| macOS | x64, arm64 |
|
||||
| FreeBSD | x64, x32, arm64, armv6, armv7 |
|
||||
| FreeBSD | x64, x32, arm64, armv5, armv6, armv7 |
|
||||
| Solaris | x64 |
|
||||
footer: |
|
||||
**完整更新日志**: https://github.com/{{ .Env.GITHUB_OWNER }}/{{ .Env.GITHUB_REPO }}/compare/{{ .PreviousTag }}...{{ .Tag }}
|
||||
extra_files:
|
||||
- glob: ./dist-lite/*
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-dev-{{ .ShortCommit }}"
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# fscan v2.2.0-rc
|
||||
|
||||
> ⚠️ **这是预发布版本 (Release Candidate)**,可能存在未发现的问题。
|
||||
> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://github.com/shadow1ng/fscan/issues/new/choose) 反馈,帮助我们尽快稳定正式版。
|
||||
> 生产环境建议继续使用 [v2.1.3](https://github.com/shadow1ng/fscan/releases/tag/v2.1.3)。
|
||||
|
||||
---
|
||||
|
||||
## 与 v2.1.3 的主要变更
|
||||
|
||||
### 🏗️ 架构升级:嵌入式 Scanner SDK
|
||||
|
||||
新增 `pkg/fscan` 包,fscan 从纯 CLI 工具进化为**可嵌入的扫描引擎**。
|
||||
|
||||
- 支持从其他 Go 程序直接调用 fscan 扫描能力
|
||||
- 每个 Scanner 实例拥有独立的 config/state/session
|
||||
- 支持多实例并发扫描,日志和结果完全隔离
|
||||
- 全局状态已完整迁移到 session 实例
|
||||
|
||||
### 🚀 大规模扫描支持
|
||||
|
||||
- **流式 Host Iterator** — /8 级别 CIDR 不再一次性展开到内存
|
||||
- **移除 MaxHosts 硬限制** — 大网段不再被静默截断
|
||||
- **ARM 原子计数器对齐** — 修复 32 位 ARM 平台 panic
|
||||
|
||||
### 🔌 新增协议插件(+9)
|
||||
|
||||
| 插件 | 用途 |
|
||||
|------|------|
|
||||
| IMAP / POP3 | 邮件服务器检测 |
|
||||
| JDWP | Java Debug 端口检测 |
|
||||
| NFS / RMI | 文件共享 / Java 远程调用 |
|
||||
| IPMI | 服务器带外管理 |
|
||||
| SNMP (UDP) | 网络设备管理 |
|
||||
| DNS / BACnet / Modbus | DNS 服务 / 工控协议 |
|
||||
|
||||
全部为**原生协议实现**,零外部依赖。
|
||||
|
||||
### ⚡ 性能与体积优化
|
||||
|
||||
- Kafka/MongoDB/Cassandra 从重型客户端库改为 raw TCP 实现,**二进制体积减小**
|
||||
- 热路径零分配、自适应连接池 CAS 无锁化
|
||||
- Scanner 客户端指纹收敛(降低被检测概率)
|
||||
|
||||
### 🌐 网络能力增强
|
||||
|
||||
- **SOCKS5 代理认证**支持(用户名/密码)
|
||||
- **国密 TLS**(SM2/SM3/SM4)网站扫描
|
||||
- 支持 `-add-password` 追加多个自定义密码
|
||||
|
||||
### 🔧 本地后渗透插件
|
||||
|
||||
- 新增 sshkey、4 种持久化插件(crontask/systemd/winregistry/winschtask)
|
||||
- minidump 降级链凭据提取 + 杀软前置检测
|
||||
- cleaner 清理所有持久化痕迹
|
||||
- systeminfo 整合合并(fileinfo/dcinfo/avdetect/envinfo)
|
||||
|
||||
### 📦 工程改进
|
||||
|
||||
- 目录重命名:`WebScan/` → `webscan/`、`mylib/` → `libs/`
|
||||
- i18n 覆盖扩展(中/英双语)
|
||||
- 全量 lint/errcheck 修复
|
||||
- 补充大量单元测试
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 已知问题 / 注意事项
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| SDK 并发限速共享 | `CanSendPacket()` 全局限速器在多 Scanner 实例间共享(按进程限速,非按实例) |
|
||||
| CEL 回连验证走全局 | DNSLog 反连检测的 HTTP 请求计数走全局 state(影响可忽略) |
|
||||
| 本地插件仅限授权场景 | 持久化/minidump/shell 类插件仅用于授权渗透测试 |
|
||||
| WebUI 为实验性功能 | `fscan-web` 版本的 Web 界面尚未完善 |
|
||||
| `common` 包未完全拆分 | 内部架构的 common 包职责仍较重,后续版本持续优化 |
|
||||
|
||||
---
|
||||
|
||||
## 反馈与贡献
|
||||
|
||||
这是一个 RC 版本,我们非常需要你的反馈:
|
||||
|
||||
- 🐛 发现 Bug → [提交 Bug 报告](https://github.com/shadow1ng/fscan/issues/new?template=bug_report.yml)
|
||||
- 🎯 结果不准 → [提交误报/漏报](https://github.com/shadow1ng/fscan/issues/new?template=false_positive.yml)
|
||||
- ✨ 功能建议 → [提交功能请求](https://github.com/shadow1ng/fscan/issues/new?template=feature_request.yml)
|
||||
- 💬 使用疑问 → [Discussions](https://github.com/shadow1ng/fscan/discussions)
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
| 版本 | 说明 |
|
||||
|------|------|
|
||||
| **fscan** | 标准版,包含全部插件(推荐) |
|
||||
| **fscan-nolocal** | 精简版,不含本地后渗透模块(体积更小) |
|
||||
| **fscan-web** | WebUI版,带 Web 管理界面(主流平台) |
|
||||
|
||||
## 平台支持
|
||||
|
||||
| 平台 | 架构 |
|
||||
|------|------|
|
||||
| Linux | x64, x32, arm64, armv5/6/7, mips, mips64, mipsle |
|
||||
| Windows | x64, x32 |
|
||||
| macOS | x64 (Intel), arm64 (Apple Silicon) |
|
||||
| FreeBSD | x64, x32, arm64, armv5/6/7 |
|
||||
| Solaris | X64n |
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 构建 fscan-lite 并准备发布产物
|
||||
|
||||
set -e
|
||||
|
||||
VERSION="${1:-dev}"
|
||||
LITE_DIR="fscan-lite"
|
||||
OUTPUT_DIR="dist-lite"
|
||||
|
||||
echo "==> 构建 fscan-lite (版本: $VERSION)"
|
||||
|
||||
# 清理旧产物
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# 进入 lite 目录
|
||||
cd "$LITE_DIR"
|
||||
|
||||
# 源文件
|
||||
SOURCES="src/main.c src/scanner.c src/platform.c"
|
||||
INCLUDE="-Iinclude"
|
||||
CFLAGS_BASE="-std=c89 -Wall -O2"
|
||||
|
||||
# 构建 Linux 版本
|
||||
echo "==> 构建 Linux 版本..."
|
||||
|
||||
# Linux x64
|
||||
echo " - Linux x64"
|
||||
mkdir -p bin
|
||||
gcc $CFLAGS_BASE $INCLUDE -o bin/fscan-lite $SOURCES -lpthread
|
||||
cp bin/fscan-lite "../$OUTPUT_DIR/fscan-lite_${VERSION}_linux_x64"
|
||||
rm -rf bin
|
||||
|
||||
# Linux x32
|
||||
echo " - Linux x32"
|
||||
mkdir -p bin
|
||||
gcc $CFLAGS_BASE -m32 $INCLUDE -o bin/fscan-lite $SOURCES -lpthread 2>/dev/null || echo " (跳过: 缺少 32-bit 支持)"
|
||||
if [ -f bin/fscan-lite ]; then
|
||||
cp bin/fscan-lite "../$OUTPUT_DIR/fscan-lite_${VERSION}_linux_x32"
|
||||
fi
|
||||
rm -rf bin
|
||||
|
||||
# 构建 Windows 版本
|
||||
echo "==> 构建 Windows 版本..."
|
||||
|
||||
# Windows x64
|
||||
echo " - Windows x64"
|
||||
mkdir -p bin
|
||||
x86_64-w64-mingw32-gcc $CFLAGS_BASE $INCLUDE -o bin/fscan-lite.exe $SOURCES -lws2_32 -static
|
||||
if [ -f bin/fscan-lite.exe ]; then
|
||||
cp bin/fscan-lite.exe "../$OUTPUT_DIR/fscan-lite_${VERSION}_windows_x64.exe"
|
||||
echo " ✓ 编译成功"
|
||||
else
|
||||
echo " ✗ 编译失败"
|
||||
fi
|
||||
rm -rf bin
|
||||
|
||||
# Windows x32
|
||||
echo " - Windows x32"
|
||||
mkdir -p bin
|
||||
i686-w64-mingw32-gcc $CFLAGS_BASE $INCLUDE -o bin/fscan-lite.exe $SOURCES -lws2_32 -static
|
||||
if [ -f bin/fscan-lite.exe ]; then
|
||||
cp bin/fscan-lite.exe "../$OUTPUT_DIR/fscan-lite_${VERSION}_windows_x32.exe"
|
||||
echo " ✓ 编译成功"
|
||||
else
|
||||
echo " ✗ 编译失败"
|
||||
fi
|
||||
rm -rf bin
|
||||
|
||||
cd ..
|
||||
|
||||
# 统计产物
|
||||
echo ""
|
||||
echo "==> 构建完成!"
|
||||
echo "产物列表:"
|
||||
if [ -d "$OUTPUT_DIR" ]; then
|
||||
ls -lh "$OUTPUT_DIR" 2>/dev/null || echo " (无产物)"
|
||||
echo ""
|
||||
FILECOUNT=$(ls "$OUTPUT_DIR" 2>/dev/null | wc -l)
|
||||
echo "总计: $FILECOUNT 个文件"
|
||||
fi
|
||||
@@ -14,18 +14,17 @@ on:
|
||||
description: '创建草稿发布'
|
||||
type: boolean
|
||||
default: false
|
||||
prerelease:
|
||||
description: '标记为预发布'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
@@ -33,10 +32,41 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 准备 Release Notes
|
||||
if: ${{ !inputs.snapshot }}
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
NOTES_FILE=".github/release-notes/${TAG}.md"
|
||||
|
||||
if [ -f "$NOTES_FILE" ]; then
|
||||
echo "📝 找到版本 Release Notes: $NOTES_FILE"
|
||||
cp "$NOTES_FILE" /tmp/release-notes.md
|
||||
else
|
||||
echo "⚠️ 未找到 $NOTES_FILE,使用自动生成的 changelog"
|
||||
echo "" > /tmp/release-notes.md
|
||||
fi
|
||||
|
||||
echo "RELEASE_NOTES_FILE=/tmp/release-notes.md" >> $GITHUB_ENV
|
||||
|
||||
- name: 构建和发布
|
||||
uses: ./.github/actions/build-release
|
||||
with:
|
||||
mode: ${{ inputs.snapshot && 'snapshot' || 'release' }}
|
||||
go-version: '1.20'
|
||||
retention-days: '90'
|
||||
release-args: ${{ inputs.draft && '--draft' || '' }} ${{ inputs.prerelease && '--prerelease' || '' }}
|
||||
release-args: ${{ inputs.draft && '--draft' || '' }}
|
||||
|
||||
- name: 更新 Release Notes
|
||||
if: ${{ !inputs.snapshot && env.RELEASE_NOTES_FILE != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
NOTES_FILE="${RELEASE_NOTES_FILE}"
|
||||
|
||||
if [ -s "$NOTES_FILE" ]; then
|
||||
echo "📝 更新 Release Notes..."
|
||||
# 用版本特定的 notes 替换 goreleaser 生成的 body
|
||||
gh release edit "$TAG" --notes-file "$NOTES_FILE"
|
||||
echo "✅ Release Notes 已更新"
|
||||
fi
|
||||
|
||||
@@ -3,6 +3,7 @@ name: 测试构建
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev-*
|
||||
- develop
|
||||
@@ -37,6 +38,9 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: 代码检查
|
||||
@@ -55,32 +59,22 @@ jobs:
|
||||
|
||||
- name: 运行 golangci-lint
|
||||
run: |
|
||||
# 下载 golangci-lint v2
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest
|
||||
# 安装 golangci-lint v2 (需 Go 1.22+,仅用于静态分析,不影响二进制兼容性)
|
||||
go install github.com/golangci/golangci-lint/v2/cmd/[email protected]
|
||||
|
||||
# 运行检查并灵活处理结果
|
||||
set +e
|
||||
golangci-lint run --timeout=5m > lint_output.txt 2>&1
|
||||
LINT_EXIT_CODE=$?
|
||||
# 运行检查
|
||||
golangci-lint run --timeout=5m > lint_output.txt 2>&1 || true
|
||||
cat lint_output.txt
|
||||
set -e
|
||||
|
||||
# 只关注真正的 bug,忽略代码质量建议
|
||||
# 过滤规则:
|
||||
# - gocognit/gocyclo: 复杂度警告(阈值已在配置中设置)
|
||||
# - QF/S/ST: staticcheck 的代码质量改进建议(非bug)
|
||||
if [ $LINT_EXIT_CODE -ne 0 ]; then
|
||||
CRITICAL_ISSUES=$(grep -E "\.go:[0-9]+:[0-9]+:" lint_output.txt | grep -v "gocognit" | grep -v "gocyclo" | grep -v "QF[0-9]" | grep -v " S[0-9]" | grep -v "ST[0-9]" || true)
|
||||
if [ -n "$CRITICAL_ISSUES" ]; then
|
||||
echo "❌ Linting failed with critical issues:"
|
||||
echo "$CRITICAL_ISSUES" | head -20
|
||||
exit 1
|
||||
else
|
||||
echo "⚠️ Only quality suggestions - PASSING"
|
||||
exit 0
|
||||
fi
|
||||
# 过滤非关键问题(复杂度建议和 staticcheck 风格建议不阻塞 CI)
|
||||
CRITICAL=$(grep -E "\.go:[0-9]+:[0-9]+:" lint_output.txt \
|
||||
| grep -vE "gocognit|gocyclo|QF[0-9]+| S[0-9]+|ST[0-9]+" || true)
|
||||
if [ -n "$CRITICAL" ]; then
|
||||
echo "❌ Linting failed with critical issues:"
|
||||
echo "$CRITICAL" | head -20
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ No lint issues found"
|
||||
echo "✅ Lint 检查通过"
|
||||
|
||||
- name: 检查代码复杂度(质量门禁)
|
||||
run: |
|
||||
@@ -131,7 +125,8 @@ jobs:
|
||||
- name: 运行测试
|
||||
run: |
|
||||
# 排除第三方grdp库测试(存在环境依赖问题)
|
||||
go test -vet=off -race -coverprofile=coverage.out -covermode=atomic $(go list ./... | grep -v '/mylib/grdp/')
|
||||
PKGS=$(go list ./... | grep -v '/libs/grdp/')
|
||||
go test -vet=off -race -coverprofile=coverage.out -covermode=atomic $PKGS
|
||||
|
||||
- name: 上传覆盖率
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -152,18 +147,14 @@ jobs:
|
||||
echo "### 🚦 覆盖率质量门禁" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 提取总体覆盖率
|
||||
TOTAL_COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
|
||||
# 提取总体覆盖率(awk 整数比较,不需要 bc)
|
||||
TOTAL_COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print int($3)}')
|
||||
|
||||
echo "总体覆盖率: ${TOTAL_COVERAGE}%" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 检查核心模块覆盖率(core, common/parsers必须>50%)
|
||||
CORE_COVERAGE=$(go tool cover -func=coverage.out | grep "^github.com/shadow1ng/fscan/core/" | grep -v "_test.go" | awk '{sum+=$3; count++} END {if(count>0) print sum/count; else print 0}')
|
||||
PARSERS_COVERAGE=$(go tool cover -func=coverage.out | grep "^github.com/shadow1ng/fscan/common/parsers/" | grep -v "_test.go" | awk '{sum+=$3; count++} END {if(count>0) print sum/count; else print 0}')
|
||||
|
||||
# 警告阈值:总体<40%, 核心模块<50%
|
||||
if (( $(echo "$TOTAL_COVERAGE < 40" | bc -l) )); then
|
||||
# 警告阈值:总体<40%
|
||||
if [ "$TOTAL_COVERAGE" -lt 40 ]; then
|
||||
echo "⚠️ **警告**: 总体覆盖率 ${TOTAL_COVERAGE}% < 40%,建议补充测试" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
result.txt
|
||||
result.json
|
||||
main
|
||||
/main
|
||||
.idea
|
||||
fscan.exe
|
||||
fscan
|
||||
/fscan
|
||||
fscanapi.csv
|
||||
|
||||
# IDE files / IDE 文件
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ linters:
|
||||
- vendor
|
||||
- testdocker
|
||||
- image
|
||||
- mylib/grdp
|
||||
- libs/grdp
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 50
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
内网综合扫描工具,一键自动化漏扫。
|
||||
|
||||
**版本**: 2.1.2
|
||||
**版本**: 2.2.0-rc
|
||||
|
||||
## 功能特性
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
- **并发控制** - 端口扫描线程、服务扫描线程独立配置
|
||||
|
||||
### 扩展功能
|
||||
- **SDK嵌入** - `pkg/fscan`提供Go SDK,可嵌入Agent或安全平台,支持任务控制(Pause/Resume)、实时进度回调、TaskID追溯
|
||||
- **Web管理界面** - 可视化扫描任务管理(条件编译 -tags web)
|
||||
- **Lab靶场环境** - 内置Docker靶场用于测试学习
|
||||
- **插件化架构** - 服务插件/Web插件/本地插件分离,易于扩展
|
||||
@@ -211,9 +212,13 @@ yay -S fscan-git
|
||||
`fscan.exe -h 192.168.x.x -m ssh -user root -pwd password`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -m ssh -user root -pwda pass1 pass2 pass3` (追加多个密码)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -socks5 socks5://user:[email protected]:1080` (SOCKS5认证代理)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
|
||||

|
||||
|
||||
@@ -234,6 +239,11 @@ yay -S fscan-git
|
||||
- **后两周** - Bug修复与代码整合
|
||||
- **欢迎PR** - 期待您的贡献!
|
||||
|
||||
### SDK & Agent 集成
|
||||
- 扩展SDK能力,完善端侧Agent嵌入支持
|
||||
- 断点续扫、带宽级限速、内存水位控制
|
||||
- 更多Agent场景的集成示例
|
||||
|
||||
### 插件生态
|
||||
- 持续扩展服务插件覆盖范围
|
||||
- 为每个服务插件开发更多漏洞检测和利用能力
|
||||
|
||||
+5
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
Comprehensive intranet scanning tool for automated vulnerability assessment.
|
||||
|
||||
**Version**: 2.1.2
|
||||
**Version**: 2.2.0-rc
|
||||
|
||||
## Features
|
||||
|
||||
@@ -211,9 +211,13 @@ yay -S fscan-git
|
||||
`fscan.exe -h 192.168.x.x -m ssh -user root -pwd password`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -m ssh -user root -pwda pass1 pass2 pass3` (add multiple passwords)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -socks5 socks5://user:[email protected]:1080` (SOCKS5 proxy with authentication)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
|
||||

|
||||
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
---
|
||||
name: fscan-agent
|
||||
description: 使用 fscan 进行网络扫描和安全评估。当用户要求扫描网段、探测主机存活、发现开放端口、识别服务、检测漏洞或弱口令时使用。支持 NDJSON 结构化输出,适合 AI agent 管道消费。
|
||||
argument-hint: <目标IP/网段> [附加参数]
|
||||
allowed-tools: Bash, Read, Agent
|
||||
---
|
||||
|
||||
# Fscan AI Agent Skill
|
||||
|
||||
## 工具概述
|
||||
|
||||
Fscan 是一款内网综合扫描工具,功能包括:
|
||||
- 主机存活探测(ICMP / TCP)
|
||||
- 端口扫描与服务识别
|
||||
- 漏洞检测(MS17-010、Redis 未授权等)
|
||||
- 弱口令爆破(SSH、SMB、MySQL、MSSQL、FTP、RDP 等)
|
||||
- Web 指纹识别与 POC 扫描
|
||||
- NetBIOS / SMB 信息收集
|
||||
- 本地信息收集(杀软检测、系统信息等)
|
||||
|
||||
二进制路径:当前项目编译产物 `fscan_cli`,或系统 PATH 中的 `fscan`。
|
||||
|
||||
## 调用格式
|
||||
|
||||
```bash
|
||||
# AI agent 标准用法:NDJSON 输出,无人类日志干扰
|
||||
fscan -h <目标> -silent [其他参数]
|
||||
|
||||
# 解析输出
|
||||
fscan -h 192.168.1.0/24 -silent | jq 'select(.type=="VULN")'
|
||||
```
|
||||
|
||||
## 核心参数
|
||||
|
||||
### 目标指定
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `-h` | 目标主机(IP / CIDR / 范围) | `-h 192.168.1.0/24` `-h 10.0.0.1-10.0.0.100` |
|
||||
| `-hf` | 从文件读取目标 | `-hf targets.txt` |
|
||||
| `-p` | 指定端口(逗号/范围) | `-p 22,80,443,445,3306` `-p 1-1000` |
|
||||
| `-ep` | 排除端口 | `-ep 25,110` |
|
||||
| `-eh` | 排除主机 | `-eh 192.168.1.1` |
|
||||
| `-u` | 指定 URL(Web 扫描) | `-u https://example.com` |
|
||||
| `-uf` | URL 文件 | `-uf urls.txt` |
|
||||
|
||||
### 扫描控制
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `-m` | 扫描模式 | `all` |
|
||||
| `-t` | 端口扫描线程数 | `600` |
|
||||
| `-mt` | 模块线程数 | `20` |
|
||||
| `-time` | 连接超时(秒) | `3` |
|
||||
| `-gt` | 全局超时(秒) | `180` |
|
||||
| `-np` | 跳过存活检测 | `false` |
|
||||
| `-ntp` | 禁用 TCP 补充探测 | `false` |
|
||||
| `-ao` | 仅存活检测 | `false` |
|
||||
| `-nobr` | 禁用暴力破解 | `false` |
|
||||
| `-full` | 全量 POC 扫描 | `false` |
|
||||
| `-max-retries` | 最大重试次数 | `1` |
|
||||
|
||||
### 认证
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `-user` | 用户名 |
|
||||
| `-pwd` | 密码 |
|
||||
| `-usera` | 追加用户名 |
|
||||
| `-pwda` | 追加密码,支持逗号或空格分隔多个值 |
|
||||
| `-userf` | 用户名字典文件 |
|
||||
| `-pwdf` | 密码字典文件 |
|
||||
| `-domain` | 域名(SMB/WMI) |
|
||||
| `-sshkey` | SSH 私钥文件 |
|
||||
| `-hash` / `-hashf` | NTLM Hash / Hash 文件 |
|
||||
|
||||
### 代理
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `-socks5` | SOCKS5 代理 (`127.0.0.1:1080`) |
|
||||
| `-proxy` | HTTP 代理 (`http://127.0.0.1:8080`) |
|
||||
| `-iface` | 指定本地网卡 IP(VPN 场景) |
|
||||
|
||||
### 输出
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `-silent` | 静默模式:stdout 仅输出 NDJSON |
|
||||
| `-o` | 输出文件路径(默认 `result.txt`) |
|
||||
| `-f` | 输出格式:`txt` / `json` / `csv` |
|
||||
| `-no` | 禁用文件保存 |
|
||||
| `-debug` | 调试模式:日志写入 `fscan_debug.log` |
|
||||
| `-log` | 日志级别(`debug` / `info` / `base` / `error`) |
|
||||
|
||||
### 扫描模式 `-m` 的取值
|
||||
|
||||
| 值 | 说明 |
|
||||
|------|------|
|
||||
| `all` | 全部扫描(默认) |
|
||||
| `icmp` | 仅 ICMP 存活检测 |
|
||||
| 插件名 | 仅运行指定插件(如 `ssh`、`smb`、`ms17010`、`webtitle`) |
|
||||
|
||||
## 服务插件列表
|
||||
|
||||
| 插件 | 默认端口 | 功能 |
|
||||
|------|----------|------|
|
||||
| `ftp` | 21 | FTP 弱口令 |
|
||||
| `ssh` | 22 | SSH 弱口令 |
|
||||
| `telnet` | 23 | Telnet 弱口令 |
|
||||
| `smtp` | 25 | SMTP 弱口令 |
|
||||
| `findnet` | 135 | RPC 网络信息发现(NetInfo) |
|
||||
| `netbios` | 139 | NetBIOS 信息收集 |
|
||||
| `smb` | 445 | SMB 弱口令 |
|
||||
| `ms17010` | 445 | MS17-010 永恒之蓝检测 |
|
||||
| `ldap` | 389 | LDAP 弱口令 |
|
||||
| `mssql` | 1433 | MSSQL 弱口令 |
|
||||
| `oracle` | 1521 | Oracle 弱口令 |
|
||||
| `mysql` | 3306 | MySQL 弱口令 |
|
||||
| `rdp` | 3389 | RDP 弱口令 + 系统信息 |
|
||||
| `postgresql` | 5432 | PostgreSQL 弱口令 |
|
||||
| `vnc` | 5900 | VNC 弱口令 |
|
||||
| `redis` | 6379 | Redis 未授权 + 弱口令 |
|
||||
| `elasticsearch` | 9200 | ES 未授权 |
|
||||
| `mongodb` | 27017 | MongoDB 未授权 + 弱口令 |
|
||||
| `memcached` | 11211 | Memcached 未授权 |
|
||||
| `kafka` | 9092 | Kafka 未授权 |
|
||||
| `activemq` | 61616 | ActiveMQ 弱口令 |
|
||||
| `rabbitmq` | 5672 | RabbitMQ 弱口令 |
|
||||
| `cassandra` | 9042 | Cassandra 弱口令 |
|
||||
| `neo4j` | 7687 | Neo4j 弱口令 |
|
||||
| `rsync` | 873 | Rsync 未授权 |
|
||||
| `webtitle` | 80/443 | Web 标题 + 指纹识别 |
|
||||
| `webpoc` | 80/443 | Web 漏洞 POC |
|
||||
|
||||
## 本地插件(`-local`)
|
||||
|
||||
```bash
|
||||
fscan -local avdetect # 杀软检测
|
||||
fscan -local systeminfo # 系统信息收集
|
||||
fscan -local envinfo # 环境变量信息
|
||||
fscan -local dcinfo # 域控信息
|
||||
fscan -local fileinfo # 敏感文件搜索
|
||||
```
|
||||
|
||||
## NDJSON 输出 Schema(`-silent` 模式)
|
||||
|
||||
每行一个 JSON 对象,所有字段定义:
|
||||
|
||||
| 字段 | 类型 | 出现条件 | 说明 |
|
||||
|------|------|----------|------|
|
||||
| `type` | string | 必有 | `HOST` / `PORT` / `SERVICE` / `VULN` |
|
||||
| `target` | string | 必有 | 原始目标 `host` 或 `host:port` |
|
||||
| `status` | string | 必有 | 状态描述 |
|
||||
| `host` | string | 必有 | IP 地址 |
|
||||
| `port` | int | PORT/SERVICE/VULN | 端口号 |
|
||||
| `service` | string | SERVICE/VULN | 服务名(ssh, smb, http 等) |
|
||||
| `protocol` | string | HOST/SERVICE | 协议(ICMP, TCP, http, https) |
|
||||
| `banner` | string | SERVICE | 服务 Banner |
|
||||
| `title` | string | SERVICE (web) | 网页标题 |
|
||||
| `url` | string | SERVICE (web) | 完整 URL |
|
||||
| `vulnerability` | string | VULN | 漏洞名称 |
|
||||
| `username` | string | VULN (弱口令) | 用户名 |
|
||||
| `password` | string | VULN (弱口令) | 密码 |
|
||||
| `plugin` | string | SERVICE/VULN | 产生结果的插件名 |
|
||||
| `version` | string | SERVICE | 服务版本号 |
|
||||
| `os` | string | SERVICE | 操作系统信息 |
|
||||
|
||||
### 输出示例
|
||||
|
||||
```jsonl
|
||||
{"type":"HOST","target":"192.168.1.5","status":"alive","host":"192.168.1.5","protocol":"ICMP"}
|
||||
{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":22}
|
||||
{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":445}
|
||||
{"type":"SERVICE","target":"192.168.1.5:22","status":"identified","host":"192.168.1.5","port":22,"service":"ssh","banner":"SSH-2.0-OpenSSH_8.9p1","version":"8.9p1","plugin":"portscan"}
|
||||
{"type":"SERVICE","target":"192.168.1.5:80","status":"web","host":"192.168.1.5","port":80,"service":"http","protocol":"http","url":"http://192.168.1.5:80","title":"Welcome","plugin":"webtitle"}
|
||||
{"type":"VULN","target":"192.168.1.5:445","status":"MS17-010 (Windows Server 2012 R2 Standard 9600)","host":"192.168.1.5","port":445,"vulnerability":"MS17-010","service":"smb","plugin":"ms17010"}
|
||||
{"type":"VULN","target":"192.168.1.5:22","status":"weak_credential: root:123456","host":"192.168.1.5","port":22,"service":"ssh","username":"root","password":"123456","plugin":"ssh"}
|
||||
{"type":"VULN","target":"192.168.1.5:6379","status":"Redis unauthorized","host":"192.168.1.5","port":6379,"vulnerability":"Redis unauthorized access","service":"redis","plugin":"redis"}
|
||||
```
|
||||
|
||||
### 结果产出顺序
|
||||
|
||||
1. `HOST` — 存活探测阶段
|
||||
2. `PORT` — 端口扫描阶段(与 SERVICE 可能交错)
|
||||
3. `SERVICE` — 服务识别阶段
|
||||
4. `VULN` — 漏洞/弱口令检测阶段
|
||||
|
||||
同一 `host:port` 可产生多条结果(PORT + SERVICE + VULN)。
|
||||
|
||||
## 常用场景参数组合
|
||||
|
||||
### 全网段快速扫描
|
||||
|
||||
```bash
|
||||
fscan -h 192.168.1.0/24 -silent
|
||||
```
|
||||
|
||||
### 跳过存活检测直接扫端口(目标明确时)
|
||||
|
||||
```bash
|
||||
fscan -h 192.168.1.0/24 -silent -np
|
||||
```
|
||||
|
||||
### 指定端口精确扫描
|
||||
|
||||
```bash
|
||||
fscan -h 10.0.0.0/24 -silent -p 22,80,443,445,3389,3306,6379
|
||||
```
|
||||
|
||||
### 仅存活探测
|
||||
|
||||
```bash
|
||||
fscan -h 172.16.0.0/16 -silent -m icmp
|
||||
```
|
||||
|
||||
### 低速隐蔽扫描
|
||||
|
||||
```bash
|
||||
fscan -h 192.168.1.0/24 -silent -t 30 -time 5
|
||||
```
|
||||
|
||||
### 通过 SOCKS5 代理扫描内网
|
||||
|
||||
```bash
|
||||
fscan -h 10.0.0.0/24 -silent -socks5 127.0.0.1:1080
|
||||
```
|
||||
|
||||
### 仅做弱口令检测
|
||||
|
||||
```bash
|
||||
fscan -h 192.168.1.10 -silent -m ssh -user root -pwdf /path/to/passwords.txt
|
||||
```
|
||||
|
||||
### Web 目标扫描
|
||||
|
||||
```bash
|
||||
fscan -u https://target.com -silent -full
|
||||
```
|
||||
|
||||
### 多目标文件批量扫描
|
||||
|
||||
```bash
|
||||
fscan -hf targets.txt -silent -o results.json -f json
|
||||
```
|
||||
|
||||
### 带调试日志的排障扫描
|
||||
|
||||
```bash
|
||||
# NDJSON 到 stdout,debug 日志到文件,互不干扰
|
||||
fscan -h 192.168.1.0/24 -silent -debug
|
||||
# 事后查看:cat fscan_debug.log
|
||||
```
|
||||
|
||||
## AI Agent 结果处理
|
||||
|
||||
### Python 管道消费
|
||||
|
||||
```python
|
||||
import json, subprocess
|
||||
|
||||
proc = subprocess.Popen(
|
||||
["fscan", "-h", "192.168.1.0/24", "-silent"],
|
||||
stdout=subprocess.PIPE, text=True
|
||||
)
|
||||
|
||||
hosts, services, vulns = [], [], []
|
||||
for line in proc.stdout:
|
||||
r = json.loads(line)
|
||||
if r["type"] == "HOST":
|
||||
hosts.append(r["host"])
|
||||
elif r["type"] == "SERVICE":
|
||||
services.append(r)
|
||||
elif r["type"] == "VULN":
|
||||
vulns.append(r)
|
||||
|
||||
proc.wait()
|
||||
```
|
||||
|
||||
### jq 过滤
|
||||
|
||||
```bash
|
||||
# 提取所有弱口令
|
||||
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.username != null) | "\(.host):\(.port) \(.service) \(.username):\(.password)"'
|
||||
|
||||
# 提取所有漏洞
|
||||
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="VULN") | "\(.host):\(.port) \(.vulnerability)"'
|
||||
|
||||
# 提取 Web 服务
|
||||
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.url != null) | "\(.url) \(.title)"'
|
||||
|
||||
# 统计开放端口
|
||||
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="PORT") | .port' | sort -n | uniq -c | sort -rn
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `-silent` 抑制所有人类可读日志,stdout 仅输出 NDJSON
|
||||
- 空字段不出现在 JSON 中(`omitempty`)
|
||||
- 进程退出码 `0` 正常完成,非 `0` 表示参数错误或初始化失败
|
||||
- `-silent` 和 `-debug` 可同时使用,互不干扰
|
||||
- SOCKS5 代理下 fscan 信任协议层连接结果,不做额外深度验证
|
||||
- 扫描大网段时线程数会自动调整,资源耗尽时自适应降级
|
||||
- 默认超时 3 秒,防火墙 drop 的端口会静默超时,不计入失败率
|
||||
@@ -36,7 +36,7 @@ var (
|
||||
"6000,6379,6380,6443,6666,6667," +
|
||||
// Web/中间件 (7001-9999)
|
||||
// 注意:9100 已移除,该端口为打印机 RAW 端口,发送数据会触发打印
|
||||
"7001,7002,7474,7687,8000,8005,8008,8009,8080,8081,8086,8088,8089,8090,8161,8180,8443,8500,8834,8848,8880,8888,9000,9001,9042,9080,9090,9092,9093,9160,9200,9300,9418,9443,9999," +
|
||||
"7001,7002,7474,7687,8000,8005,8008,8009,8080,8081,8086,8088,8089,8090,8161,8180,8443,8500,8834,8848,8880,8883,8888,9000,9001,9042,9080,9090,9092,9093,9160,9200,9300,9418,9443,9999," +
|
||||
// 管理/监控 (10000-11211)
|
||||
"10000,10051,10250,10255,11211," +
|
||||
// 消息队列/集群 (15672-27018)
|
||||
@@ -48,7 +48,7 @@ var (
|
||||
DbPorts = "1433,1521,3306,5432,5672,5984,6379,7687,8086,9042,9093,9160,9200,11211,26379,27017,27018,61616"
|
||||
|
||||
// ServicePorts 服务端口
|
||||
ServicePorts = "21,22,23,25,53,110,111,135,139,143,161,389,445,465,502,512,513,514,587,623,636,873,993,995,1433,1521,2049,2181,2222,3306,3389,5432,5672,5671,5900,5985,5986,6379,8161,8443,9000,9092,9093,9200,10051,11211,15672,15671,27017,61616,61613"
|
||||
ServicePorts = "21,22,23,25,53,110,111,135,139,143,161,389,445,465,502,512,513,514,587,623,636,873,993,995,1433,1521,1883,2049,2181,2222,3306,3389,5432,5672,5671,5900,5985,5986,6379,8161,8443,8883,9000,9092,9093,9200,10051,11211,15672,15671,27017,61616,61613"
|
||||
|
||||
// CommonPorts 常用端口
|
||||
CommonPorts = "21,22,23,25,53,80,110,135,139,143,443,445,993,995,1723,3389,5060,5985,5986"
|
||||
|
||||
+55
-19
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/common/parsers"
|
||||
)
|
||||
|
||||
@@ -28,12 +29,12 @@ func BuildConfig(fv *FlagVars, info *HostInfo) (*Config, *State, error) {
|
||||
|
||||
// 3. 解析凭据
|
||||
if err := parseCredentials(fv, cfg); err != nil {
|
||||
return nil, nil, fmt.Errorf("凭据解析失败: %w", err)
|
||||
return nil, nil, fmt.Errorf("%s: %w", i18n.GetText("config_credentials_parse_failed"), err)
|
||||
}
|
||||
|
||||
// 4. 解析目标(主机、端口、URL)
|
||||
if err := parseTargets(fv, info, cfg, state); err != nil {
|
||||
return nil, nil, fmt.Errorf("目标解析失败: %w", err)
|
||||
return nil, nil, fmt.Errorf("%s: %w", i18n.GetText("config_targets_parse_failed"), err)
|
||||
}
|
||||
|
||||
// 5. 应用日志级别
|
||||
@@ -48,7 +49,10 @@ func BuildConfig(fv *FlagVars, info *HostInfo) (*Config, *State, error) {
|
||||
|
||||
func parseCredentials(fv *FlagVars, cfg *Config) error {
|
||||
// 解析用户名
|
||||
usernames := parseUsernames(fv)
|
||||
usernames, err := parseUsernames(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(usernames) > 0 {
|
||||
for serviceName := range cfg.Credentials.Userdict {
|
||||
cfg.Credentials.Userdict[serviceName] = usernames
|
||||
@@ -56,7 +60,10 @@ func parseCredentials(fv *FlagVars, cfg *Config) error {
|
||||
}
|
||||
|
||||
// 解析密码
|
||||
passwords := parsePasswords(fv)
|
||||
passwords, err := parsePasswords(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(passwords) > 0 {
|
||||
cfg.Credentials.Passwords = passwords
|
||||
}
|
||||
@@ -83,7 +90,7 @@ func parseCredentials(fv *FlagVars, cfg *Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseUsernames(fv *FlagVars) []string {
|
||||
func parseUsernames(fv *FlagVars) ([]string, error) {
|
||||
var usernames []string
|
||||
|
||||
// 命令行用户名
|
||||
@@ -100,6 +107,8 @@ func parseUsernames(fv *FlagVars) []string {
|
||||
if fv.UsersFile != "" {
|
||||
if lines, err := parsers.ReadLinesFromFile(fv.UsersFile); err == nil {
|
||||
usernames = append(usernames, lines...)
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_users_failed", fv.UsersFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,30 +122,47 @@ func parseUsernames(fv *FlagVars) []string {
|
||||
}
|
||||
}
|
||||
|
||||
return removeDuplicate(usernames)
|
||||
return removeDuplicate(usernames), nil
|
||||
}
|
||||
|
||||
func parsePasswords(fv *FlagVars) []string {
|
||||
func parsePasswords(fv *FlagVars) ([]string, error) {
|
||||
var passwords []string
|
||||
|
||||
// 命令行密码
|
||||
if fv.Password != "" {
|
||||
passwords = append(passwords, strings.Split(fv.Password, ",")...)
|
||||
passwords = append(passwords, fv.Password)
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.PasswordsFile != "" {
|
||||
if lines, err := parsers.ReadLinesFromFile(fv.PasswordsFile); err == nil {
|
||||
passwords = append(passwords, lines...)
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_passwords_failed", fv.PasswordsFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
// 额外密码
|
||||
if fv.AddPasswords != "" {
|
||||
passwords = append(passwords, strings.Split(fv.AddPasswords, ",")...)
|
||||
passwords = append(passwords, splitCredentialValues(fv.AddPasswords)...)
|
||||
}
|
||||
|
||||
return removeDuplicate(passwords)
|
||||
return removeDuplicate(passwords), nil
|
||||
}
|
||||
|
||||
func splitCredentialValues(input string) []string {
|
||||
fields := strings.FieldsFunc(input, func(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||||
})
|
||||
|
||||
values := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field != "" {
|
||||
values = append(values, field)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func parseUserPassPairs(fv *FlagVars) ([]config.CredentialPair, error) {
|
||||
@@ -172,12 +198,15 @@ func parseHashes(fv *FlagVars) ([]string, [][]byte, error) {
|
||||
// 命令行哈希
|
||||
if fv.HashValue != "" {
|
||||
hash := strings.TrimSpace(fv.HashValue)
|
||||
if len(hash) == 32 {
|
||||
hashValues = append(hashValues, hash)
|
||||
if hashByte, err := hex.DecodeString(hash); err == nil {
|
||||
hashBytes = append(hashBytes, hashByte)
|
||||
}
|
||||
if len(hash) != 32 {
|
||||
return nil, nil, fmt.Errorf("invalid hash length: %s", hash)
|
||||
}
|
||||
hashByte, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
hashValues = append(hashValues, hash)
|
||||
hashBytes = append(hashBytes, hashByte)
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
@@ -205,13 +234,17 @@ func parseTargets(fv *FlagVars, info *HostInfo, cfg *Config, state *State) error
|
||||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= 1 && port <= 65535 {
|
||||
// 有效的 host:port 格式
|
||||
state.SetHostPorts([]string{info.Host})
|
||||
info.Host = ""
|
||||
ports = "" // 清空端口,避免双重扫描
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 URL
|
||||
urls := parseURLs(fv)
|
||||
urls, err := parseURLs(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(urls) > 0 {
|
||||
state.SetURLs(urls)
|
||||
if info.URL == "" && len(urls) == 1 {
|
||||
@@ -227,7 +260,7 @@ func parseTargets(fv *FlagVars, info *HostInfo, cfg *Config, state *State) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseURLs(fv *FlagVars) []string {
|
||||
func parseURLs(fv *FlagVars) ([]string, error) {
|
||||
var urls []string
|
||||
|
||||
// 命令行 URL
|
||||
@@ -246,10 +279,12 @@ func parseURLs(fv *FlagVars) []string {
|
||||
for _, line := range lines {
|
||||
urls = append(urls, normalizeURL(line))
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_urls_failed", fv.URLsFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
return removeDuplicate(urls)
|
||||
return removeDuplicate(urls), nil
|
||||
}
|
||||
|
||||
func normalizeURL(rawURL string) string {
|
||||
@@ -257,7 +292,8 @@ func normalizeURL(rawURL string) string {
|
||||
if rawURL == "" {
|
||||
return rawURL
|
||||
}
|
||||
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
|
||||
lowerURL := strings.ToLower(rawURL)
|
||||
if !strings.HasPrefix(lowerURL, "http://") && !strings.HasPrefix(lowerURL, "https://") {
|
||||
return "http://" + rawURL
|
||||
}
|
||||
return rawURL
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParsePasswordsKeepsPrimaryPasswordLiteral(t *testing.T) {
|
||||
fv := &FlagVars{
|
||||
Password: "root admin",
|
||||
AddPasswords: "pass1 pass2,pass3\tpass4",
|
||||
}
|
||||
|
||||
got, err := parsePasswords(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parsePasswords error = %v", err)
|
||||
}
|
||||
want := []string{"root admin", "pass1", "pass2", "pass3", "pass4"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("parsePasswords() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigReturnsUserFileError(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{UsersFile: "missing-users-file.txt"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for missing users file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigReturnsPasswordFileError(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{PasswordsFile: "missing-passwords-file.txt"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for missing passwords file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigReturnsURLFileError(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{URLsFile: "missing-urls-file.txt"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for missing urls file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigRejectsInvalidHashValue(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{HashValue: "not-md5"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for invalid hash value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTargetsHostPortDoesNotLeaveSyntheticHost(t *testing.T) {
|
||||
fv := &FlagVars{Ports: "22"}
|
||||
info := &HostInfo{Host: "127.0.0.1:8080"}
|
||||
cfg := BuildConfigFromFlags(fv)
|
||||
state := NewState()
|
||||
|
||||
if err := parseTargets(fv, info, cfg, state); err != nil {
|
||||
t.Fatalf("parseTargets error = %v", err)
|
||||
}
|
||||
|
||||
if info.Host != "" {
|
||||
t.Fatalf("info.Host = %q, want empty after host:port extraction", info.Host)
|
||||
}
|
||||
if got := state.GetHostPorts(); !reflect.DeepEqual(got, []string{"127.0.0.1:8080"}) {
|
||||
t.Fatalf("hostPorts = %#v, want host:port target", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeURLKeepsUppercaseScheme(t *testing.T) {
|
||||
got := normalizeURL("HTTPS://example.com")
|
||||
if got != "HTTPS://example.com" {
|
||||
t.Fatalf("normalizeURL() = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ type Config struct {
|
||||
ModuleThreadNum int // 模块线程数
|
||||
DisableBrute bool // 禁用暴力破解
|
||||
DisablePing bool // 禁用Ping检测
|
||||
DisableTcpProbe bool // 禁用TCP补充探测
|
||||
|
||||
// 扫描模式
|
||||
Mode string // 扫描模式
|
||||
@@ -147,6 +148,7 @@ func NewConfig() *Config {
|
||||
ModuleThreadNum: 10,
|
||||
DisableBrute: false,
|
||||
DisablePing: false,
|
||||
DisableTcpProbe: false,
|
||||
|
||||
// 扫描模式
|
||||
Mode: DefaultScanMode,
|
||||
|
||||
+24
-22
@@ -9,6 +9,8 @@ import (
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"runtime/trace"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -19,82 +21,82 @@ var (
|
||||
|
||||
func Start() {
|
||||
if err := os.MkdirAll(profilesPath, 0755); err != nil {
|
||||
fmt.Printf("[DEBUG] 创建 profiles 目录失败: %v\n", err)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_profiles_failed", err))
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
cpuProfile, err = os.Create(profilesPath + "/cpu.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] 创建 CPU profile 失败: %v\n", err)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_cpu_profile_failed", err))
|
||||
} else {
|
||||
if err := pprof.StartCPUProfile(cpuProfile); err != nil {
|
||||
fmt.Printf("[DEBUG] 启动 CPU profile 失败: %v\n", err)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_start_cpu_profile_failed", err))
|
||||
cpuProfile.Close()
|
||||
cpuProfile = nil
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] CPU profiling 已启动 -> %s/cpu.prof\n", profilesPath)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_cpu_profile_started", profilesPath))
|
||||
}
|
||||
}
|
||||
|
||||
traceFile, err = os.Create(profilesPath + "/trace.out")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] 创建 trace 文件失败: %v\n", err)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_trace_failed", err))
|
||||
} else {
|
||||
if err := trace.Start(traceFile); err != nil {
|
||||
fmt.Printf("[DEBUG] 启动 trace 失败: %v\n", err)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_start_trace_failed", err))
|
||||
traceFile.Close()
|
||||
traceFile = nil
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] Execution trace 已启动 -> %s/trace.out\n", profilesPath)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_trace_started", profilesPath))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] 性能分析已启动,程序结束时自动保存到 %s/\n", profilesPath)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_profiling_started", profilesPath))
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
if cpuProfile != nil {
|
||||
pprof.StopCPUProfile()
|
||||
cpuProfile.Close()
|
||||
fmt.Printf("[DEBUG] CPU profile 已保存\n")
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_cpu_profile_saved"))
|
||||
}
|
||||
|
||||
if traceFile != nil {
|
||||
trace.Stop()
|
||||
traceFile.Close()
|
||||
fmt.Printf("[DEBUG] Trace 已保存\n")
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_trace_saved"))
|
||||
}
|
||||
|
||||
memProfile, err := os.Create(profilesPath + "/mem.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] 创建内存 profile 失败: %v\n", err)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_mem_profile_failed", err))
|
||||
} else {
|
||||
runtime.GC()
|
||||
if err := pprof.WriteHeapProfile(memProfile); err != nil {
|
||||
fmt.Printf("[DEBUG] 写入内存 profile 失败: %v\n", err)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_write_mem_profile_failed", err))
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] 内存 profile 已保存 -> %s/mem.prof\n", profilesPath)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_mem_profile_saved", profilesPath))
|
||||
}
|
||||
memProfile.Close()
|
||||
}
|
||||
|
||||
goroutineProfile, err := os.Create(profilesPath + "/goroutine.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] 创建 goroutine profile 失败: %v\n", err)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_goroutine_profile_failed", err))
|
||||
} else {
|
||||
if err := pprof.Lookup("goroutine").WriteTo(goroutineProfile, 0); err != nil {
|
||||
fmt.Printf("[DEBUG] 写入 goroutine profile 失败: %v\n", err)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_write_goroutine_profile_failed", err))
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] Goroutine profile 已保存 -> %s/goroutine.prof\n", profilesPath)
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_goroutine_profile_saved", profilesPath))
|
||||
}
|
||||
goroutineProfile.Close()
|
||||
}
|
||||
|
||||
fmt.Printf("\n[DEBUG] 所有性能分析文件已保存到 %s/\n", profilesPath)
|
||||
fmt.Printf("[DEBUG] 查看方法:\n")
|
||||
fmt.Printf(" CPU 火焰图: go tool pprof -http=:8081 %s/cpu.prof\n", profilesPath)
|
||||
fmt.Printf(" 内存火焰图: go tool pprof -http=:8081 %s/mem.prof\n", profilesPath)
|
||||
fmt.Printf(" 协程分析: go tool pprof -http=:8081 %s/goroutine.prof\n", profilesPath)
|
||||
fmt.Printf(" 执行时间线: go tool trace %s/trace.out\n", profilesPath)
|
||||
fmt.Printf("\n[DEBUG] %s\n", i18n.Tr("debug_profiles_saved", profilesPath))
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_view_methods"))
|
||||
fmt.Printf(" %s: go tool pprof -http=:8081 %s/cpu.prof\n", i18n.GetText("debug_cpu_flamegraph"), profilesPath)
|
||||
fmt.Printf(" %s: go tool pprof -http=:8081 %s/mem.prof\n", i18n.GetText("debug_mem_flamegraph"), profilesPath)
|
||||
fmt.Printf(" %s: go tool pprof -http=:8081 %s/goroutine.prof\n", i18n.GetText("debug_goroutine_analysis"), profilesPath)
|
||||
fmt.Printf(" %s: go tool trace %s/trace.out\n", i18n.GetText("debug_execution_timeline"), profilesPath)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// DNSCache 并发安全的 DNS 解析缓存
|
||||
// 对纯 IP 输入零开销(直接返回),对域名避免重复系统调用
|
||||
var DNSCache = &dnsCache{}
|
||||
|
||||
type dnsCache struct {
|
||||
m sync.Map // host -> *net.IPAddr
|
||||
}
|
||||
|
||||
// ResolveIP 解析 host 为 *net.IPAddr,结果缓存
|
||||
func (c *dnsCache) ResolveIP(host string) (*net.IPAddr, error) {
|
||||
if v, ok := c.m.Load(host); ok {
|
||||
addr, _ := v.(*net.IPAddr)
|
||||
return addr, nil
|
||||
}
|
||||
addr, err := net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.m.Store(host, addr)
|
||||
return addr, nil
|
||||
}
|
||||
+80
-6
@@ -15,6 +15,9 @@ import (
|
||||
// ErrShowHelp 表示用户请求显示帮助(正常退出)
|
||||
var ErrShowHelp = errors.New("show help requested")
|
||||
|
||||
// IsLocalMode 由 plugins 包注册,判断 -m 指定的是否全是本地插件
|
||||
var IsLocalMode func(mode string) bool
|
||||
|
||||
// Banner 显示程序横幅信息
|
||||
func Banner() {
|
||||
// 静默模式下完全跳过Banner显示
|
||||
@@ -109,7 +112,8 @@ func Flag(Info *HostInfo) error {
|
||||
flag.IntVar(&fv.ModuleThreadNum, "mt", 20, i18n.GetText("flag_module_thread_num"))
|
||||
flag.Int64Var(&fv.GlobalTimeout, "gt", 180, i18n.GetText("flag_global_timeout"))
|
||||
flag.BoolVar(&fv.DisablePing, "np", false, i18n.GetText("flag_disable_ping"))
|
||||
flag.StringVar(&fv.LocalPlugin, "local", "", "指定本地插件名称 (如: cleaner, avdetect, keylogger 等)")
|
||||
flag.BoolVar(&fv.DisableTcpProbe, "ntp", false, i18n.GetText("flag_disable_tcp_probe"))
|
||||
flag.StringVar(&fv.LocalPlugin, "local", "", i18n.GetText("flag_local_plugin"))
|
||||
flag.BoolVar(&fv.AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
@@ -181,8 +185,9 @@ func Flag(Info *HostInfo) error {
|
||||
flag.BoolVar(&fv.Silent, "silent", false, i18n.GetText("flag_silent_mode"))
|
||||
flag.BoolVar(&fv.NoColor, "nocolor", false, i18n.GetText("flag_no_color"))
|
||||
flag.StringVar(&fv.LogLevel, "log", LogLevelBaseInfoSuccess, i18n.GetText("flag_log_level"))
|
||||
flag.BoolVar(&fv.Debug, "debug", false, i18n.GetText("flag_debug"))
|
||||
flag.BoolVar(&fv.DisableProgress, "nopg", false, i18n.GetText("flag_disable_progress"))
|
||||
flag.BoolVar(&fv.PerfStats, "perf", false, "输出性能统计JSON")
|
||||
flag.BoolVar(&fv.PerfStats, "perf", false, i18n.GetText("flag_perf_stats"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 其他参数
|
||||
@@ -222,7 +227,9 @@ func Flag(Info *HostInfo) error {
|
||||
|
||||
// parseCommandLineArgs 解析命令行参数
|
||||
func parseCommandLineArgs() error {
|
||||
flag.Parse()
|
||||
if err := flag.CommandLine.Parse(normalizeMultiValueFlagArgs(os.Args[1:], "-pwda")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 显示Banner
|
||||
Banner()
|
||||
@@ -231,6 +238,63 @@ func parseCommandLineArgs() error {
|
||||
return checkParameterConflicts()
|
||||
}
|
||||
|
||||
func normalizeMultiValueFlagArgs(args []string, names ...string) []string {
|
||||
multiValueFlags := make(map[string]struct{}, len(names))
|
||||
for _, name := range names {
|
||||
multiValueFlags[name] = struct{}{}
|
||||
}
|
||||
|
||||
normalized := make([]string, 0, len(args))
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
name, value, ok := splitMultiValueFlag(arg, multiValueFlags)
|
||||
if !ok {
|
||||
normalized = append(normalized, arg)
|
||||
continue
|
||||
}
|
||||
|
||||
values := []string{}
|
||||
if value != "" {
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
j := i + 1
|
||||
for ; j < len(args); j++ {
|
||||
if strings.HasPrefix(args[j], "-") {
|
||||
break
|
||||
}
|
||||
values = append(values, args[j])
|
||||
}
|
||||
i = j - 1
|
||||
|
||||
if strings.Contains(arg, "=") {
|
||||
normalized = append(normalized, name+"="+strings.Join(values, ","))
|
||||
} else {
|
||||
normalized = append(normalized, name)
|
||||
if len(values) > 0 {
|
||||
normalized = append(normalized, strings.Join(values, ","))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
func splitMultiValueFlag(arg string, names map[string]struct{}) (string, string, bool) {
|
||||
if _, ok := names[arg]; ok {
|
||||
return arg, "", true
|
||||
}
|
||||
|
||||
for name := range names {
|
||||
prefix := name + "="
|
||||
if strings.HasPrefix(arg, prefix) {
|
||||
return name, strings.TrimPrefix(arg, prefix), true
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// preProcessLanguage 预处理语言参数,在定义flag之前设置语言
|
||||
func preProcessLanguage() {
|
||||
// 遍历命令行参数查找-lang参数
|
||||
@@ -270,9 +334,14 @@ func shouldShowHelp(Info *HostInfo, fv *FlagVars) bool {
|
||||
// 检查是否提供了扫描目标
|
||||
hasTarget := Info.Host != "" || fv.TargetURL != "" || fv.HostsFile != "" || fv.URLsFile != ""
|
||||
|
||||
// 本地模式需要指定插件才算有效目标
|
||||
// 本地模式不需要目标主机
|
||||
if fv.LocalPlugin != "" {
|
||||
hasTarget = true
|
||||
return false
|
||||
}
|
||||
|
||||
// -m 指定的全是本地插件时也不需要目标
|
||||
if IsLocalMode != nil && IsLocalMode(fv.ScanMode) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果没有提供任何扫描目标,则显示帮助
|
||||
@@ -284,6 +353,11 @@ func shouldShowHelp(Info *HostInfo, fv *FlagVars) bool {
|
||||
func checkParameterConflicts() error {
|
||||
fv := flagVars
|
||||
|
||||
// -debug 等价于 -log debug
|
||||
if fv.Debug {
|
||||
fv.LogLevel = LogLevelDebug
|
||||
}
|
||||
|
||||
// 检查 -ao 和 -m icmp 同时指定的情况(向后兼容提示)
|
||||
if fv.AliveOnly && fv.ScanMode == "icmp" {
|
||||
LogInfo(i18n.GetText("param_conflict_ao_icmp_both"))
|
||||
@@ -295,7 +369,7 @@ func checkParameterConflicts() error {
|
||||
invalidChars := []string{",", ";", " ", "|", "&"}
|
||||
for _, char := range invalidChars {
|
||||
if strings.Contains(fv.LocalPlugin, char) {
|
||||
return fmt.Errorf("本地插件只能指定单个插件,不支持使用 '%s' 分隔的多个插件", char)
|
||||
return fmt.Errorf("%s", i18n.Tr("param_local_multi_plugin", char))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeMultiValueFlagArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "space separated pwda values",
|
||||
args: []string{"-h", "192.168.1.1", "-pwda", "pass1", "pass2", "pass3", "-m", "ssh"},
|
||||
want: []string{"-h", "192.168.1.1", "-pwda", "pass1,pass2,pass3", "-m", "ssh"},
|
||||
},
|
||||
{
|
||||
name: "equals form with extra values",
|
||||
args: []string{"-pwda=pass1", "pass2", "-h", "192.168.1.1"},
|
||||
want: []string{"-pwda=pass1,pass2", "-h", "192.168.1.1"},
|
||||
},
|
||||
{
|
||||
name: "unrelated args unchanged",
|
||||
args: []string{"-h", "192.168.1.1", "-m", "ssh"},
|
||||
want: []string{"-h", "192.168.1.1", "-m", "ssh"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := normalizeMultiValueFlagArgs(tt.args, "-pwda")
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("normalizeMultiValueFlagArgs() = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ type FlagVars struct {
|
||||
TimeoutSec int64 // 秒,需转换为 time.Duration
|
||||
GlobalTimeout int64
|
||||
DisablePing bool
|
||||
DisableTcpProbe bool
|
||||
LocalPlugin string
|
||||
AliveOnly bool
|
||||
DisableBrute bool
|
||||
@@ -94,6 +95,7 @@ type FlagVars struct {
|
||||
Silent bool
|
||||
NoColor bool
|
||||
LogLevel string
|
||||
Debug bool
|
||||
DisableProgress bool
|
||||
PerfStats bool
|
||||
Language string
|
||||
@@ -137,6 +139,7 @@ func BuildConfigFromFlags(fv *FlagVars) *Config {
|
||||
ModuleThreadNum: fv.ModuleThreadNum,
|
||||
DisableBrute: fv.DisableBrute,
|
||||
DisablePing: fv.DisablePing,
|
||||
DisableTcpProbe: fv.DisableTcpProbe,
|
||||
|
||||
// 扫描模式
|
||||
Mode: fv.ScanMode,
|
||||
|
||||
+7
-3
@@ -2,7 +2,11 @@
|
||||
|
||||
package common
|
||||
|
||||
import "flag"
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// WebMode 表示是否启动Web管理界面
|
||||
var WebMode bool
|
||||
@@ -11,6 +15,6 @@ var WebMode bool
|
||||
var WebPort int
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&WebMode, "web", false, "启动Web管理界面 (Start Web UI)")
|
||||
flag.IntVar(&WebPort, "webport", 10240, "Web服务器端口 (Web server port)")
|
||||
flag.BoolVar(&WebMode, "web", false, i18n.GetText("flag_web_mode"))
|
||||
flag.IntVar(&WebPort, "webport", 10240, i18n.GetText("flag_web_port"))
|
||||
}
|
||||
|
||||
+10
-5
@@ -2,9 +2,12 @@ package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -28,7 +31,7 @@ type HostInfo struct {
|
||||
|
||||
// Target 返回 host:port 格式字符串
|
||||
func (h *HostInfo) Target() string {
|
||||
return fmt.Sprintf("%s:%d", h.Host, h.Port)
|
||||
return net.JoinHostPort(h.Host, strconv.Itoa(h.Port))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -62,11 +65,13 @@ const (
|
||||
|
||||
// 版本信息,通过 ldflags 注入
|
||||
var (
|
||||
version = "2.1.2"
|
||||
version = "2.2.0-rc"
|
||||
commit = "unknown"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
func GetVersion() string { return version }
|
||||
|
||||
// 运行时数据已迁移到Config对象中,使用GetGlobalConfig()访问
|
||||
|
||||
// Shell状态已迁移到State对象中,使用GetGlobalState()访问
|
||||
@@ -92,9 +97,9 @@ type PacketLimitError struct {
|
||||
|
||||
func (e *PacketLimitError) Error() string {
|
||||
if e.Sentinel == ErrMaxPacketReached {
|
||||
return fmt.Sprintf("已达到最大发包数量限制: %d", e.Limit)
|
||||
return i18n.Tr("packet_limit_max_reached", e.Limit)
|
||||
}
|
||||
return fmt.Sprintf("发包速率受限: %d包/分钟", e.Limit)
|
||||
return i18n.Tr("packet_limit_rate_limited", e.Limit)
|
||||
}
|
||||
|
||||
func (e *PacketLimitError) Unwrap() error {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHostInfoTargetUsesBracketedIPv6(t *testing.T) {
|
||||
info := &HostInfo{Host: "2001:db8::1", Port: 443}
|
||||
if got, want := info.Target(), "[2001:db8::1]:443"; got != want {
|
||||
t.Fatalf("Target() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,13 @@ func SetLanguage(l string) {
|
||||
localizer = i18n.NewLocalizer(bundle, lang, FallbackLanguage)
|
||||
}
|
||||
|
||||
// GetLanguage returns the currently configured language.
|
||||
func GetLanguage() string {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return lang
|
||||
}
|
||||
|
||||
// GetText 获取国际化文本(无参数)
|
||||
func GetText(key string) string {
|
||||
mu.RLock()
|
||||
|
||||
+1094
-32
File diff suppressed because it is too large
Load Diff
+1093
-34
File diff suppressed because it is too large
Load Diff
+17
-11
@@ -2,6 +2,8 @@ package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -13,9 +15,10 @@ initialize.go - 统一初始化入口
|
||||
|
||||
// InitResult 初始化结果
|
||||
type InitResult struct {
|
||||
Config *Config
|
||||
State *State
|
||||
Info *HostInfo
|
||||
Config *Config
|
||||
State *State
|
||||
Info *HostInfo
|
||||
Session *ScanSession
|
||||
}
|
||||
|
||||
// Initialize 统一初始化函数
|
||||
@@ -27,7 +30,7 @@ func Initialize(info *HostInfo) (*InitResult, error) {
|
||||
// 2. 从 FlagVars 构建 Config 和 State
|
||||
cfg, state, err := BuildConfig(GetFlagVars(), info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("配置构建失败: %w", err)
|
||||
return nil, fmt.Errorf("%s: %w", i18n.GetText("config_build_failed"), err)
|
||||
}
|
||||
|
||||
// 3. 设置全局实例
|
||||
@@ -36,13 +39,16 @@ func Initialize(info *HostInfo) (*InitResult, error) {
|
||||
|
||||
// 4. 初始化输出系统
|
||||
if err := InitOutput(); err != nil {
|
||||
return nil, fmt.Errorf("输出初始化失败: %w", err)
|
||||
return nil, fmt.Errorf("%s: %w", i18n.GetText("output_init_failed"), err)
|
||||
}
|
||||
|
||||
session := NewScanSession(cfg, state, GetFlagVars())
|
||||
|
||||
return &InitResult{
|
||||
Config: cfg,
|
||||
State: state,
|
||||
Info: info,
|
||||
Config: cfg,
|
||||
State: state,
|
||||
Info: info,
|
||||
Session: session,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -61,7 +67,7 @@ func ValidateExclusiveParams(info *HostInfo) error {
|
||||
if fv.TargetURL != "" {
|
||||
paramCount++
|
||||
if activeParam != "" {
|
||||
activeParam += " 和 -u"
|
||||
activeParam = i18n.Tr("param_join_and", activeParam, "-u")
|
||||
} else {
|
||||
activeParam = "-u"
|
||||
}
|
||||
@@ -69,14 +75,14 @@ func ValidateExclusiveParams(info *HostInfo) error {
|
||||
if fv.LocalPlugin != "" {
|
||||
paramCount++
|
||||
if activeParam != "" {
|
||||
activeParam += " 和 -local"
|
||||
activeParam = i18n.Tr("param_join_and", activeParam, "-local")
|
||||
} else {
|
||||
activeParam = "-local"
|
||||
}
|
||||
}
|
||||
|
||||
if paramCount > 1 {
|
||||
return fmt.Errorf("参数 %s 互斥,请只指定一个扫描目标\n -h: 网络主机扫描\n -u: Web URL扫描\n -local: 本地信息收集", activeParam)
|
||||
return fmt.Errorf("%s", i18n.Tr("param_exclusive", activeParam))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+52
-2
@@ -14,11 +14,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
globalLogger *logging.Logger
|
||||
loggerOnce sync.Once
|
||||
globalLogger *logging.Logger
|
||||
loggerOnce sync.Once
|
||||
loggerMu sync.Mutex
|
||||
silentLoggerRefs int
|
||||
)
|
||||
|
||||
func getGlobalLogger() *logging.Logger {
|
||||
loggerMu.Lock()
|
||||
defer loggerMu.Unlock()
|
||||
|
||||
loggerOnce.Do(func() {
|
||||
fv := GetFlagVars()
|
||||
level := getLogLevelFromString(fv.LogLevel)
|
||||
@@ -27,8 +32,12 @@ func getGlobalLogger() *logging.Logger {
|
||||
EnableColor: !fv.NoColor,
|
||||
SlowOutput: false,
|
||||
ShowProgress: !fv.DisableProgress,
|
||||
Silent: fv.Silent || silentLoggerRefs > 0,
|
||||
StartTime: GetGlobalState().GetStartTime(),
|
||||
}
|
||||
if fv.Debug {
|
||||
config.DebugLogFile = "fscan_debug.log"
|
||||
}
|
||||
globalLogger = logging.NewLogger(config)
|
||||
globalLogger.SetCoordinatedOutput(LogWithProgress)
|
||||
})
|
||||
@@ -77,3 +86,44 @@ func LogVuln(result string) { getGlobalLogger().Vuln(result) }
|
||||
|
||||
// LogError 输出错误日志
|
||||
func LogError(errMsg string) { getGlobalLogger().Error(errMsg) }
|
||||
|
||||
// CloseLogger 关闭日志系统,释放文件资源
|
||||
func CloseLogger() {
|
||||
loggerMu.Lock()
|
||||
defer loggerMu.Unlock()
|
||||
closeLoggerLocked()
|
||||
}
|
||||
|
||||
// PushSilentLogger suppresses process-wide legacy log output until the returned
|
||||
// restore function is called. It is reference counted so concurrent embedded
|
||||
// scans can overlap safely.
|
||||
func PushSilentLogger() func() {
|
||||
loggerMu.Lock()
|
||||
silentLoggerRefs++
|
||||
resetLoggerLocked()
|
||||
loggerMu.Unlock()
|
||||
|
||||
var once sync.Once
|
||||
return func() {
|
||||
once.Do(func() {
|
||||
loggerMu.Lock()
|
||||
if silentLoggerRefs > 0 {
|
||||
silentLoggerRefs--
|
||||
}
|
||||
resetLoggerLocked()
|
||||
loggerMu.Unlock()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func resetLoggerLocked() {
|
||||
closeLoggerLocked()
|
||||
globalLogger = nil
|
||||
loggerOnce = sync.Once{}
|
||||
}
|
||||
|
||||
func closeLoggerLocked() {
|
||||
if globalLogger != nil {
|
||||
globalLogger.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -24,8 +25,10 @@ type LoggerConfig struct {
|
||||
EnableColor bool `json:"enable_color"`
|
||||
SlowOutput bool `json:"slow_output"`
|
||||
ShowProgress bool `json:"show_progress"`
|
||||
Silent bool `json:"silent"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
LevelColors map[LogLevel]interface{} `json:"-"`
|
||||
DebugLogFile string `json:"debug_log_file"`
|
||||
}
|
||||
|
||||
// DefaultLoggerConfig 默认日志器配置
|
||||
@@ -47,6 +50,7 @@ type Logger struct {
|
||||
startTime time.Time
|
||||
coordinatedOutput func(string)
|
||||
initialized bool
|
||||
debugFile *os.File
|
||||
}
|
||||
|
||||
// NewLogger 创建新的日志管理器
|
||||
@@ -55,11 +59,20 @@ func NewLogger(config *LoggerConfig) *Logger {
|
||||
config = DefaultLoggerConfig()
|
||||
}
|
||||
|
||||
return &Logger{
|
||||
l := &Logger{
|
||||
config: config,
|
||||
startTime: config.StartTime,
|
||||
initialized: true,
|
||||
}
|
||||
|
||||
if config.DebugLogFile != "" {
|
||||
f, err := os.OpenFile(config.DebugLogFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err == nil {
|
||||
l.debugFile = f
|
||||
}
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// Initialize 初始化日志器
|
||||
@@ -111,6 +124,10 @@ func (l *Logger) log(level LogLevel, content string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if l.config.Silent {
|
||||
return
|
||||
}
|
||||
|
||||
if !l.shouldLog(level) {
|
||||
return
|
||||
}
|
||||
@@ -134,12 +151,37 @@ func (l *Logger) log(level LogLevel, content string) {
|
||||
l.outputMessage(level, logMsg)
|
||||
}
|
||||
|
||||
// 写入debug日志文件(纯文本,无颜色)
|
||||
if l.debugFile != nil {
|
||||
timestamp := time.Since(l.startTime).Truncate(time.Millisecond)
|
||||
if strings.Contains(content, "\n") {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
_, _ = fmt.Fprintf(l.debugFile, "[%s] %s %s\n", timestamp, prefix, line)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(l.debugFile, "[%s] %s %s\n", timestamp, prefix, content)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据慢速输出设置决定是否添加延迟
|
||||
if l.config.SlowOutput {
|
||||
time.Sleep(SlowOutputDelay)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭日志器,释放文件资源
|
||||
func (l *Logger) Close() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.debugFile != nil {
|
||||
_ = l.debugFile.Close()
|
||||
l.debugFile = nil
|
||||
}
|
||||
}
|
||||
|
||||
// shouldLog 检查是否应该记录该级别的日志
|
||||
// 层级过滤:消息级别 >= 配置级别 时显示,Error 始终显示
|
||||
func (l *Logger) shouldLog(level LogLevel) bool {
|
||||
|
||||
+14
-10
@@ -16,6 +16,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/common/proxy"
|
||||
)
|
||||
|
||||
@@ -102,22 +103,20 @@ func createProxyConfig(timeout time.Duration) *proxy.ProxyConfig {
|
||||
// TCP 连接
|
||||
// =============================================================================
|
||||
|
||||
// WrapperTcpWithTimeout TCP连接包装器,带超时
|
||||
// 支持通过代理管理器进行SOCKS5和HTTP代理连接,并集成发包控制
|
||||
// 使用全局拨号器复用连接,避免重复创建代理握手开销
|
||||
// Deprecated: WrapperTcpWithTimeout 仅供 libs/grdp 兼容使用,新代码请用 ScanSession.DialTCP
|
||||
//
|
||||
//nolint:revive // 保持向后兼容性,避免破坏大量现有代码
|
||||
//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)
|
||||
LogError(i18n.Tr("tcp_connection_restricted", address, reason))
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", reason))
|
||||
}
|
||||
|
||||
// 获取全局拨号器(复用,避免重复创建)
|
||||
dialer, err := getGlobalDialer(timeout)
|
||||
if err != nil {
|
||||
LogError(fmt.Sprintf("获取代理拨号器失败: %v", err))
|
||||
LogError(i18n.Tr("proxy_dialer_failed", err))
|
||||
GetGlobalState().IncrementTCPFailedPacketCount()
|
||||
return nil, err
|
||||
}
|
||||
@@ -128,7 +127,7 @@ func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.
|
||||
// 统计TCP包数量 - 无论是否使用代理都要计数
|
||||
if err != nil {
|
||||
GetGlobalState().IncrementTCPFailedPacketCount()
|
||||
LogDebug(fmt.Sprintf("连接 %s 失败: %v", address, err))
|
||||
LogDebug(i18n.Tr("connection_failed", address, err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -158,12 +157,17 @@ func IsProxyReliable() bool {
|
||||
return proxy.IsProxyReliable()
|
||||
}
|
||||
|
||||
// IsSOCKS5Proxy 检查当前代理是否为SOCKS5类型
|
||||
func IsSOCKS5Proxy() bool {
|
||||
return proxy.IsSOCKS5Proxy()
|
||||
}
|
||||
|
||||
// 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)
|
||||
LogError(i18n.Tr("http_request_restricted", req.URL.String(), reason))
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", reason))
|
||||
}
|
||||
|
||||
// 执行HTTP请求
|
||||
|
||||
+26
-6
@@ -1,9 +1,6 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
import "sync"
|
||||
|
||||
// ResultBuffer 公共的去重缓冲逻辑,供各Writer复用
|
||||
type ResultBuffer struct {
|
||||
@@ -59,7 +56,8 @@ func (b *ResultBuffer) Add(result *ScanResult) {
|
||||
b.seenServices[key] = len(b.ServiceResults)
|
||||
b.ServiceResults = append(b.ServiceResults, result)
|
||||
} else {
|
||||
// 保留信息更完整的记录
|
||||
b.mergeDetails(b.ServiceResults[idx], result)
|
||||
// 保留信息更完整的记录,同时保留另一条记录补充的字段
|
||||
if b.isMoreComplete(result, b.ServiceResults[idx]) {
|
||||
b.ServiceResults[idx] = result
|
||||
}
|
||||
@@ -72,6 +70,28 @@ func (b *ResultBuffer) Add(result *ScanResult) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ResultBuffer) mergeDetails(oldResult, newResult *ScanResult) {
|
||||
if oldResult == nil || newResult == nil {
|
||||
return
|
||||
}
|
||||
if oldResult.Details == nil {
|
||||
oldResult.Details = make(map[string]interface{})
|
||||
}
|
||||
if newResult.Details == nil {
|
||||
newResult.Details = make(map[string]interface{})
|
||||
}
|
||||
for k, v := range oldResult.Details {
|
||||
if _, exists := newResult.Details[k]; !exists {
|
||||
newResult.Details[k] = v
|
||||
}
|
||||
}
|
||||
for k, v := range newResult.Details {
|
||||
if _, exists := oldResult.Details[k]; !exists {
|
||||
oldResult.Details[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generateKey 生成结果的唯一键(用于去重)
|
||||
func (b *ResultBuffer) generateKey(result *ScanResult) string {
|
||||
switch result.Type {
|
||||
@@ -80,7 +100,7 @@ func (b *ResultBuffer) generateKey(result *ScanResult) string {
|
||||
case TypePort:
|
||||
if result.Details != nil {
|
||||
if port, ok := result.Details["port"]; ok {
|
||||
return fmt.Sprintf("%s:%v", result.Target, port)
|
||||
return targetWithPort(result.Target, port)
|
||||
}
|
||||
}
|
||||
return result.Target
|
||||
|
||||
@@ -226,6 +226,40 @@ func TestResultBuffer_ServiceUpdate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultBuffer_ServiceUpdateMergesDetails(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "identified",
|
||||
Details: map[string]interface{}{
|
||||
"service": "http",
|
||||
"banner": "HTTP/1.1 200 OK",
|
||||
},
|
||||
})
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "web",
|
||||
Details: map[string]interface{}{
|
||||
"title": "Home",
|
||||
"status": 200,
|
||||
"server": "nginx",
|
||||
},
|
||||
})
|
||||
|
||||
if len(buf.ServiceResults) != 1 {
|
||||
t.Fatalf("期望1条服务记录,实际 %d", len(buf.ServiceResults))
|
||||
}
|
||||
details := buf.ServiceResults[0].Details
|
||||
for _, key := range []string{"service", "banner", "title", "status", "server"} {
|
||||
if _, ok := details[key]; !ok {
|
||||
t.Errorf("合并后的服务记录缺少字段 %q: %#v", key, details)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_ServiceNoDowngrade 测试不降级服务记录
|
||||
//
|
||||
// 当新记录不如旧记录完整时,不应替换
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type StdoutNDJSONWriter struct {
|
||||
mu sync.Mutex
|
||||
writer *bufio.Writer
|
||||
}
|
||||
|
||||
func NewStdoutNDJSONWriter() *StdoutNDJSONWriter {
|
||||
return &StdoutNDJSONWriter{
|
||||
writer: bufio.NewWriter(os.Stdout),
|
||||
}
|
||||
}
|
||||
|
||||
// ndjsonRecord NDJSON 输出的扁平化结构
|
||||
type ndjsonRecord struct {
|
||||
Type ResultType `json:"type"`
|
||||
Target string `json:"target"`
|
||||
Status string `json:"status"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Service string `json:"service,omitempty"`
|
||||
// 通用可选字段
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Banner string `json:"banner,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
// 漏洞/弱口令
|
||||
Vulnerability string `json:"vulnerability,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
// 其他
|
||||
Plugin string `json:"plugin,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
}
|
||||
|
||||
func (w *StdoutNDJSONWriter) WriteResult(result *ScanResult) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
rec := w.flatten(result)
|
||||
data, err := json.Marshal(rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
if _, err := w.writer.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.writer.Flush()
|
||||
}
|
||||
|
||||
func (w *StdoutNDJSONWriter) flatten(r *ScanResult) *ndjsonRecord {
|
||||
rec := &ndjsonRecord{
|
||||
Type: r.Type,
|
||||
Target: r.Target,
|
||||
Status: r.Status,
|
||||
}
|
||||
|
||||
// 从 target 拆分 host:port
|
||||
if host, port, ok := splitHostPort(r.Target); ok {
|
||||
rec.Host = host
|
||||
rec.Port = port
|
||||
} else {
|
||||
rec.Host = r.Target
|
||||
}
|
||||
|
||||
d := r.Details
|
||||
if d == nil {
|
||||
return rec
|
||||
}
|
||||
|
||||
// 从 details 提升一级字段(覆盖拆分结果)
|
||||
if v, ok := d["port"]; ok {
|
||||
if p, ok := toInt(v); ok {
|
||||
rec.Port = p
|
||||
}
|
||||
}
|
||||
|
||||
rec.Service = strVal(d, "service")
|
||||
rec.Protocol = strVal(d, "protocol")
|
||||
rec.Banner = strVal(d, "banner")
|
||||
rec.Title = strVal(d, "title")
|
||||
rec.URL = strVal(d, "url")
|
||||
rec.Vulnerability = strVal(d, "vulnerability")
|
||||
rec.Username = strVal(d, "username")
|
||||
rec.Password = strVal(d, "password")
|
||||
rec.Plugin = strVal(d, "plugin")
|
||||
rec.Version = strVal(d, "version")
|
||||
rec.OS = strVal(d, "os")
|
||||
|
||||
return rec
|
||||
}
|
||||
|
||||
func (w *StdoutNDJSONWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.writer.Flush()
|
||||
}
|
||||
|
||||
func strVal(d map[string]interface{}, key string) string {
|
||||
v, ok := d[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func toInt(v interface{}) (int, bool) {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n, true
|
||||
case int64:
|
||||
return int(n), true
|
||||
case float64:
|
||||
return int(n), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func splitHostPort(target string) (string, int, bool) {
|
||||
idx := strings.LastIndex(target, ":")
|
||||
if idx < 0 {
|
||||
return "", 0, false
|
||||
}
|
||||
host := target[:idx]
|
||||
var port int
|
||||
if _, err := fmt.Sscanf(target[idx+1:], "%d", &port); err != nil {
|
||||
return "", 0, false
|
||||
}
|
||||
return host, port, true
|
||||
}
|
||||
+95
-51
@@ -5,21 +5,51 @@ import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// escapeControlChars 转义控制字符
|
||||
func escapeControlChars(s string) string {
|
||||
replacer := strings.NewReplacer(
|
||||
"\r\n", "\\r\\n",
|
||||
"\n", "\\n",
|
||||
"\r", "\\r",
|
||||
"\t", "\\t",
|
||||
)
|
||||
return replacer.Replace(s)
|
||||
s = strings.ToValidUTF8(s, "?")
|
||||
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '\n':
|
||||
b.WriteString("\\n")
|
||||
case '\r':
|
||||
b.WriteString("\\r")
|
||||
case '\t':
|
||||
b.WriteString("\\t")
|
||||
default:
|
||||
if r < 0x20 || r == 0x7f {
|
||||
fmt.Fprintf(&b, "\\x%02x", r)
|
||||
continue
|
||||
}
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func targetWithPort(target string, port interface{}) string {
|
||||
if port == nil {
|
||||
return target
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(target); err == nil {
|
||||
return target
|
||||
}
|
||||
portText := fmt.Sprint(port)
|
||||
if strings.Count(target, ":") == 1 {
|
||||
return target
|
||||
}
|
||||
return net.JoinHostPort(target, portText)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -99,13 +129,13 @@ func (w *TXTWriter) Write(result *ScanResult) error {
|
||||
func (w *TXTWriter) getSeparator(newType ResultType) string {
|
||||
switch newType {
|
||||
case TypeHost:
|
||||
return "# ===== 存活主机 ====="
|
||||
return i18n.GetText("output_section_hosts")
|
||||
case TypePort:
|
||||
return "# ===== 开放端口 ====="
|
||||
return i18n.GetText("output_section_ports")
|
||||
case TypeService:
|
||||
return "# ===== 服务信息 ====="
|
||||
return i18n.GetText("output_section_services")
|
||||
case TypeVuln:
|
||||
return "# ===== 漏洞信息 ====="
|
||||
return i18n.GetText("output_section_vulns")
|
||||
default:
|
||||
return "# ===================="
|
||||
}
|
||||
@@ -119,7 +149,7 @@ func (w *TXTWriter) formatLine(result *ScanResult) string {
|
||||
case TypePort:
|
||||
port := w.getDetail(result, "port")
|
||||
if port != nil {
|
||||
return fmt.Sprintf("%s:%v", result.Target, port)
|
||||
return targetWithPort(result.Target, port)
|
||||
}
|
||||
return result.Target
|
||||
case TypeService:
|
||||
@@ -152,12 +182,7 @@ func (w *TXTWriter) formatServiceLine(result *ScanResult) string {
|
||||
}
|
||||
|
||||
// 非Web服务:ip:port service banner
|
||||
target := result.Target
|
||||
if !strings.Contains(target, ":") {
|
||||
if port := w.getDetail(result, "port"); port != nil {
|
||||
target = fmt.Sprintf("%s:%v", target, port)
|
||||
}
|
||||
}
|
||||
target := targetWithPort(result.Target, w.getDetail(result, "port"))
|
||||
|
||||
var parts []string
|
||||
parts = append(parts, target)
|
||||
@@ -176,20 +201,9 @@ func (w *TXTWriter) formatServiceLine(result *ScanResult) string {
|
||||
|
||||
// formatWebServiceLine 格式化Web服务结果
|
||||
func (w *TXTWriter) formatWebServiceLine(result *ScanResult) string {
|
||||
target := result.Target
|
||||
if !strings.Contains(target, ":") {
|
||||
if port := w.getDetail(result, "port"); port != nil {
|
||||
target = fmt.Sprintf("%s:%v", target, port)
|
||||
}
|
||||
}
|
||||
target := targetWithPort(result.Target, w.getDetail(result, "port"))
|
||||
|
||||
protocol := "http"
|
||||
service := w.getDetailStr(result, "service")
|
||||
if service == "https" || strings.Contains(target, ":443") {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s://%s", protocol, target)
|
||||
url := fmt.Sprintf("%s://%s", w.webProtocol(result, target), target)
|
||||
title := w.getDetailStr(result, "title")
|
||||
status := w.getDetail(result, "status")
|
||||
server := w.getDetailStr(result, "server")
|
||||
@@ -355,27 +369,16 @@ func (w *TXTWriter) writeWebServices() {
|
||||
continue
|
||||
}
|
||||
|
||||
target := result.Target
|
||||
if !strings.Contains(target, ":") {
|
||||
if port := w.getDetail(result, "port"); port != nil {
|
||||
target = fmt.Sprintf("%s:%v", target, port)
|
||||
}
|
||||
}
|
||||
target := targetWithPort(result.Target, w.getDetail(result, "port"))
|
||||
|
||||
protocol := "http"
|
||||
service := w.getDetailStr(result, "service")
|
||||
if service == "https" || strings.Contains(target, ":443") {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
urls = append(urls, fmt.Sprintf("%s://%s", protocol, target))
|
||||
urls = append(urls, fmt.Sprintf("%s://%s", w.webProtocol(result, target), target))
|
||||
}
|
||||
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = w.bufWriter.WriteString("# ===== Web服务 =====\n")
|
||||
_, _ = w.bufWriter.WriteString(i18n.GetText("output_section_web_services") + "\n")
|
||||
for _, url := range urls {
|
||||
_, _ = w.bufWriter.WriteString(url + "\n")
|
||||
}
|
||||
@@ -397,6 +400,19 @@ func (w *TXTWriter) isWebService(result *ScanResult) bool {
|
||||
return service == "http" || service == "https"
|
||||
}
|
||||
|
||||
func (w *TXTWriter) webProtocol(result *ScanResult, target string) string {
|
||||
protocol := strings.ToLower(w.getDetailStr(result, "protocol"))
|
||||
if protocol == "http" || protocol == "https" {
|
||||
return protocol
|
||||
}
|
||||
|
||||
service := strings.ToLower(w.getDetailStr(result, "service"))
|
||||
if service == "https" || strings.Contains(target, ":443") {
|
||||
return "https"
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
// GetFormat 获取格式类型
|
||||
func (w *TXTWriter) GetFormat() Format {
|
||||
return FormatTXT
|
||||
@@ -647,7 +663,7 @@ func (w *CSVWriter) Close() error {
|
||||
// 写入各分类
|
||||
w.writeSection("# Hosts", []string{"Target"}, w.buffer.HostResults, w.formatHostRecord)
|
||||
w.writeSection("# Ports", []string{"Target", "Port", "Status"}, w.buffer.PortResults, w.formatPortRecord)
|
||||
w.writeSection("# Services", []string{"Target", "Service", "Version", "Banner"}, w.buffer.ServiceResults, w.formatServiceRecord)
|
||||
w.writeSection("# Services", []string{"Target", "Service", "Version", "Title", "Status", "Server", "Fingerprints", "Banner"}, w.buffer.ServiceResults, w.formatServiceRecord)
|
||||
w.writeSection("# Vulns", []string{"Target", "Type", "Details"}, w.buffer.VulnResults, w.formatVulnRecord)
|
||||
|
||||
w.closed = true
|
||||
@@ -697,7 +713,7 @@ func (w *CSVWriter) formatPortRecord(result *ScanResult) []string {
|
||||
}
|
||||
|
||||
func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string {
|
||||
service, version, banner := "", "", ""
|
||||
service, version, title, status, server, fingerprints, banner := "", "", "", "", "", "", ""
|
||||
if result.Details != nil {
|
||||
if s, ok := result.Details["service"].(string); ok {
|
||||
service = s
|
||||
@@ -705,9 +721,22 @@ func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string {
|
||||
if s, ok := result.Details["name"].(string); ok && service == "" {
|
||||
service = s
|
||||
}
|
||||
if s, ok := result.Details["plugin"].(string); ok && service == "" {
|
||||
service = s
|
||||
}
|
||||
if v, ok := result.Details["version"].(string); ok {
|
||||
version = v
|
||||
}
|
||||
if t, ok := result.Details["title"].(string); ok {
|
||||
title = escapeControlChars(t)
|
||||
}
|
||||
if s, ok := result.Details["status"]; ok && s != nil && s != 0 {
|
||||
status = fmt.Sprintf("%v", s)
|
||||
}
|
||||
if s, ok := result.Details["server"].(string); ok {
|
||||
server = escapeControlChars(s)
|
||||
}
|
||||
fingerprints = formatFingerprints(result.Details["fingerprints"])
|
||||
if b, ok := result.Details["banner"].(string); ok {
|
||||
banner = escapeControlChars(b)
|
||||
if len(banner) > 100 {
|
||||
@@ -716,12 +745,27 @@ func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string {
|
||||
}
|
||||
}
|
||||
target := result.Target
|
||||
if !strings.Contains(target, ":") {
|
||||
if p, ok := result.Details["port"]; ok {
|
||||
target = fmt.Sprintf("%s:%v", target, p)
|
||||
if result.Details != nil {
|
||||
target = targetWithPort(target, result.Details["port"])
|
||||
}
|
||||
return []string{target, service, version, title, status, server, fingerprints, banner}
|
||||
}
|
||||
|
||||
func formatFingerprints(value interface{}) string {
|
||||
switch v := value.(type) {
|
||||
case []string:
|
||||
return strings.Join(v, ",")
|
||||
case []interface{}:
|
||||
parts := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if s, ok := item.(string); ok && s != "" {
|
||||
parts = append(parts, s)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
return []string{target, service, version, banner}
|
||||
}
|
||||
|
||||
func (w *CSVWriter) formatVulnRecord(result *ScanResult) []string {
|
||||
|
||||
@@ -57,6 +57,28 @@ func createTestResult(resultType ResultType, target, status string, details map[
|
||||
}
|
||||
}
|
||||
|
||||
func TestTargetWithPortIPv6(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
target string
|
||||
port interface{}
|
||||
want string
|
||||
}{
|
||||
{name: "ipv4 without port", target: "192.168.1.1", port: 80, want: "192.168.1.1:80"},
|
||||
{name: "ipv4 with port", target: "192.168.1.1:80", port: 443, want: "192.168.1.1:80"},
|
||||
{name: "ipv6 without port", target: "2001:db8::1", port: 443, want: "[2001:db8::1]:443"},
|
||||
{name: "ipv6 with port", target: "[2001:db8::1]:443", port: 80, want: "[2001:db8::1]:443"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := targetWithPort(tt.target, tt.port); got != tt.want {
|
||||
t.Fatalf("targetWithPort(%q, %v) = %q, want %q", tt.target, tt.port, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TXTWriter - 基础功能测试
|
||||
// =============================================================================
|
||||
@@ -1139,7 +1161,7 @@ func TestCSVWriter_ErrorHandling(t *testing.T) {
|
||||
// TestCSVWriter_DetailsFormatting 测试CSV的Details字段格式化
|
||||
//
|
||||
// CSVWriter 对不同类型有不同的格式:
|
||||
// - Service类型:Target, Service, Version, Banner
|
||||
// - Service类型:Target, Service, Version, Title, Status, Server, Fingerprints, Banner
|
||||
func TestCSVWriter_DetailsFormatting(t *testing.T) {
|
||||
dir := createTestDir(t)
|
||||
filePath := filepath.Join(dir, "test.csv")
|
||||
@@ -1188,6 +1210,86 @@ func TestCSVWriter_DetailsFormatting(t *testing.T) {
|
||||
t.Logf("✓ CSV Details格式化测试通过")
|
||||
}
|
||||
|
||||
func TestCSVWriter_WebServiceFields(t *testing.T) {
|
||||
dir := createTestDir(t)
|
||||
filePath := filepath.Join(dir, "test.csv")
|
||||
|
||||
writer, _ := NewCSVWriter(filePath)
|
||||
defer func() { _ = writer.Close() }()
|
||||
|
||||
_ = writer.WriteHeader()
|
||||
result := createTestResult(
|
||||
TypeService,
|
||||
"192.168.1.1:80",
|
||||
"web",
|
||||
map[string]interface{}{
|
||||
"plugin": "webtitle",
|
||||
"is_web": true,
|
||||
"port": 80,
|
||||
"title": "Home",
|
||||
"status": 200,
|
||||
"server": "nginx",
|
||||
"fingerprints": []string{"nginx", "php"},
|
||||
"banner": "HTTP/1.1 200 OK\x00\nServer: nginx",
|
||||
},
|
||||
)
|
||||
_ = writer.Write(result)
|
||||
writer.Close()
|
||||
|
||||
content := readFileContent(t, filePath)
|
||||
for _, want := range []string{
|
||||
"Target,Service,Version,Title,Status,Server,Fingerprints,Banner",
|
||||
"webtitle",
|
||||
"Home",
|
||||
"200",
|
||||
"nginx",
|
||||
"nginx,php",
|
||||
"\\x00\\nServer: nginx",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Errorf("CSV文件缺少 %q,内容:\n%s", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTXTWriter_WebServiceProtocolFromDetails(t *testing.T) {
|
||||
dir := createTestDir(t)
|
||||
filePath := filepath.Join(dir, "test_web_protocol.txt")
|
||||
|
||||
writer, err := NewTXTWriter(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("创建TXTWriter失败: %v", err)
|
||||
}
|
||||
|
||||
result := createTestResult(
|
||||
TypeService,
|
||||
"192.168.1.1:8443",
|
||||
"web",
|
||||
map[string]interface{}{
|
||||
"plugin": "webtitle",
|
||||
"is_web": true,
|
||||
"port": 8443,
|
||||
"protocol": "https",
|
||||
"title": "Home",
|
||||
"status": 200,
|
||||
},
|
||||
)
|
||||
if err := writer.Write(result); err != nil {
|
||||
t.Fatalf("Write()失败: %v", err)
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("Close()失败: %v", err)
|
||||
}
|
||||
|
||||
content := readFileContent(t, filePath)
|
||||
if !strings.Contains(content, "https://192.168.1.1:8443") {
|
||||
t.Fatalf("TXT输出缺少HTTPS URL,内容:\n%s", content)
|
||||
}
|
||||
if strings.Contains(content, "http://192.168.1.1:8443") {
|
||||
t.Fatalf("TXT输出不应把HTTPS目标降级为HTTP,内容:\n%s", content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestJSONWriter_FlushAndFormat 测试JSON的Flush和GetFormat
|
||||
func TestJSONWriter_FlushAndFormat(t *testing.T) {
|
||||
dir := createTestDir(t)
|
||||
|
||||
+17
-1
@@ -15,10 +15,18 @@ import (
|
||||
// ResultOutput 全局输出管理器
|
||||
var ResultOutput *output.Manager
|
||||
|
||||
// StdoutWriter silent模式下的NDJSON stdout写入器
|
||||
var StdoutWriter *output.StdoutNDJSONWriter
|
||||
|
||||
// InitOutput 初始化输出系统
|
||||
func InitOutput() error {
|
||||
fv := GetFlagVars()
|
||||
|
||||
// silent模式:初始化NDJSON stdout写入器(独立于文件输出)
|
||||
if fv.Silent {
|
||||
StdoutWriter = output.NewStdoutNDJSONWriter()
|
||||
}
|
||||
|
||||
// 用户通过-no flag禁用保存时,跳过文件初始化避免不必要的资源开销
|
||||
if fv.DisableSave {
|
||||
return nil
|
||||
@@ -59,6 +67,9 @@ func InitOutput() error {
|
||||
|
||||
// CloseOutput 关闭输出系统
|
||||
func CloseOutput() error {
|
||||
if StdoutWriter != nil {
|
||||
_ = StdoutWriter.Close()
|
||||
}
|
||||
if ResultOutput == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -80,8 +91,13 @@ func SaveResult(result *output.ScanResult) error {
|
||||
"details": result.Details,
|
||||
})
|
||||
|
||||
// silent模式:NDJSON实时输出到stdout
|
||||
if StdoutWriter != nil {
|
||||
_ = StdoutWriter.WriteResult(result)
|
||||
}
|
||||
|
||||
// 用户禁用保存或输出未初始化时,跳过文件保存
|
||||
if GetFlagVars().DisableSave || ResultOutput == nil {
|
||||
if GetGlobalConfig().Output.DisableSave || ResultOutput == nil {
|
||||
return nil
|
||||
}
|
||||
return ResultOutput.SaveResult(result)
|
||||
|
||||
@@ -21,15 +21,6 @@ const (
|
||||
MaxPort = 65535
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// IP/主机解析常量
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// SimpleMaxHosts 最大主机数量限制
|
||||
SimpleMaxHosts = 10000
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 哈希验证常量
|
||||
// =============================================================================
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
const DefaultHostBatchSize = 4096
|
||||
|
||||
type HostIterator struct {
|
||||
sources []hostSource
|
||||
current hostSource
|
||||
exclude *hostMatcher
|
||||
}
|
||||
|
||||
func NewHostIterator(host string, filename string, nohosts ...string) (*HostIterator, error) {
|
||||
var sources []hostSource
|
||||
|
||||
if filename != "" {
|
||||
fileSrc, err := newFileHostSource(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, fileSrc)
|
||||
}
|
||||
|
||||
hostSources, err := newHostSources(host)
|
||||
if err != nil {
|
||||
closeHostSources(sources)
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, hostSources...)
|
||||
|
||||
matcher := newHostMatcher()
|
||||
for _, exclude := range nohosts {
|
||||
if strings.TrimSpace(exclude) == "" {
|
||||
continue
|
||||
}
|
||||
if err := matcher.add(exclude); err != nil {
|
||||
closeHostSources(sources)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &HostIterator{
|
||||
sources: sources,
|
||||
exclude: matcher,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (it *HostIterator) Close() error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
var firstErr error
|
||||
if it.current != nil {
|
||||
firstErr = it.current.Close()
|
||||
it.current = nil
|
||||
}
|
||||
for _, src := range it.sources {
|
||||
if err := src.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
it.sources = nil
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (it *HostIterator) Next() (string, bool, error) {
|
||||
for {
|
||||
if it.current == nil {
|
||||
if len(it.sources) == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
it.current = it.sources[0]
|
||||
it.sources = it.sources[1:]
|
||||
}
|
||||
|
||||
host, ok, err := it.current.Next()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if !ok {
|
||||
if err := it.current.Close(); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
it.current = nil
|
||||
continue
|
||||
}
|
||||
if it.exclude != nil && it.exclude.match(host) {
|
||||
continue
|
||||
}
|
||||
return host, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (it *HostIterator) NextBatch(ctx context.Context, size int) ([]string, error) {
|
||||
if size <= 0 {
|
||||
size = DefaultHostBatchSize
|
||||
}
|
||||
|
||||
batch := make([]string, 0, size)
|
||||
seen := make(map[string]struct{}, size)
|
||||
for len(batch) < size {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return batch, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
host, ok, err := it.Next()
|
||||
if err != nil {
|
||||
return batch, err
|
||||
}
|
||||
if !ok {
|
||||
return batch, nil
|
||||
}
|
||||
if _, exists := seen[host]; exists {
|
||||
continue
|
||||
}
|
||||
seen[host] = struct{}{}
|
||||
batch = append(batch, host)
|
||||
}
|
||||
return batch, nil
|
||||
}
|
||||
|
||||
type hostSource interface {
|
||||
Next() (string, bool, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type singleHostSource struct {
|
||||
host string
|
||||
done bool
|
||||
}
|
||||
|
||||
func (s *singleHostSource) Next() (string, bool, error) {
|
||||
if s.done {
|
||||
return "", false, nil
|
||||
}
|
||||
s.done = true
|
||||
return s.host, true, nil
|
||||
}
|
||||
|
||||
func (s *singleHostSource) Close() error { return nil }
|
||||
|
||||
type cidrHostSource struct {
|
||||
current uint32
|
||||
end uint32
|
||||
done bool
|
||||
}
|
||||
|
||||
func (s *cidrHostSource) Next() (string, bool, error) {
|
||||
if s.done || s.current > s.end {
|
||||
return "", false, nil
|
||||
}
|
||||
host := uint32ToIP(s.current)
|
||||
if s.current == s.end {
|
||||
s.done = true
|
||||
} else {
|
||||
s.current++
|
||||
}
|
||||
return host, true, nil
|
||||
}
|
||||
|
||||
func (s *cidrHostSource) Close() error { return nil }
|
||||
|
||||
type fileHostSource struct {
|
||||
file *os.File
|
||||
scanner *bufio.Scanner
|
||||
current hostSource
|
||||
}
|
||||
|
||||
func newFileHostSource(filename string) (*fileHostSource, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
src := &fileHostSource{
|
||||
file: file,
|
||||
scanner: bufio.NewScanner(file),
|
||||
}
|
||||
src.scanner.Buffer(make([]byte, 64*1024), 4*1024*1024)
|
||||
return src, nil
|
||||
}
|
||||
|
||||
func (s *fileHostSource) Next() (string, bool, error) {
|
||||
for {
|
||||
if s.current != nil {
|
||||
host, ok, err := s.current.Next()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if ok {
|
||||
return host, true, nil
|
||||
}
|
||||
_ = s.current.Close()
|
||||
s.current = nil
|
||||
}
|
||||
|
||||
if !s.scanner.Scan() {
|
||||
if err := s.scanner.Err(); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
line := strings.TrimSpace(s.scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
sources, err := newHostSources(line)
|
||||
if err != nil || len(sources) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(sources) == 1 {
|
||||
s.current = sources[0]
|
||||
continue
|
||||
}
|
||||
s.current = &multiHostSource{sources: sources}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *fileHostSource) Close() error {
|
||||
if s.current != nil {
|
||||
_ = s.current.Close()
|
||||
s.current = nil
|
||||
}
|
||||
if s.file == nil {
|
||||
return nil
|
||||
}
|
||||
err := s.file.Close()
|
||||
s.file = nil
|
||||
return err
|
||||
}
|
||||
|
||||
type multiHostSource struct {
|
||||
sources []hostSource
|
||||
current hostSource
|
||||
}
|
||||
|
||||
func (s *multiHostSource) Next() (string, bool, error) {
|
||||
for {
|
||||
if s.current == nil {
|
||||
if len(s.sources) == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
s.current = s.sources[0]
|
||||
s.sources = s.sources[1:]
|
||||
}
|
||||
host, ok, err := s.current.Next()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if ok {
|
||||
return host, true, nil
|
||||
}
|
||||
_ = s.current.Close()
|
||||
s.current = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *multiHostSource) Close() error {
|
||||
if s.current != nil {
|
||||
_ = s.current.Close()
|
||||
s.current = nil
|
||||
}
|
||||
closeHostSources(s.sources)
|
||||
s.sources = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func newHostSources(host string) ([]hostSource, error) {
|
||||
var sources []hostSource
|
||||
for _, h := range strings.Split(host, ",") {
|
||||
h = strings.TrimSpace(h)
|
||||
if h == "" {
|
||||
continue
|
||||
}
|
||||
src, err := newHostSource(h)
|
||||
if err != nil {
|
||||
closeHostSources(sources)
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, src)
|
||||
}
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func newHostSource(host string) (hostSource, error) {
|
||||
switch {
|
||||
case host == "192":
|
||||
return newCIDRHostSource("192.168.0.0/16")
|
||||
case host == "172":
|
||||
return newCIDRHostSource("172.16.0.0/12")
|
||||
case host == "10":
|
||||
return newCIDRHostSource("10.0.0.0/8")
|
||||
case strings.Contains(host, "/"):
|
||||
src, err := newCIDRHostSource(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(i18n.Tr("parser_cidr_failed", host)+": %w", err)
|
||||
}
|
||||
return src, nil
|
||||
case strings.Contains(host, "-") && !strings.Contains(host, ":") && looksLikeIPRange(host):
|
||||
src, err := newRangeHostSource(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(i18n.Tr("parser_ip_range_failed", host)+": %w", err)
|
||||
}
|
||||
return src, nil
|
||||
default:
|
||||
return &singleHostSource{host: host}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func newCIDRHostSource(cidr string) (hostSource, error) {
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
start, ok := ipToUint32(ipNet.IP)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s", i18n.GetText("parser_ipv4_only"))
|
||||
}
|
||||
ones, bits := ipNet.Mask.Size()
|
||||
if bits != 32 {
|
||||
return nil, fmt.Errorf("%s", i18n.GetText("parser_ipv4_only"))
|
||||
}
|
||||
size := uint64(1) << uint(32-ones)
|
||||
end := start + uint32(size-1)
|
||||
if size > 2 {
|
||||
start++
|
||||
end--
|
||||
}
|
||||
return &cidrHostSource{current: start, end: end}, nil
|
||||
}
|
||||
|
||||
func newRangeHostSource(rangeStr string) (hostSource, error) {
|
||||
parts := strings.Split(rangeStr, "-")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_ip_range_fmt", rangeStr))
|
||||
}
|
||||
|
||||
startIPStr := strings.TrimSpace(parts[0])
|
||||
endIPStr := strings.TrimSpace(parts[1])
|
||||
startIP := net.ParseIP(startIPStr)
|
||||
if startIP == nil {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_start_ip", startIPStr))
|
||||
}
|
||||
|
||||
if len(endIPStr) < 4 || !strings.Contains(endIPStr, ".") {
|
||||
endNum, err := strconv.Atoi(endIPStr)
|
||||
if err != nil || endNum > 255 {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_ip_end_val", endIPStr))
|
||||
}
|
||||
parts := strings.Split(startIPStr, ".")
|
||||
if len(parts) != 4 {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_ip_fmt", startIPStr))
|
||||
}
|
||||
parts[3] = strconv.Itoa(endNum)
|
||||
endIPStr = strings.Join(parts, ".")
|
||||
}
|
||||
|
||||
start, ok := ipToUint32(startIP)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s", i18n.GetText("parser_ipv4_only"))
|
||||
}
|
||||
end, ok := ipToUint32(net.ParseIP(endIPStr))
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_end_ip", endIPStr))
|
||||
}
|
||||
if start > end {
|
||||
return nil, fmt.Errorf("%s", i18n.GetText("parser_start_gt_end"))
|
||||
}
|
||||
return &cidrHostSource{current: start, end: end}, nil
|
||||
}
|
||||
|
||||
func closeHostSources(sources []hostSource) {
|
||||
for _, src := range sources {
|
||||
_ = src.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type hostMatcher struct {
|
||||
exact map[string]struct{}
|
||||
ranges []ipRange
|
||||
}
|
||||
|
||||
type ipRange struct {
|
||||
start uint32
|
||||
end uint32
|
||||
}
|
||||
|
||||
func newHostMatcher() *hostMatcher {
|
||||
return &hostMatcher{exact: make(map[string]struct{})}
|
||||
}
|
||||
|
||||
func (m *hostMatcher) add(input string) error {
|
||||
for _, h := range strings.Split(input, ",") {
|
||||
h = strings.TrimSpace(h)
|
||||
if h == "" {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case h == "192":
|
||||
if err := m.addCIDR("192.168.0.0/16"); err != nil {
|
||||
return err
|
||||
}
|
||||
case h == "172":
|
||||
if err := m.addCIDR("172.16.0.0/12"); err != nil {
|
||||
return err
|
||||
}
|
||||
case h == "10":
|
||||
if err := m.addCIDR("10.0.0.0/8"); err != nil {
|
||||
return err
|
||||
}
|
||||
case strings.Contains(h, "/"):
|
||||
if err := m.addCIDR(h); err != nil {
|
||||
return err
|
||||
}
|
||||
case strings.Contains(h, "-") && !strings.Contains(h, ":") && looksLikeIPRange(h):
|
||||
if err := m.addRange(h); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
m.exact[h] = struct{}{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *hostMatcher) addCIDR(cidr string) error {
|
||||
src, err := newCIDRHostSource(cidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rangeSrc, ok := src.(*cidrHostSource)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s", i18n.GetText("parser_ipv4_only"))
|
||||
}
|
||||
m.ranges = append(m.ranges, ipRange{start: rangeSrc.current, end: rangeSrc.end})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *hostMatcher) addRange(rangeStr string) error {
|
||||
src, err := newRangeHostSource(rangeStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rangeSrc, ok := src.(*cidrHostSource)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s", i18n.GetText("parser_ipv4_only"))
|
||||
}
|
||||
m.ranges = append(m.ranges, ipRange{start: rangeSrc.current, end: rangeSrc.end})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *hostMatcher) match(host string) bool {
|
||||
if _, ok := m.exact[host]; ok {
|
||||
return true
|
||||
}
|
||||
ip, ok := ipToUint32(net.ParseIP(host))
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, r := range m.ranges {
|
||||
if ip >= r.start && ip <= r.end {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ipToUint32(ip net.IP) (uint32, bool) {
|
||||
ip4 := ip.To4()
|
||||
if ip4 == nil {
|
||||
return 0, false
|
||||
}
|
||||
return uint32(ip4[0])<<24 | uint32(ip4[1])<<16 | uint32(ip4[2])<<8 | uint32(ip4[3]), true
|
||||
}
|
||||
|
||||
func uint32ToIP(v uint32) string {
|
||||
return fmt.Sprintf("%d.%d.%d.%d", byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHostIteratorCIDRBatch(t *testing.T) {
|
||||
iter, err := NewHostIterator("192.168.1.0/30", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("NewHostIterator error = %v", err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
batch, err := iter.NextBatch(context.Background(), 10)
|
||||
if err != nil {
|
||||
t.Fatalf("NextBatch error = %v", err)
|
||||
}
|
||||
|
||||
want := []string{"192.168.1.1", "192.168.1.2"}
|
||||
if !reflect.DeepEqual(batch, want) {
|
||||
t.Fatalf("batch = %#v, want %#v", batch, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostIteratorDoesNotExpandWholeRangeAtOnce(t *testing.T) {
|
||||
iter, err := NewHostIterator("10", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("NewHostIterator error = %v", err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
batch, err := iter.NextBatch(context.Background(), 3)
|
||||
if err != nil {
|
||||
t.Fatalf("NextBatch error = %v", err)
|
||||
}
|
||||
|
||||
want := []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"}
|
||||
if !reflect.DeepEqual(batch, want) {
|
||||
t.Fatalf("batch = %#v, want %#v", batch, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostIteratorExcludeCIDR(t *testing.T) {
|
||||
iter, err := NewHostIterator("192.168.1.0/29", "", "192.168.1.2-192.168.1.4")
|
||||
if err != nil {
|
||||
t.Fatalf("NewHostIterator error = %v", err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
batch, err := iter.NextBatch(context.Background(), 10)
|
||||
if err != nil {
|
||||
t.Fatalf("NextBatch error = %v", err)
|
||||
}
|
||||
|
||||
want := []string{"192.168.1.1", "192.168.1.5", "192.168.1.6"}
|
||||
if !reflect.DeepEqual(batch, want) {
|
||||
t.Fatalf("batch = %#v, want %#v", batch, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostIteratorAcceptsMultipleExcludeSources(t *testing.T) {
|
||||
iter, err := NewHostIterator("192.168.1.0/29", "", "192.168.1.2", "192.168.1.5")
|
||||
if err != nil {
|
||||
t.Fatalf("NewHostIterator error = %v", err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
batch, err := iter.NextBatch(context.Background(), 10)
|
||||
if err != nil {
|
||||
t.Fatalf("NextBatch error = %v", err)
|
||||
}
|
||||
|
||||
want := []string{"192.168.1.1", "192.168.1.3", "192.168.1.4", "192.168.1.6"}
|
||||
if !reflect.DeepEqual(batch, want) {
|
||||
t.Fatalf("batch = %#v, want %#v", batch, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostIteratorReadsLongHostFileLine(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := dir + "/hosts.txt"
|
||||
longPrefix := strings.Repeat("a", 70*1024)
|
||||
host := longPrefix + ".example.com"
|
||||
if err := os.WriteFile(path, []byte(host+"\n"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile error = %v", err)
|
||||
}
|
||||
|
||||
iter, err := NewHostIterator("", path)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHostIterator error = %v", err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
batch, err := iter.NextBatch(context.Background(), 1)
|
||||
if err != nil {
|
||||
t.Fatalf("NextBatch error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(batch, []string{host}) {
|
||||
t.Fatalf("batch = %#v, want long host", batch)
|
||||
}
|
||||
}
|
||||
@@ -590,6 +590,18 @@ func TestParseIP_IPRange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIP_IPRangeNoLimit(t *testing.T) {
|
||||
result, err := parseIPRangeString("192.168.1.1-5")
|
||||
if err != nil {
|
||||
t.Fatalf("parseIPRangeString error = %v", err)
|
||||
}
|
||||
|
||||
expected := []string{"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4", "192.168.1.5"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("parseIPRangeString = %v, want %v", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseIP_FromFile 测试从文件读取
|
||||
//
|
||||
// 验证:文件中的IP列表被正确读取
|
||||
@@ -684,6 +696,18 @@ func TestParseIP_Exclude(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIPMultipleExcludeSources(t *testing.T) {
|
||||
result, err := ParseIP("192.168.1.1-192.168.1.4", "", "192.168.1.2", "192.168.1.4")
|
||||
if err != nil {
|
||||
t.Fatalf("ParseIP error = %v", err)
|
||||
}
|
||||
|
||||
expected := []string{"192.168.1.1", "192.168.1.3"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Fatalf("ParseIP with multiple excludes = %v, want %v", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseIP_Deduplicate 测试去重
|
||||
func TestParseIP_Deduplicate(t *testing.T) {
|
||||
result, err := ParseIP("192.168.1.1,192.168.1.1,192.168.1.2,192.168.1.2", "", "")
|
||||
@@ -788,7 +812,9 @@ func TestParsePortRange(t *testing.T) {
|
||||
// TestExcludeHosts 测试排除主机
|
||||
func TestExcludeHosts(t *testing.T) {
|
||||
hosts := []string{"host1", "host2", "host3", "host4"}
|
||||
exclude := []string{"host2", "host4"}
|
||||
exclude := newHostMatcher()
|
||||
exclude.exact["host2"] = struct{}{}
|
||||
exclude.exact["host4"] = struct{}{}
|
||||
|
||||
result := excludeFromList(hosts, exclude)
|
||||
expected := []string{"host1", "host3"}
|
||||
@@ -803,7 +829,7 @@ func TestExcludeHosts(t *testing.T) {
|
||||
// TestExcludeHosts_EmptyExclude 测试空排除列表
|
||||
func TestExcludeHosts_EmptyExclude(t *testing.T) {
|
||||
hosts := []string{"host1", "host2"}
|
||||
result := excludeFromList(hosts, []string{})
|
||||
result := excludeFromList(hosts, nil)
|
||||
|
||||
if !reflect.DeepEqual(result, hosts) {
|
||||
t.Errorf("excludeFromList(空排除列表) 应该返回原列表")
|
||||
@@ -878,12 +904,6 @@ func TestParseIP_InternalNetworkShortcuts(t *testing.T) {
|
||||
100, // 172.16.0.0/12 应该很多
|
||||
"172.",
|
||||
},
|
||||
{
|
||||
"10简写",
|
||||
"10",
|
||||
100, // 10.0.0.0/8 应该很多
|
||||
"10.",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -969,6 +989,24 @@ func TestParseIP_FullIPRange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIP_FullIPRangeComplete(t *testing.T) {
|
||||
result, err := parseIPRangeString("192.168.1.1-192.168.1.5")
|
||||
if err != nil {
|
||||
t.Fatalf("parseIPRangeString error = %v", err)
|
||||
}
|
||||
|
||||
expected := []string{
|
||||
"192.168.1.1",
|
||||
"192.168.1.2",
|
||||
"192.168.1.3",
|
||||
"192.168.1.4",
|
||||
"192.168.1.5",
|
||||
}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("parseIPRangeString no limit = %v, want %v", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseIP_InvalidCIDR 测试无效CIDR
|
||||
func TestParseIP_InvalidCIDR(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -977,7 +1015,7 @@ func TestParseIP_InvalidCIDR(t *testing.T) {
|
||||
expectErr bool
|
||||
}{
|
||||
{"无效掩码/33", "192.168.1.0/33", true},
|
||||
{"无效掩码/0", "192.168.1.0/0", false}, // /0 技术上是有效的
|
||||
{"有效掩码/32", "192.168.1.1/32", false},
|
||||
{"格式错误", "192.168.1.0/abc", true},
|
||||
{"缺少掩码", "192.168.1.0/", true},
|
||||
}
|
||||
@@ -1173,7 +1211,7 @@ test:
|
||||
{"admin", "password123"},
|
||||
{"root", "toor"},
|
||||
{"user", "pass:with:colons"}, // 密码可以包含冒号
|
||||
{"test", ""}, // 空密码
|
||||
{"test", ""}, // 空密码
|
||||
}
|
||||
|
||||
if len(result) != len(tests) {
|
||||
|
||||
+42
-45
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -37,7 +38,7 @@ func ParseIP(host string, filename string, nohosts ...string) ([]string, error)
|
||||
if filename != "" {
|
||||
fileHosts, err := ReadLinesFromFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取主机文件失败: %w", err)
|
||||
return nil, fmt.Errorf(i18n.GetText("parser_read_hosts_failed")+": %w", err)
|
||||
}
|
||||
for _, h := range fileHosts {
|
||||
parsed, err := parseHostString(h)
|
||||
@@ -52,18 +53,27 @@ func ParseIP(host string, filename string, nohosts ...string) ([]string, error)
|
||||
if host != "" {
|
||||
hostList, err := parseHostString(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析主机失败: %w", err)
|
||||
return nil, fmt.Errorf(i18n.GetText("parser_parse_host_failed")+": %w", err)
|
||||
}
|
||||
hosts = append(hosts, hostList...)
|
||||
}
|
||||
|
||||
// 处理排除主机
|
||||
if len(nohosts) > 0 && nohosts[0] != "" {
|
||||
excludeList, err := parseHostString(nohosts[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析排除主机失败: %w", err)
|
||||
if len(nohosts) > 0 {
|
||||
matcher := newHostMatcher()
|
||||
hasExclude := false
|
||||
for _, exclude := range nohosts {
|
||||
if strings.TrimSpace(exclude) == "" {
|
||||
continue
|
||||
}
|
||||
hasExclude = true
|
||||
if err := matcher.add(exclude); err != nil {
|
||||
return nil, fmt.Errorf(i18n.GetText("parser_parse_exclude_failed")+": %w", err)
|
||||
}
|
||||
}
|
||||
if hasExclude {
|
||||
hosts = excludeFromList(hosts, matcher)
|
||||
}
|
||||
hosts = excludeFromList(hosts, excludeList)
|
||||
}
|
||||
|
||||
// 去重和排序
|
||||
@@ -71,7 +81,7 @@ func ParseIP(host string, filename string, nohosts ...string) ([]string, error)
|
||||
sort.Strings(hosts)
|
||||
|
||||
if len(hosts) == 0 {
|
||||
return nil, fmt.Errorf("没有找到有效的主机")
|
||||
return nil, fmt.Errorf("%s", i18n.GetText("parser_no_valid_hosts"))
|
||||
}
|
||||
|
||||
return hosts, nil
|
||||
@@ -89,33 +99,33 @@ func parseHostString(host string) ([]string, error) {
|
||||
|
||||
switch {
|
||||
case h == "192":
|
||||
cidrHosts, err := parseIPCIDR("192.168.0.0/16", SimpleMaxHosts)
|
||||
cidrHosts, err := parseIPCIDR("192.168.0.0/16")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hosts = append(hosts, cidrHosts...)
|
||||
case h == "172":
|
||||
cidrHosts, err := parseIPCIDR("172.16.0.0/12", SimpleMaxHosts)
|
||||
cidrHosts, err := parseIPCIDR("172.16.0.0/12")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hosts = append(hosts, cidrHosts...)
|
||||
case h == "10":
|
||||
cidrHosts, err := parseIPCIDR("10.0.0.0/8", SimpleMaxHosts)
|
||||
cidrHosts, err := parseIPCIDR("10.0.0.0/8")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hosts = append(hosts, cidrHosts...)
|
||||
case strings.Contains(h, "/"):
|
||||
cidrHosts, err := parseIPCIDR(h, SimpleMaxHosts)
|
||||
cidrHosts, err := parseIPCIDR(h)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CIDR解析失败 %s: %w", h, err)
|
||||
return nil, fmt.Errorf(i18n.Tr("parser_cidr_failed", h)+": %w", err)
|
||||
}
|
||||
hosts = append(hosts, cidrHosts...)
|
||||
case strings.Contains(h, "-") && !strings.Contains(h, ":") && looksLikeIPRange(h):
|
||||
rangeHosts, err := parseIPRangeString(h, SimpleMaxHosts)
|
||||
rangeHosts, err := parseIPRangeString(h)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("IP范围解析失败 %s: %w", h, err)
|
||||
return nil, fmt.Errorf(i18n.Tr("parser_ip_range_failed", h)+": %w", err)
|
||||
}
|
||||
hosts = append(hosts, rangeHosts...)
|
||||
default:
|
||||
@@ -211,6 +221,7 @@ func ReadLinesFromFile(filename string) ([]string, error) {
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Buffer(make([]byte, 64*1024), 4*1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line != "" && !strings.HasPrefix(line, "#") {
|
||||
@@ -288,7 +299,7 @@ func ParseHashFile(filename string) ([]string, [][]byte, error) {
|
||||
// =============================================================================
|
||||
|
||||
// parseIPCIDR 解析CIDR网段
|
||||
func parseIPCIDR(cidr string, maxTargets int) ([]string, error) {
|
||||
func parseIPCIDR(cidr string) ([]string, error) {
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -298,17 +309,11 @@ func parseIPCIDR(cidr string, maxTargets int) ([]string, error) {
|
||||
ip := make(net.IP, len(ipNet.IP))
|
||||
copy(ip, ipNet.IP)
|
||||
|
||||
count := 0
|
||||
for ipNet.Contains(ip) {
|
||||
ips = append(ips, ip.String())
|
||||
count++
|
||||
if count >= maxTargets {
|
||||
break
|
||||
}
|
||||
incrementIP(ip)
|
||||
}
|
||||
|
||||
// 移除网络地址和广播地址
|
||||
if len(ips) > 2 {
|
||||
ips = ips[1 : len(ips)-1]
|
||||
}
|
||||
@@ -330,10 +335,10 @@ func looksLikeIPRange(s string) bool {
|
||||
}
|
||||
|
||||
// parseIPRangeString 解析IP范围字符串
|
||||
func parseIPRangeString(rangeStr string, maxTargets int) ([]string, error) {
|
||||
func parseIPRangeString(rangeStr string) ([]string, error) {
|
||||
parts := strings.Split(rangeStr, "-")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("无效的IP范围格式: %s", rangeStr)
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_ip_range_fmt", rangeStr))
|
||||
}
|
||||
|
||||
startIPStr := strings.TrimSpace(parts[0])
|
||||
@@ -341,7 +346,7 @@ func parseIPRangeString(rangeStr string, maxTargets int) ([]string, error) {
|
||||
|
||||
startIP := net.ParseIP(startIPStr)
|
||||
if startIP == nil {
|
||||
return nil, fmt.Errorf("无效的起始IP地址: %s", startIPStr)
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_start_ip", startIPStr))
|
||||
}
|
||||
|
||||
// 处理简写格式 (如: 192.168.1.1-100)
|
||||
@@ -352,28 +357,28 @@ func parseIPRangeString(rangeStr string, maxTargets int) ([]string, error) {
|
||||
// 处理完整格式 (如: 192.168.1.1-192.168.1.100)
|
||||
endIP := net.ParseIP(endIPStr)
|
||||
if endIP == nil {
|
||||
return nil, fmt.Errorf("无效的结束IP地址: %s", endIPStr)
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_end_ip", endIPStr))
|
||||
}
|
||||
|
||||
return parseIPFullRange(startIP, endIP, maxTargets)
|
||||
return parseIPFullRange(startIP, endIP)
|
||||
}
|
||||
|
||||
// parseIPShortRange 解析短格式IP范围
|
||||
func parseIPShortRange(startIPStr, endSuffix string) ([]string, error) {
|
||||
endNum, err := strconv.Atoi(endSuffix)
|
||||
if err != nil || endNum > 255 {
|
||||
return nil, fmt.Errorf("无效的IP范围结束值: %s", endSuffix)
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_ip_end_val", endSuffix))
|
||||
}
|
||||
|
||||
ipParts := strings.Split(startIPStr, ".")
|
||||
if len(ipParts) != 4 {
|
||||
return nil, fmt.Errorf("无效的IP地址格式: %s", startIPStr)
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_ip_fmt", startIPStr))
|
||||
}
|
||||
|
||||
prefixIP := strings.Join(ipParts[0:3], ".")
|
||||
startNum, err := strconv.Atoi(ipParts[3])
|
||||
if err != nil || startNum > endNum {
|
||||
return nil, fmt.Errorf("无效的IP范围: %s-%s", startIPStr, endSuffix)
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_ip_range_val", startIPStr, endSuffix))
|
||||
}
|
||||
|
||||
var allIP []string
|
||||
@@ -385,30 +390,27 @@ func parseIPShortRange(startIPStr, endSuffix string) ([]string, error) {
|
||||
}
|
||||
|
||||
// parseIPFullRange 解析完整格式的IP范围
|
||||
func parseIPFullRange(startIP, endIP net.IP, maxTargets int) ([]string, error) {
|
||||
func parseIPFullRange(startIP, endIP net.IP) ([]string, error) {
|
||||
start4 := startIP.To4()
|
||||
end4 := endIP.To4()
|
||||
if start4 == nil || end4 == nil {
|
||||
return nil, fmt.Errorf("仅支持IPv4地址范围")
|
||||
return nil, fmt.Errorf("%s", i18n.GetText("parser_ipv4_only"))
|
||||
}
|
||||
|
||||
startInt := (int(start4[0]) << 24) | (int(start4[1]) << 16) | (int(start4[2]) << 8) | int(start4[3])
|
||||
endInt := (int(end4[0]) << 24) | (int(end4[1]) << 16) | (int(end4[2]) << 8) | int(end4[3])
|
||||
|
||||
if startInt > endInt {
|
||||
return nil, fmt.Errorf("起始IP大于结束IP")
|
||||
return nil, fmt.Errorf("%s", i18n.GetText("parser_start_gt_end"))
|
||||
}
|
||||
|
||||
var ips []string
|
||||
current := make(net.IP, len(start4))
|
||||
copy(current, start4)
|
||||
|
||||
count := 0
|
||||
for {
|
||||
ips = append(ips, current.String())
|
||||
count++
|
||||
|
||||
if current.Equal(end4) || count >= maxTargets {
|
||||
if current.Equal(end4) {
|
||||
break
|
||||
}
|
||||
incrementIP(current)
|
||||
@@ -428,19 +430,14 @@ func incrementIP(ip net.IP) {
|
||||
}
|
||||
|
||||
// excludeFromList 从列表中排除指定项
|
||||
func excludeFromList(hosts, excludeList []string) []string {
|
||||
if len(excludeList) == 0 {
|
||||
func excludeFromList(hosts []string, matcher *hostMatcher) []string {
|
||||
if matcher == nil {
|
||||
return hosts
|
||||
}
|
||||
|
||||
excludeMap := make(map[string]struct{}, len(excludeList))
|
||||
for _, e := range excludeList {
|
||||
excludeMap[e] = struct{}{}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(hosts))
|
||||
for _, h := range hosts {
|
||||
if _, found := excludeMap[h]; !found {
|
||||
if !matcher.match(h) {
|
||||
result = append(result, h)
|
||||
}
|
||||
}
|
||||
|
||||
+68
-47
@@ -28,8 +28,8 @@ ProgressManager.go - 固定底部进度条管理器
|
||||
type ProgressManager struct {
|
||||
mu sync.RWMutex
|
||||
enabled bool
|
||||
total int64
|
||||
current int64
|
||||
total atomic.Int64
|
||||
current atomic.Int64
|
||||
description string
|
||||
startTime time.Time
|
||||
isActive bool
|
||||
@@ -48,6 +48,10 @@ type ProgressManager struct {
|
||||
|
||||
// 进度条更新控制(减少 Windows 终端的重复输出)
|
||||
lastRenderedPercent int
|
||||
|
||||
// 引用,避免读全局
|
||||
state *State
|
||||
noColor bool
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -102,17 +106,19 @@ func GetProgressManager() *ProgressManager {
|
||||
|
||||
// InitProgress 初始化进度条
|
||||
func (pm *ProgressManager) InitProgress(total int64, description string) {
|
||||
fv := GetFlagVars()
|
||||
if fv.DisableProgress || fv.Silent {
|
||||
cfg := GetGlobalConfig()
|
||||
if cfg.Output.DisableProgress || cfg.Output.Silent {
|
||||
pm.enabled = false
|
||||
return
|
||||
}
|
||||
pm.state = GetGlobalState()
|
||||
pm.noColor = cfg.Output.NoColor
|
||||
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
pm.total = total
|
||||
pm.current = 0
|
||||
pm.total.Store(total)
|
||||
pm.current.Store(0)
|
||||
pm.description = description
|
||||
pm.startTime = time.Now()
|
||||
pm.isActive = true
|
||||
@@ -137,16 +143,24 @@ func (pm *ProgressManager) UpdateProgress(increment int64) {
|
||||
return
|
||||
}
|
||||
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
pm.current += increment
|
||||
if pm.current > pm.total {
|
||||
pm.current = pm.total
|
||||
// 原子累加,避免高并发下的锁竞争
|
||||
newCurrent := pm.current.Add(increment)
|
||||
if newCurrent > pm.total.Load() {
|
||||
pm.current.Store(pm.total.Load())
|
||||
}
|
||||
|
||||
// 更新活跃时间
|
||||
pm.lastActivity = time.Now()
|
||||
// 节流渲染:距上次渲染不足 50ms 则跳过
|
||||
now := time.Now()
|
||||
pm.mu.RLock()
|
||||
lastAct := pm.lastActivity
|
||||
pm.mu.RUnlock()
|
||||
if now.Sub(lastAct) < 50*time.Millisecond {
|
||||
return
|
||||
}
|
||||
|
||||
pm.mu.Lock()
|
||||
pm.lastActivity = now
|
||||
pm.mu.Unlock()
|
||||
|
||||
pm.renderProgress()
|
||||
}
|
||||
@@ -164,7 +178,7 @@ func (pm *ProgressManager) FinishProgress() {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
pm.current = pm.total
|
||||
pm.current.Store(pm.total.Load())
|
||||
pm.renderProgress()
|
||||
|
||||
// 停止活跃指示器
|
||||
@@ -205,20 +219,21 @@ func (pm *ProgressManager) generateProgressBar() string {
|
||||
// 获取发包统计
|
||||
packetInfo := pm.getPacketInfo()
|
||||
|
||||
if pm.total == 0 {
|
||||
if pm.total.Load() == 0 {
|
||||
spinner := pm.getActivityIndicator()
|
||||
base := fmt.Sprintf("%s %s 等待中...", pm.description, spinner)
|
||||
base := fmt.Sprintf("%s %s %s", pm.description, spinner, i18n.GetText("progress_waiting"))
|
||||
if packetInfo != "" {
|
||||
return base + " " + packetInfo
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
percentage := float64(pm.current) / float64(pm.total) * 100
|
||||
percentage := float64(pm.current.Load()) / float64(pm.total.Load()) * 100
|
||||
elapsed := time.Since(pm.startTime)
|
||||
current := pm.current.Load()
|
||||
|
||||
// 计算速度
|
||||
speed := float64(pm.current) / elapsed.Seconds()
|
||||
speed := float64(current) / elapsed.Seconds()
|
||||
speedStr := ""
|
||||
if speed > 0 {
|
||||
speedStr = fmt.Sprintf(" %.0f/s", speed)
|
||||
@@ -226,8 +241,8 @@ func (pm *ProgressManager) generateProgressBar() string {
|
||||
|
||||
// 计算预估剩余时间
|
||||
var eta string
|
||||
if pm.current > 0 && pm.current < pm.total {
|
||||
totalTime := elapsed * time.Duration(pm.total) / time.Duration(pm.current)
|
||||
if current > 0 && current < pm.total.Load() {
|
||||
totalTime := elapsed * time.Duration(pm.total.Load()) / time.Duration(current)
|
||||
remaining := totalTime - elapsed
|
||||
if remaining > 0 {
|
||||
eta = fmt.Sprintf(" ETA:%s", formatDuration(remaining))
|
||||
@@ -239,7 +254,7 @@ func (pm *ProgressManager) generateProgressBar() string {
|
||||
|
||||
// 计算固定部分的宽度
|
||||
fixedPart := fmt.Sprintf("%s %s %5.1f%% [] (%d/%d)%s%s %s",
|
||||
pm.description, spinner, percentage, pm.current, pm.total, speedStr, eta, packetInfo)
|
||||
pm.description, spinner, percentage, current, pm.total.Load(), speedStr, eta, packetInfo)
|
||||
fixedWidth := displayWidth(fixedPart)
|
||||
|
||||
// 计算进度条槽位可用宽度(预留2字符余量)
|
||||
@@ -266,7 +281,7 @@ func (pm *ProgressManager) generateProgressBar() string {
|
||||
|
||||
// 构建最终进度条
|
||||
result := fmt.Sprintf("%s %s %5.1f%% %s (%d/%d)%s%s",
|
||||
pm.description, spinner, percentage, bar, pm.current, pm.total, speedStr, eta)
|
||||
pm.description, spinner, percentage, bar, current, pm.total.Load(), speedStr, eta)
|
||||
|
||||
if packetInfo != "" {
|
||||
result += " " + packetInfo
|
||||
@@ -277,13 +292,16 @@ func (pm *ProgressManager) generateProgressBar() string {
|
||||
|
||||
// getPacketInfo 获取发包统计信息(简化版)
|
||||
func (pm *ProgressManager) getPacketInfo() string {
|
||||
packetCount := GetGlobalState().GetPacketCount()
|
||||
if pm.state == nil {
|
||||
return ""
|
||||
}
|
||||
packetCount := pm.state.GetPacketCount()
|
||||
if packetCount == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
tcpSuccess := GetGlobalState().GetTCPSuccessPacketCount()
|
||||
tcpFailed := GetGlobalState().GetTCPFailedPacketCount()
|
||||
tcpSuccess := pm.state.GetTCPSuccessPacketCount()
|
||||
tcpFailed := pm.state.GetTCPFailedPacketCount()
|
||||
|
||||
// 简化格式:TCP:成功/失败
|
||||
if tcpSuccess > 0 || tcpFailed > 0 {
|
||||
@@ -301,13 +319,15 @@ func (pm *ProgressManager) showCompletionInfo() {
|
||||
fmt.Print("\n")
|
||||
|
||||
completionMsg := i18n.GetText("progress_scan_completed")
|
||||
if GetFlagVars().NoColor {
|
||||
fmt.Printf("[完成] %s %d/%d (耗时: %s)\n",
|
||||
completionMsg, pm.total, pm.total, formatDuration(elapsed))
|
||||
doneMsg := i18n.GetText("progress_done")
|
||||
durationMsg := i18n.GetText("progress_duration")
|
||||
if pm.noColor {
|
||||
fmt.Printf("[%s] %s %d/%d (%s: %s)\n",
|
||||
doneMsg, completionMsg, pm.total.Load(), pm.total.Load(), durationMsg, formatDuration(elapsed))
|
||||
} else {
|
||||
fmt.Printf("%s[完成] %s %d/%d%s %s(耗时: %s)%s\n",
|
||||
AnsiGreen, completionMsg, pm.total, pm.total, AnsiReset,
|
||||
AnsiGray, formatDuration(elapsed), AnsiReset)
|
||||
fmt.Printf("%s[%s] %s %d/%d%s %s(%s: %s)%s\n",
|
||||
AnsiGreen, doneMsg, completionMsg, pm.total.Load(), pm.total.Load(), AnsiReset,
|
||||
AnsiGray, durationMsg, formatDuration(elapsed), AnsiReset)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,10 +478,10 @@ func (pm *ProgressManager) GetPercent() float64 {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
|
||||
if !pm.isActive || pm.total == 0 {
|
||||
if !pm.isActive || pm.total.Load() == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(pm.current) / float64(pm.total) * 100
|
||||
return float64(pm.current.Load()) / float64(pm.total.Load()) * 100
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -470,6 +490,10 @@ func (pm *ProgressManager) GetPercent() float64 {
|
||||
|
||||
// LogWithProgress 在进度条活跃时协调日志输出
|
||||
func LogWithProgress(message string) {
|
||||
if cfg := GetGlobalConfig(); cfg != nil && cfg.Output.Silent {
|
||||
return
|
||||
}
|
||||
|
||||
pm := GetProgressManager()
|
||||
if !pm.IsActive() {
|
||||
// 如果进度条不活跃,直接输出
|
||||
@@ -498,8 +522,8 @@ func (pm *ProgressManager) renderProgressUnsafe() {
|
||||
|
||||
// 计算当前百分比(避免除零)
|
||||
currentPercent := 0
|
||||
if pm.total > 0 {
|
||||
currentPercent = int((pm.current * 100) / pm.total)
|
||||
if pm.total.Load() > 0 {
|
||||
currentPercent = int((pm.current.Load() * 100) / pm.total.Load())
|
||||
}
|
||||
|
||||
// 只在百分比变化时更新,减少不必要的渲染
|
||||
@@ -532,7 +556,7 @@ func (pm *ProgressManager) renderProgressUnsafe() {
|
||||
fmt.Print(clearStr)
|
||||
|
||||
// 输出进度条(带颜色,如果启用)
|
||||
if GetFlagVars().NoColor {
|
||||
if pm.noColor {
|
||||
fmt.Print(progressBar)
|
||||
} else {
|
||||
fmt.Printf("%s%s%s", AnsiCyan, progressBar, AnsiReset)
|
||||
@@ -618,8 +642,8 @@ ConcurrencyMonitor - 并发监控器
|
||||
// ConcurrencyMonitor 并发监控器
|
||||
type ConcurrencyMonitor struct {
|
||||
// 主扫描器层级
|
||||
activePluginTasks int64 // 当前活跃的插件任务数
|
||||
totalPluginTasks int64 // 总插件任务数
|
||||
activePluginTasks atomic.Int64 // 当前活跃的插件任务数
|
||||
totalPluginTasks atomic.Int64 // 总插件任务数
|
||||
|
||||
// 插件内连接层级已移除 - 原代码为死代码,无任何调用者
|
||||
}
|
||||
@@ -634,10 +658,7 @@ var (
|
||||
// GetConcurrencyMonitor 获取全局并发监控器
|
||||
func GetConcurrencyMonitor() *ConcurrencyMonitor {
|
||||
concurrencyMutex.Do(func() {
|
||||
globalConcurrencyMonitor = &ConcurrencyMonitor{
|
||||
activePluginTasks: 0,
|
||||
totalPluginTasks: 0,
|
||||
}
|
||||
globalConcurrencyMonitor = &ConcurrencyMonitor{}
|
||||
})
|
||||
return globalConcurrencyMonitor
|
||||
}
|
||||
@@ -648,18 +669,18 @@ func GetConcurrencyMonitor() *ConcurrencyMonitor {
|
||||
|
||||
// StartPluginTask 开始插件任务
|
||||
func (m *ConcurrencyMonitor) StartPluginTask() {
|
||||
atomic.AddInt64(&m.activePluginTasks, 1)
|
||||
atomic.AddInt64(&m.totalPluginTasks, 1)
|
||||
m.activePluginTasks.Add(1)
|
||||
m.totalPluginTasks.Add(1)
|
||||
}
|
||||
|
||||
// FinishPluginTask 完成插件任务
|
||||
func (m *ConcurrencyMonitor) FinishPluginTask() {
|
||||
atomic.AddInt64(&m.activePluginTasks, -1)
|
||||
m.activePluginTasks.Add(-1)
|
||||
}
|
||||
|
||||
// GetPluginTaskStats 获取插件任务统计
|
||||
func (m *ConcurrencyMonitor) GetPluginTaskStats() (active int64, total int64) {
|
||||
return atomic.LoadInt64(&m.activePluginTasks), atomic.LoadInt64(&m.totalPluginTasks)
|
||||
return m.activePluginTasks.Load(), m.totalPluginTasks.Load()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
+18
-16
@@ -2,6 +2,8 @@ package proxy
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -151,41 +153,41 @@ const (
|
||||
// 错误消息常量
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
var (
|
||||
// ErrMsgUnsupportedProxyType Manager错误消息 - 不支持的代理类型
|
||||
ErrMsgUnsupportedProxyType = "不支持的代理类型"
|
||||
ErrMsgUnsupportedProxyType = i18n.GetText("proxy_unsupported_type")
|
||||
// ErrMsgEmptyConfig 配置不能为空
|
||||
ErrMsgEmptyConfig = "配置不能为空"
|
||||
ErrMsgEmptyConfig = i18n.GetText("proxy_empty_config")
|
||||
|
||||
// ErrMsgSOCKS5ParseFailed SOCKS5错误消息 - 地址解析失败
|
||||
ErrMsgSOCKS5ParseFailed = "SOCKS5代理地址解析失败"
|
||||
ErrMsgSOCKS5ParseFailed = i18n.GetText("proxy_socks5_parse_failed")
|
||||
// ErrMsgSOCKS5CreateFailed 拨号器创建失败
|
||||
ErrMsgSOCKS5CreateFailed = "SOCKS5拨号器创建失败"
|
||||
ErrMsgSOCKS5CreateFailed = i18n.GetText("proxy_socks5_create_failed")
|
||||
// ErrMsgSOCKS5ConnTimeout 连接超时
|
||||
ErrMsgSOCKS5ConnTimeout = "SOCKS5连接超时"
|
||||
ErrMsgSOCKS5ConnTimeout = i18n.GetText("proxy_socks5_conn_timeout")
|
||||
// ErrMsgSOCKS5ConnFailed 连接失败
|
||||
ErrMsgSOCKS5ConnFailed = "SOCKS5连接失败"
|
||||
ErrMsgSOCKS5ConnFailed = i18n.GetText("proxy_socks5_conn_failed")
|
||||
|
||||
// ErrMsgDirectConnFailed 直连错误消息 - 直连失败
|
||||
ErrMsgDirectConnFailed = "直连失败"
|
||||
ErrMsgDirectConnFailed = i18n.GetText("proxy_direct_conn_failed")
|
||||
|
||||
// ErrMsgHTTPConnFailed HTTP代理错误消息 - 连接失败
|
||||
ErrMsgHTTPConnFailed = "连接HTTP代理服务器失败"
|
||||
ErrMsgHTTPConnFailed = i18n.GetText("proxy_http_conn_failed")
|
||||
// ErrMsgHTTPSetWriteTimeout 设置写超时失败
|
||||
ErrMsgHTTPSetWriteTimeout = "设置写超时失败"
|
||||
ErrMsgHTTPSetWriteTimeout = i18n.GetText("proxy_http_set_write_timeout")
|
||||
// ErrMsgHTTPSendConnectFail 发送CONNECT请求失败
|
||||
ErrMsgHTTPSendConnectFail = "发送CONNECT请求失败"
|
||||
ErrMsgHTTPSendConnectFail = i18n.GetText("proxy_http_send_connect_failed")
|
||||
// ErrMsgHTTPSetReadTimeout 设置读超时失败
|
||||
ErrMsgHTTPSetReadTimeout = "设置读超时失败"
|
||||
ErrMsgHTTPSetReadTimeout = i18n.GetText("proxy_http_set_read_timeout")
|
||||
// ErrMsgHTTPReadRespFailed 读取响应失败
|
||||
ErrMsgHTTPReadRespFailed = "读取HTTP响应失败"
|
||||
ErrMsgHTTPReadRespFailed = i18n.GetText("proxy_http_read_response_failed")
|
||||
// ErrMsgHTTPProxyAuthFailed 代理认证失败
|
||||
ErrMsgHTTPProxyAuthFailed = "HTTP代理连接失败,状态码: %d"
|
||||
ErrMsgHTTPProxyAuthFailed = i18n.GetText("proxy_http_status_failed")
|
||||
|
||||
// ErrMsgTLSTCPConnFailed TLS错误消息 - TCP连接失败
|
||||
ErrMsgTLSTCPConnFailed = "建立TCP连接失败"
|
||||
ErrMsgTLSTCPConnFailed = i18n.GetText("proxy_tls_tcp_conn_failed")
|
||||
// ErrMsgTLSHandshakeFailed TLS握手失败
|
||||
ErrMsgTLSHandshakeFailed = "TLS握手失败"
|
||||
ErrMsgTLSHandshakeFailed = i18n.GetText("proxy_tls_handshake_failed")
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -19,6 +19,9 @@ var (
|
||||
|
||||
// proxyProbed 标记代理是否已经探测过(避免重复探测)
|
||||
proxyProbed atomic.Bool
|
||||
|
||||
// currentProxyType 当前代理类型
|
||||
currentProxyType atomic.Int32
|
||||
)
|
||||
|
||||
// SetProxyEnabled 设置代理启用状态
|
||||
@@ -61,6 +64,11 @@ func IsProxyProbed() bool {
|
||||
return proxyProbed.Load()
|
||||
}
|
||||
|
||||
// IsSOCKS5Proxy 检查当前代理是否为SOCKS5类型
|
||||
func IsSOCKS5Proxy() bool {
|
||||
return proxyEnabled.Load() && ProxyType(currentProxyType.Load()) == ProxyTypeSOCKS5
|
||||
}
|
||||
|
||||
// AutoConfigureProxy 自动配置代理相关行为
|
||||
// 根据代理类型和状态自动调整扫描策略
|
||||
func AutoConfigureProxy(config *ProxyConfig) {
|
||||
@@ -74,6 +82,7 @@ func AutoConfigureProxy(config *ProxyConfig) {
|
||||
|
||||
// 启用代理标记
|
||||
SetProxyEnabled(true)
|
||||
currentProxyType.Store(int32(config.Type))
|
||||
|
||||
// SOCKS5代理默认假设非标准(后续由探测函数验证)
|
||||
if config.Type == ProxyTypeSOCKS5 {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -24,27 +23,27 @@ func (h *httpDialer) Dial(network, address string) (net.Conn, error) {
|
||||
|
||||
func (h *httpDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
start := time.Now()
|
||||
atomic.AddInt64(&h.stats.TotalConnections, 1)
|
||||
h.stats.addTotal(1)
|
||||
|
||||
// 连接到HTTP代理服务器
|
||||
proxyConn, err := h.baseDial.DialContext(ctx, NetworkTCP, h.config.Address)
|
||||
if err != nil {
|
||||
atomic.AddInt64(&h.stats.FailedConnections, 1)
|
||||
h.stats.LastError = err.Error()
|
||||
h.stats.addFailed(1)
|
||||
h.stats.setLastError(err.Error())
|
||||
return nil, NewProxyError(ErrTypeConnection, ErrMsgHTTPConnFailed, ErrCodeHTTPConnFailed, err)
|
||||
}
|
||||
|
||||
// 发送CONNECT请求
|
||||
if err := h.sendConnectRequest(proxyConn, address); err != nil {
|
||||
_ = proxyConn.Close() // 错误处理路径,Close错误可忽略
|
||||
atomic.AddInt64(&h.stats.FailedConnections, 1)
|
||||
h.stats.LastError = err.Error()
|
||||
h.stats.addFailed(1)
|
||||
h.stats.setLastError(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
h.stats.LastConnectTime = start
|
||||
atomic.AddInt64(&h.stats.ActiveConnections, 1)
|
||||
h.stats.setLastConnectTime(start)
|
||||
h.stats.addActive(1)
|
||||
h.updateAverageConnectTime(duration)
|
||||
|
||||
return &trackedConn{
|
||||
@@ -108,7 +107,8 @@ func (h *httpDialer) sendConnectRequest(conn net.Conn, address string) error {
|
||||
|
||||
// updateAverageConnectTime 更新平均连接时间
|
||||
func (h *httpDialer) updateAverageConnectTime(duration time.Duration) {
|
||||
// 简单的移动平均
|
||||
h.stats.mu.Lock()
|
||||
defer h.stats.mu.Unlock()
|
||||
if h.stats.AverageConnectTime == 0 {
|
||||
h.stats.AverageConnectTime = duration
|
||||
} else {
|
||||
|
||||
+26
-37
@@ -4,9 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
@@ -128,9 +126,7 @@ func (m *manager) Stats() *ProxyStats {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
// 返回副本以避免并发问题
|
||||
statsCopy := *m.stats
|
||||
return &statsCopy
|
||||
return m.stats.snapshot()
|
||||
}
|
||||
|
||||
// createDirectDialer 创建直连拨号器
|
||||
@@ -146,6 +142,9 @@ func (m *manager) createDirectDialer() Dialer {
|
||||
func (m *manager) createSOCKS5Dialer() (Dialer, error) {
|
||||
// 检查缓存
|
||||
cacheKey := fmt.Sprintf(CacheKeySOCKS5, m.config.Address)
|
||||
if m.config.Username != "" || m.config.Password != "" {
|
||||
cacheKey = fmt.Sprintf("%s_%s_%s", cacheKey, m.config.Username, m.config.Password)
|
||||
}
|
||||
m.cacheMu.RLock()
|
||||
if time.Now().Before(m.cacheExpiry) {
|
||||
if cached, exists := m.dialerCache[cacheKey]; exists {
|
||||
@@ -155,18 +154,6 @@ func (m *manager) createSOCKS5Dialer() (Dialer, error) {
|
||||
}
|
||||
m.cacheMu.RUnlock()
|
||||
|
||||
// 解析代理地址
|
||||
proxyURL := fmt.Sprintf(SOCKS5URLFormat, m.config.Address)
|
||||
if m.config.Username != "" {
|
||||
proxyURL = fmt.Sprintf(SOCKS5URLAuthFormat,
|
||||
m.config.Username, m.config.Password, m.config.Address)
|
||||
}
|
||||
|
||||
u, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return nil, NewProxyError(ErrTypeConfig, ErrMsgSOCKS5ParseFailed, ErrCodeSOCKS5ParseFailed, err)
|
||||
}
|
||||
|
||||
// 创建基础拨号器
|
||||
baseDial := &net.Dialer{
|
||||
Timeout: m.config.Timeout,
|
||||
@@ -175,16 +162,14 @@ func (m *manager) createSOCKS5Dialer() (Dialer, error) {
|
||||
|
||||
// 创建SOCKS5拨号器
|
||||
var auth *proxy.Auth
|
||||
if u.User != nil {
|
||||
if m.config.Username != "" || m.config.Password != "" {
|
||||
auth = &proxy.Auth{
|
||||
User: u.User.Username(),
|
||||
}
|
||||
if password, hasPassword := u.User.Password(); hasPassword {
|
||||
auth.Password = password
|
||||
User: m.config.Username,
|
||||
Password: m.config.Password,
|
||||
}
|
||||
}
|
||||
|
||||
socksDialer, err := proxy.SOCKS5(NetworkTCP, u.Host, auth, baseDial)
|
||||
socksDialer, err := proxy.SOCKS5(NetworkTCP, m.config.Address, auth, baseDial)
|
||||
if err != nil {
|
||||
return nil, NewProxyError(ErrTypeConnection, ErrMsgSOCKS5CreateFailed, ErrCodeSOCKS5CreateFailed, err)
|
||||
}
|
||||
@@ -248,7 +233,7 @@ func (d *directDialer) Dial(network, address string) (net.Conn, error) {
|
||||
|
||||
func (d *directDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
start := time.Now()
|
||||
atomic.AddInt64(&d.stats.TotalConnections, 1)
|
||||
d.stats.addTotal(1)
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: d.timeout,
|
||||
@@ -264,15 +249,16 @@ func (d *directDialer) DialContext(ctx context.Context, network, address string)
|
||||
conn, err := dialer.DialContext(ctx, network, address)
|
||||
|
||||
duration := time.Since(start)
|
||||
d.stats.LastConnectTime = start
|
||||
|
||||
d.stats.setLastConnectTime(start)
|
||||
|
||||
if err != nil {
|
||||
atomic.AddInt64(&d.stats.FailedConnections, 1)
|
||||
d.stats.LastError = err.Error()
|
||||
d.stats.addFailed(1)
|
||||
d.stats.setLastError(err.Error())
|
||||
return nil, NewProxyError(ErrTypeConnection, ErrMsgDirectConnFailed, ErrCodeDirectConnFailed, err)
|
||||
}
|
||||
|
||||
atomic.AddInt64(&d.stats.ActiveConnections, 1)
|
||||
d.stats.addActive(1)
|
||||
d.updateAverageConnectTime(duration)
|
||||
|
||||
return &trackedConn{
|
||||
@@ -294,7 +280,7 @@ func (s *socks5Dialer) Dial(network, address string) (net.Conn, error) {
|
||||
|
||||
func (s *socks5Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
start := time.Now()
|
||||
atomic.AddInt64(&s.stats.TotalConnections, 1)
|
||||
s.stats.addTotal(1)
|
||||
|
||||
// 创建一个带超时的上下文
|
||||
dialCtx, cancel := context.WithTimeout(ctx, s.config.Timeout)
|
||||
@@ -322,20 +308,21 @@ func (s *socks5Dialer) DialContext(ctx context.Context, network, address string)
|
||||
|
||||
select {
|
||||
case <-dialCtx.Done():
|
||||
atomic.AddInt64(&s.stats.FailedConnections, 1)
|
||||
s.stats.LastError = dialCtx.Err().Error()
|
||||
s.stats.addFailed(1)
|
||||
s.stats.setLastError(dialCtx.Err().Error())
|
||||
return nil, NewProxyError(ErrTypeTimeout, ErrMsgSOCKS5ConnTimeout, ErrCodeSOCKS5ConnTimeout, dialCtx.Err())
|
||||
case result := <-connChan:
|
||||
duration := time.Since(start)
|
||||
s.stats.LastConnectTime = start
|
||||
|
||||
s.stats.setLastConnectTime(start)
|
||||
|
||||
if result.err != nil {
|
||||
atomic.AddInt64(&s.stats.FailedConnections, 1)
|
||||
s.stats.LastError = result.err.Error()
|
||||
s.stats.addFailed(1)
|
||||
s.stats.setLastError(result.err.Error())
|
||||
return nil, NewProxyError(ErrTypeConnection, ErrMsgSOCKS5ConnFailed, ErrCodeSOCKS5ConnFailed, result.err)
|
||||
}
|
||||
|
||||
atomic.AddInt64(&s.stats.ActiveConnections, 1)
|
||||
s.stats.addActive(1)
|
||||
s.updateAverageConnectTime(duration)
|
||||
|
||||
return &trackedConn{
|
||||
@@ -347,7 +334,8 @@ func (s *socks5Dialer) DialContext(ctx context.Context, network, address string)
|
||||
|
||||
// updateAverageConnectTime 更新平均连接时间
|
||||
func (d *directDialer) updateAverageConnectTime(duration time.Duration) {
|
||||
// 简单的移动平均
|
||||
d.stats.mu.Lock()
|
||||
defer d.stats.mu.Unlock()
|
||||
if d.stats.AverageConnectTime == 0 {
|
||||
d.stats.AverageConnectTime = duration
|
||||
} else {
|
||||
@@ -356,7 +344,8 @@ func (d *directDialer) updateAverageConnectTime(duration time.Duration) {
|
||||
}
|
||||
|
||||
func (s *socks5Dialer) updateAverageConnectTime(duration time.Duration) {
|
||||
// 简单的移动平均
|
||||
s.stats.mu.Lock()
|
||||
defer s.stats.mu.Unlock()
|
||||
if s.stats.AverageConnectTime == 0 {
|
||||
s.stats.AverageConnectTime = duration
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -242,6 +245,73 @@ func TestGetDialer_HTTPS(t *testing.T) {
|
||||
t.Logf("✓ GetDialer 返回HTTPS代理拨号器")
|
||||
}
|
||||
|
||||
func TestGetDialer_SOCKS5AuthSpecialChars(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen failed: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
type credential struct {
|
||||
user string
|
||||
pass string
|
||||
}
|
||||
authCh := make(chan credential, 1)
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
user, pass, err := handleTestSOCKS5Auth(conn)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
authCh <- credential{user: user, pass: pass}
|
||||
}()
|
||||
|
||||
origProbed := IsProxyProbed()
|
||||
SetProxyProbed(true)
|
||||
defer SetProxyProbed(origProbed)
|
||||
|
||||
config := &ProxyConfig{
|
||||
Type: ProxyTypeSOCKS5,
|
||||
Address: ln.Addr().String(),
|
||||
Username: "user",
|
||||
Password: "p@ss:word#1",
|
||||
Timeout: time.Second,
|
||||
}
|
||||
manager := NewProxyManager(config)
|
||||
dialer, err := manager.GetDialer()
|
||||
if err != nil {
|
||||
t.Fatalf("GetDialer failed: %v", err)
|
||||
}
|
||||
|
||||
conn, err := dialer.Dial("tcp", "127.0.0.1:80")
|
||||
if err != nil {
|
||||
t.Fatalf("SOCKS5 dial failed: %v", err)
|
||||
}
|
||||
_ = conn.Close()
|
||||
|
||||
select {
|
||||
case got := <-authCh:
|
||||
if got.user != config.Username || got.pass != config.Password {
|
||||
t.Fatalf("auth = %q/%q, want %q/%q", got.user, got.pass, config.Username, config.Password)
|
||||
}
|
||||
case err := <-errCh:
|
||||
t.Fatalf("SOCKS5 test server failed: %v", err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for SOCKS5 auth")
|
||||
}
|
||||
|
||||
t.Logf("✓ SOCKS5认证支持特殊字符密码")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GetTLSDialer - TLS拨号器获取测试
|
||||
// =============================================================================
|
||||
@@ -559,3 +629,89 @@ func TestDirectDialer_LocalAddr_Loopback(t *testing.T) {
|
||||
t.Logf("✓ LocalAddr 绑定正常工作(连接失败是预期的): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleTestSOCKS5Auth(conn net.Conn) (string, string, error) {
|
||||
header := make([]byte, 2)
|
||||
if _, err := io.ReadFull(conn, header); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if header[0] != 0x05 {
|
||||
return "", "", fmt.Errorf("unexpected socks version: %d", header[0])
|
||||
}
|
||||
methods := make([]byte, int(header[1]))
|
||||
if _, err := io.ReadFull(conn, methods); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
hasAuth := false
|
||||
for _, method := range methods {
|
||||
if method == 0x02 {
|
||||
hasAuth = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasAuth {
|
||||
return "", "", fmt.Errorf("client did not offer username/password auth")
|
||||
}
|
||||
if _, err := conn.Write([]byte{0x05, 0x02}); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
authHeader := make([]byte, 2)
|
||||
if _, err := io.ReadFull(conn, authHeader); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if authHeader[0] != 0x01 {
|
||||
return "", "", fmt.Errorf("unexpected auth version: %d", authHeader[0])
|
||||
}
|
||||
userBytes := make([]byte, int(authHeader[1]))
|
||||
if _, err := io.ReadFull(conn, userBytes); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
passLen := make([]byte, 1)
|
||||
if _, err := io.ReadFull(conn, passLen); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
passBytes := make([]byte, int(passLen[0]))
|
||||
if _, err := io.ReadFull(conn, passBytes); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if _, err := conn.Write([]byte{0x01, 0x00}); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
reqHeader := make([]byte, 4)
|
||||
if _, err := io.ReadFull(conn, reqHeader); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if reqHeader[0] != 0x05 || reqHeader[1] != 0x01 {
|
||||
return "", "", fmt.Errorf("unexpected request header: %v", reqHeader)
|
||||
}
|
||||
if err := discardSOCKS5Address(conn, reqHeader[3]); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if _, err := conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return string(userBytes), string(passBytes), nil
|
||||
}
|
||||
|
||||
func discardSOCKS5Address(conn net.Conn, atyp byte) error {
|
||||
switch atyp {
|
||||
case 0x01:
|
||||
_, err := io.CopyN(io.Discard, conn, 6)
|
||||
return err
|
||||
case 0x03:
|
||||
length := make([]byte, 1)
|
||||
if _, err := io.ReadFull(conn, length); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := io.CopyN(io.Discard, conn, int64(length[0])+2)
|
||||
return err
|
||||
case 0x04:
|
||||
_, err := io.CopyN(io.Discard, conn, 18)
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("unsupported atyp: %d", atyp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ func (t *tlsDialerWrapper) DialTLSContext(ctx context.Context, network, address
|
||||
// 进行TLS握手
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
_ = tcpConn.Close() // TLS握手失败,Close错误可忽略
|
||||
atomic.AddInt64(&t.stats.FailedConnections, 1)
|
||||
t.stats.LastError = err.Error()
|
||||
t.stats.addFailed(1)
|
||||
t.stats.setLastError(err.Error())
|
||||
return nil, NewProxyError(ErrTypeConnection, ErrMsgTLSHandshakeFailed, ErrCodeTLSHandshakeFailed, err)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@ func (t *tlsDialerWrapper) DialTLSContext(ctx context.Context, network, address
|
||||
|
||||
// updateAverageConnectTime 更新平均连接时间
|
||||
func (t *tlsDialerWrapper) updateAverageConnectTime(duration time.Duration) {
|
||||
// 简单的移动平均
|
||||
t.stats.mu.Lock()
|
||||
defer t.stats.mu.Unlock()
|
||||
if t.stats.AverageConnectTime == 0 {
|
||||
t.stats.AverageConnectTime = duration
|
||||
} else {
|
||||
@@ -81,16 +82,16 @@ func (t *tlsDialerWrapper) updateAverageConnectTime(duration time.Duration) {
|
||||
|
||||
// trackedConn 带统计的连接
|
||||
type trackedConn struct {
|
||||
bytesSent atomic.Int64
|
||||
bytesRecv atomic.Int64
|
||||
net.Conn
|
||||
stats *ProxyStats
|
||||
bytesSent int64
|
||||
bytesRecv int64
|
||||
stats *ProxyStats
|
||||
}
|
||||
|
||||
func (tc *trackedConn) Read(b []byte) (n int, err error) {
|
||||
n, err = tc.Conn.Read(b)
|
||||
if n > 0 {
|
||||
atomic.AddInt64(&tc.bytesRecv, int64(n))
|
||||
tc.bytesRecv.Add(int64(n))
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
@@ -98,13 +99,13 @@ func (tc *trackedConn) Read(b []byte) (n int, err error) {
|
||||
func (tc *trackedConn) Write(b []byte) (n int, err error) {
|
||||
n, err = tc.Conn.Write(b)
|
||||
if n > 0 {
|
||||
atomic.AddInt64(&tc.bytesSent, int64(n))
|
||||
tc.bytesSent.Add(int64(n))
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (tc *trackedConn) Close() error {
|
||||
atomic.AddInt64(&tc.stats.ActiveConnections, -1)
|
||||
tc.stats.addActive(-1)
|
||||
return tc.Conn.Close()
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -98,6 +99,7 @@ type ProxyStats struct {
|
||||
TotalConnections int64 `json:"total_connections"`
|
||||
ActiveConnections int64 `json:"active_connections"`
|
||||
FailedConnections int64 `json:"failed_connections"`
|
||||
mu sync.Mutex `json:"-"`
|
||||
AverageConnectTime time.Duration `json:"average_connect_time"`
|
||||
LastConnectTime time.Time `json:"last_connect_time"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
@@ -105,6 +107,52 @@ type ProxyStats struct {
|
||||
ProxyAddress string `json:"proxy_address"`
|
||||
}
|
||||
|
||||
func (s *ProxyStats) addTotal(delta int64) {
|
||||
s.mu.Lock()
|
||||
s.TotalConnections += delta
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *ProxyStats) addActive(delta int64) {
|
||||
s.mu.Lock()
|
||||
s.ActiveConnections += delta
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *ProxyStats) addFailed(delta int64) {
|
||||
s.mu.Lock()
|
||||
s.FailedConnections += delta
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *ProxyStats) setLastConnectTime(t time.Time) {
|
||||
s.mu.Lock()
|
||||
s.LastConnectTime = t
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *ProxyStats) setLastError(err string) {
|
||||
s.mu.Lock()
|
||||
s.LastError = err
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *ProxyStats) snapshot() *ProxyStats {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return &ProxyStats{
|
||||
TotalConnections: s.TotalConnections,
|
||||
ActiveConnections: s.ActiveConnections,
|
||||
FailedConnections: s.FailedConnections,
|
||||
AverageConnectTime: s.AverageConnectTime,
|
||||
LastConnectTime: s.LastConnectTime,
|
||||
LastError: s.LastError,
|
||||
ProxyType: s.ProxyType,
|
||||
ProxyAddress: s.ProxyAddress,
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyError 代理错误类型
|
||||
//
|
||||
//nolint:revive // 保持与现有代码的向后兼容性
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/common/output"
|
||||
"github.com/shadow1ng/fscan/common/proxy"
|
||||
)
|
||||
|
||||
// ResultSink receives structured scan results for one scan session.
|
||||
type ResultSink func(result *output.ScanResult) error
|
||||
|
||||
// ScanSession 封装单次扫描的全部上下文
|
||||
// 一次扫描一个 session,并发扫描各自独立
|
||||
type ScanSession struct {
|
||||
Config *Config // 不可变,创建后只读
|
||||
State *State // 可变,原子操作,每会话独立
|
||||
Params *FlagVars // 原始参数,只读
|
||||
ResultSink ResultSink // 可选,覆盖全局输出
|
||||
PauseGate func(ctx context.Context) error
|
||||
|
||||
// 每会话 dialer(按 timeout 懒初始化,取决于代理配置)
|
||||
dialerMu sync.Mutex
|
||||
dialers map[time.Duration]proxy.Dialer
|
||||
dialerErrs map[time.Duration]error
|
||||
}
|
||||
|
||||
// NewScanSession 从已构建的 Config、State 和 FlagVars 创建会话
|
||||
func NewScanSession(config *Config, state *State, params *FlagVars) *ScanSession {
|
||||
return &ScanSession{
|
||||
Config: config,
|
||||
State: state,
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
|
||||
// SaveResult saves a scan result through the session sink if present, otherwise
|
||||
// falls back to the process-wide output pipeline used by the CLI.
|
||||
func (s *ScanSession) SaveResult(result *output.ScanResult) error {
|
||||
if s != nil && s.ResultSink != nil {
|
||||
return s.ResultSink(result)
|
||||
}
|
||||
return SaveResult(result)
|
||||
}
|
||||
|
||||
func (s *ScanSession) loggingEnabled() bool {
|
||||
return s == nil || s.Config == nil || !s.Config.Output.Silent
|
||||
}
|
||||
|
||||
// LogDebug writes through the session's logging policy.
|
||||
func (s *ScanSession) LogDebug(msg string) {
|
||||
if s.loggingEnabled() {
|
||||
LogDebug(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// LogInfo writes through the session's logging policy.
|
||||
func (s *ScanSession) LogInfo(msg string) {
|
||||
if s.loggingEnabled() {
|
||||
LogInfo(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// LogSuccess writes through the session's logging policy.
|
||||
func (s *ScanSession) LogSuccess(result string) {
|
||||
if s.loggingEnabled() {
|
||||
LogSuccess(result)
|
||||
}
|
||||
}
|
||||
|
||||
// LogVuln writes through the session's logging policy.
|
||||
func (s *ScanSession) LogVuln(result string) {
|
||||
if s.loggingEnabled() {
|
||||
LogVuln(result)
|
||||
}
|
||||
}
|
||||
|
||||
// LogError writes through the session's logging policy.
|
||||
func (s *ScanSession) LogError(errMsg string) {
|
||||
if s.loggingEnabled() {
|
||||
LogError(errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// DialTCP 创建 TCP 连接,内含限速检查、代理、计数
|
||||
func (s *ScanSession) DialTCP(ctx context.Context, network, address string, timeout time.Duration) (net.Conn, error) {
|
||||
// 检查发包限制
|
||||
if ok, err := CanSendPacketWith(s.Config, s.State); !ok {
|
||||
s.LogError(i18n.Tr("tcp_connection_restricted", address, err.Error()))
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", err.Error()))
|
||||
}
|
||||
|
||||
// 获取 dialer
|
||||
dialer, err := s.getDialer(timeout)
|
||||
if err != nil {
|
||||
s.LogError(i18n.Tr("proxy_dialer_failed", err))
|
||||
s.State.IncrementTCPFailedPacketCount()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := dialer.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
s.State.IncrementTCPFailedPacketCount()
|
||||
s.LogDebug(i18n.Tr("connection_failed", address, err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// SO_LINGER=0: 连接关闭时立即发送 RST,避免 TIME_WAIT 堆积
|
||||
if tc, ok := conn.(*net.TCPConn); ok {
|
||||
_ = tc.SetLinger(0)
|
||||
}
|
||||
|
||||
s.State.IncrementTCPSuccessPacketCount()
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// DialUDP creates a connected UDP socket with rate limiting and packet counting.
|
||||
// UDP cannot be proxied; if a proxy is configured the connection is made directly.
|
||||
func (s *ScanSession) DialUDP(ctx context.Context, address string, timeout time.Duration) (net.Conn, error) {
|
||||
if ok, err := CanSendPacketWith(s.Config, s.State); !ok {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", err.Error()))
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("udp", address, timeout)
|
||||
if err != nil {
|
||||
s.State.IncrementUDPPacketCount()
|
||||
return nil, err
|
||||
}
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
s.State.IncrementUDPPacketCount()
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// HTTPDo executes an HTTP request with the session's packet limits and counters.
|
||||
func (s *ScanSession) HTTPDo(client *http.Client, req *http.Request) (*http.Response, error) {
|
||||
if ok, err := CanSendPacketWith(s.Config, s.State); !ok {
|
||||
s.LogError(i18n.Tr("http_request_restricted", req.URL.String(), err.Error()))
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", err.Error()))
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
s.State.IncrementTCPFailedPacketCount()
|
||||
return nil, err
|
||||
}
|
||||
s.State.IncrementTCPSuccessPacketCount()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ProxyEnabled reports whether this scan session uses a network proxy.
|
||||
func (s *ScanSession) ProxyEnabled() bool {
|
||||
if s == nil || s.Config == nil {
|
||||
return false
|
||||
}
|
||||
return s.Config.Network.Socks5Proxy != "" || s.Config.Network.HTTPProxy != ""
|
||||
}
|
||||
|
||||
// IsSOCKS5Proxy reports whether this scan session uses SOCKS5.
|
||||
func (s *ScanSession) IsSOCKS5Proxy() bool {
|
||||
return s != nil && s.Config != nil && s.Config.Network.Socks5Proxy != ""
|
||||
}
|
||||
|
||||
// ProxyReliable reports whether the session proxy should be treated as reliable.
|
||||
func (s *ScanSession) ProxyReliable() bool {
|
||||
if !s.ProxyEnabled() || !s.IsSOCKS5Proxy() {
|
||||
return true
|
||||
}
|
||||
return proxy.IsProxyReliable()
|
||||
}
|
||||
|
||||
func (s *ScanSession) getDialer(timeout time.Duration) (proxy.Dialer, error) {
|
||||
if timeout <= 0 {
|
||||
timeout = s.Config.Timeout
|
||||
}
|
||||
|
||||
s.dialerMu.Lock()
|
||||
defer s.dialerMu.Unlock()
|
||||
|
||||
if s.dialers == nil {
|
||||
s.dialers = make(map[time.Duration]proxy.Dialer)
|
||||
s.dialerErrs = make(map[time.Duration]error)
|
||||
}
|
||||
if dialer, ok := s.dialers[timeout]; ok {
|
||||
return dialer, s.dialerErrs[timeout]
|
||||
}
|
||||
|
||||
cfg := s.createProxyConfig(timeout)
|
||||
manager := proxy.NewProxyManager(cfg)
|
||||
dialer, err := manager.GetDialer()
|
||||
s.dialers[timeout] = dialer
|
||||
s.dialerErrs[timeout] = err
|
||||
return dialer, err
|
||||
}
|
||||
|
||||
func (s *ScanSession) createProxyConfig(timeout time.Duration) *proxy.ProxyConfig {
|
||||
cfg := proxy.DefaultProxyConfig()
|
||||
cfg.Timeout = timeout
|
||||
cfg.LocalAddr = s.Config.Network.Iface
|
||||
|
||||
// 优先 SOCKS5
|
||||
if s.Config.Network.Socks5Proxy != "" {
|
||||
cfg.Type = proxy.ProxyTypeSOCKS5
|
||||
socks5URL := s.Config.Network.Socks5Proxy
|
||||
if !strings.HasPrefix(socks5URL, "socks5://") {
|
||||
socks5URL = "socks5://" + socks5URL
|
||||
}
|
||||
cfg.Address, cfg.Username, cfg.Password = parseProxyURL(socks5URL, s.Config.Network.Socks5Proxy)
|
||||
return cfg
|
||||
}
|
||||
|
||||
// 其次 HTTP
|
||||
if s.Config.Network.HTTPProxy != "" {
|
||||
if strings.HasPrefix(s.Config.Network.HTTPProxy, "https://") {
|
||||
cfg.Type = proxy.ProxyTypeHTTPS
|
||||
} else {
|
||||
cfg.Type = proxy.ProxyTypeHTTP
|
||||
}
|
||||
cfg.Address, cfg.Username, cfg.Password = parseProxyURL(s.Config.Network.HTTPProxy, s.Config.Network.HTTPProxy)
|
||||
return cfg
|
||||
}
|
||||
|
||||
cfg.Type = proxy.ProxyTypeNone
|
||||
return cfg
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestScanSessionLogMethodsHonorSilentConfig(t *testing.T) {
|
||||
loggerMu.Lock()
|
||||
silentLoggerRefs = 0
|
||||
resetLoggerLocked()
|
||||
loggerMu.Unlock()
|
||||
t.Cleanup(func() {
|
||||
loggerMu.Lock()
|
||||
silentLoggerRefs = 0
|
||||
resetLoggerLocked()
|
||||
loggerMu.Unlock()
|
||||
})
|
||||
|
||||
cfg := NewConfig()
|
||||
cfg.Output.Silent = true
|
||||
session := NewScanSession(cfg, NewState(), &FlagVars{})
|
||||
|
||||
session.LogDebug("debug")
|
||||
session.LogInfo("info")
|
||||
session.LogSuccess("success")
|
||||
session.LogVuln("vuln")
|
||||
session.LogError("error")
|
||||
|
||||
loggerMu.Lock()
|
||||
defer loggerMu.Unlock()
|
||||
if globalLogger != nil {
|
||||
t.Fatal("silent session log methods initialized global logger")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSessionDialerCacheIsTimeoutAware(t *testing.T) {
|
||||
cfg := NewConfig()
|
||||
cfg.Timeout = 5 * time.Second
|
||||
session := NewScanSession(cfg, NewState(), &FlagVars{})
|
||||
|
||||
shortTimeout := 100 * time.Millisecond
|
||||
longTimeout := 2 * time.Second
|
||||
|
||||
shortDialer, err := session.getDialer(shortTimeout)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shortDialerAgain, err := session.getDialer(shortTimeout)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
longDialer, err := session.getDialer(longTimeout)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if shortDialer != shortDialerAgain {
|
||||
t.Fatal("same timeout should reuse the session dialer")
|
||||
}
|
||||
if shortDialer == longDialer {
|
||||
t.Fatal("different timeouts should not share one session dialer")
|
||||
}
|
||||
if got := session.createProxyConfig(shortTimeout).Timeout; got != shortTimeout {
|
||||
t.Fatalf("proxy timeout = %v, want %v", got, shortTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSessionHTTPDoUsesSessionState(t *testing.T) {
|
||||
previousState := GetGlobalState()
|
||||
globalState := NewState()
|
||||
SetGlobalState(globalState)
|
||||
t.Cleanup(func() { SetGlobalState(previousState) })
|
||||
|
||||
sessionState := NewState()
|
||||
session := NewScanSession(NewConfig(), sessionState, &FlagVars{})
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNoContent,
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodHead, "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := session.HTTPDo(client, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
|
||||
if got := sessionState.GetTCPSuccessPacketCount(); got != 1 {
|
||||
t.Fatalf("session TCP success count = %d, want 1", got)
|
||||
}
|
||||
if got := globalState.GetTCPSuccessPacketCount(); got != 0 {
|
||||
t.Fatalf("global TCP success count = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSessionProxyStateComesFromConfig(t *testing.T) {
|
||||
direct := NewScanSession(NewConfig(), NewState(), &FlagVars{})
|
||||
if direct.ProxyEnabled() {
|
||||
t.Fatal("direct session should not report proxy enabled")
|
||||
}
|
||||
if direct.IsSOCKS5Proxy() {
|
||||
t.Fatal("direct session should not report SOCKS5")
|
||||
}
|
||||
if !direct.ProxyReliable() {
|
||||
t.Fatal("direct session should be reliable")
|
||||
}
|
||||
|
||||
httpCfg := NewConfig()
|
||||
httpCfg.Network.HTTPProxy = "http://127.0.0.1:8080"
|
||||
httpSession := NewScanSession(httpCfg, NewState(), &FlagVars{})
|
||||
if !httpSession.ProxyEnabled() {
|
||||
t.Fatal("HTTP proxy session should report proxy enabled")
|
||||
}
|
||||
if httpSession.IsSOCKS5Proxy() {
|
||||
t.Fatal("HTTP proxy session should not report SOCKS5")
|
||||
}
|
||||
if !httpSession.ProxyReliable() {
|
||||
t.Fatal("HTTP proxy session should be reliable")
|
||||
}
|
||||
|
||||
socksCfg := NewConfig()
|
||||
socksCfg.Network.Socks5Proxy = "127.0.0.1:1080"
|
||||
socksSession := NewScanSession(socksCfg, NewState(), &FlagVars{})
|
||||
if !socksSession.ProxyEnabled() {
|
||||
t.Fatal("SOCKS5 proxy session should report proxy enabled")
|
||||
}
|
||||
if !socksSession.IsSOCKS5Proxy() {
|
||||
t.Fatal("SOCKS5 proxy session should report SOCKS5")
|
||||
}
|
||||
}
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req)
|
||||
}
|
||||
+49
-49
@@ -23,17 +23,17 @@ state.go - 运行时状态管理
|
||||
// State 扫描器运行时状态 - 线程安全
|
||||
type State struct {
|
||||
// 计数器 - 原子操作
|
||||
packetCount int64
|
||||
tcpPacketCount int64
|
||||
tcpSuccessPacketCount int64
|
||||
tcpFailedPacketCount int64
|
||||
udpPacketCount int64
|
||||
httpPacketCount int64
|
||||
resourceExhaustedCount int64
|
||||
packetCount atomic.Int64
|
||||
tcpPacketCount atomic.Int64
|
||||
tcpSuccessPacketCount atomic.Int64
|
||||
tcpFailedPacketCount atomic.Int64
|
||||
udpPacketCount atomic.Int64
|
||||
httpPacketCount atomic.Int64
|
||||
resourceExhaustedCount atomic.Int64
|
||||
|
||||
// 任务计数
|
||||
end int64
|
||||
num int64
|
||||
end atomic.Int64
|
||||
num atomic.Int64
|
||||
|
||||
// 时间
|
||||
startTime time.Time
|
||||
@@ -71,38 +71,38 @@ func NewState() *State {
|
||||
|
||||
// IncrementPacketCount 增加总包计数
|
||||
func (s *State) IncrementPacketCount() int64 {
|
||||
return atomic.AddInt64(&s.packetCount, 1)
|
||||
return s.packetCount.Add(1)
|
||||
}
|
||||
|
||||
// IncrementTCPSuccessPacketCount 增加TCP成功连接包计数
|
||||
func (s *State) IncrementTCPSuccessPacketCount() int64 {
|
||||
atomic.AddInt64(&s.tcpSuccessPacketCount, 1)
|
||||
atomic.AddInt64(&s.tcpPacketCount, 1)
|
||||
return atomic.AddInt64(&s.packetCount, 1)
|
||||
s.tcpSuccessPacketCount.Add(1)
|
||||
s.tcpPacketCount.Add(1)
|
||||
return s.packetCount.Add(1)
|
||||
}
|
||||
|
||||
// IncrementTCPFailedPacketCount 增加TCP失败连接包计数
|
||||
func (s *State) IncrementTCPFailedPacketCount() int64 {
|
||||
atomic.AddInt64(&s.tcpFailedPacketCount, 1)
|
||||
atomic.AddInt64(&s.tcpPacketCount, 1)
|
||||
return atomic.AddInt64(&s.packetCount, 1)
|
||||
s.tcpFailedPacketCount.Add(1)
|
||||
s.tcpPacketCount.Add(1)
|
||||
return s.packetCount.Add(1)
|
||||
}
|
||||
|
||||
// IncrementUDPPacketCount 增加UDP包计数
|
||||
func (s *State) IncrementUDPPacketCount() int64 {
|
||||
atomic.AddInt64(&s.udpPacketCount, 1)
|
||||
return atomic.AddInt64(&s.packetCount, 1)
|
||||
s.udpPacketCount.Add(1)
|
||||
return s.packetCount.Add(1)
|
||||
}
|
||||
|
||||
// IncrementHTTPPacketCount 增加HTTP包计数
|
||||
func (s *State) IncrementHTTPPacketCount() int64 {
|
||||
atomic.AddInt64(&s.httpPacketCount, 1)
|
||||
return atomic.AddInt64(&s.packetCount, 1)
|
||||
s.httpPacketCount.Add(1)
|
||||
return s.packetCount.Add(1)
|
||||
}
|
||||
|
||||
// IncrementResourceExhaustedCount 增加资源耗尽错误计数
|
||||
func (s *State) IncrementResourceExhaustedCount() {
|
||||
atomic.AddInt64(&s.resourceExhaustedCount, 1)
|
||||
s.resourceExhaustedCount.Add(1)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -111,48 +111,48 @@ func (s *State) IncrementResourceExhaustedCount() {
|
||||
|
||||
// GetPacketCount 获取总包计数
|
||||
func (s *State) GetPacketCount() int64 {
|
||||
return atomic.LoadInt64(&s.packetCount)
|
||||
return s.packetCount.Load()
|
||||
}
|
||||
|
||||
// GetTCPPacketCount 获取TCP包计数
|
||||
func (s *State) GetTCPPacketCount() int64 {
|
||||
return atomic.LoadInt64(&s.tcpPacketCount)
|
||||
return s.tcpPacketCount.Load()
|
||||
}
|
||||
|
||||
// GetTCPSuccessPacketCount 获取TCP成功连接包计数
|
||||
func (s *State) GetTCPSuccessPacketCount() int64 {
|
||||
return atomic.LoadInt64(&s.tcpSuccessPacketCount)
|
||||
return s.tcpSuccessPacketCount.Load()
|
||||
}
|
||||
|
||||
// GetTCPFailedPacketCount 获取TCP失败连接包计数
|
||||
func (s *State) GetTCPFailedPacketCount() int64 {
|
||||
return atomic.LoadInt64(&s.tcpFailedPacketCount)
|
||||
return s.tcpFailedPacketCount.Load()
|
||||
}
|
||||
|
||||
// GetUDPPacketCount 获取UDP包计数
|
||||
func (s *State) GetUDPPacketCount() int64 {
|
||||
return atomic.LoadInt64(&s.udpPacketCount)
|
||||
return s.udpPacketCount.Load()
|
||||
}
|
||||
|
||||
// GetHTTPPacketCount 获取HTTP包计数
|
||||
func (s *State) GetHTTPPacketCount() int64 {
|
||||
return atomic.LoadInt64(&s.httpPacketCount)
|
||||
return s.httpPacketCount.Load()
|
||||
}
|
||||
|
||||
// GetResourceExhaustedCount 获取资源耗尽错误计数
|
||||
func (s *State) GetResourceExhaustedCount() int64 {
|
||||
return atomic.LoadInt64(&s.resourceExhaustedCount)
|
||||
return s.resourceExhaustedCount.Load()
|
||||
}
|
||||
|
||||
// ResetPacketCounters 重置所有包计数器
|
||||
func (s *State) ResetPacketCounters() {
|
||||
atomic.StoreInt64(&s.packetCount, 0)
|
||||
atomic.StoreInt64(&s.tcpPacketCount, 0)
|
||||
atomic.StoreInt64(&s.tcpSuccessPacketCount, 0)
|
||||
atomic.StoreInt64(&s.tcpFailedPacketCount, 0)
|
||||
atomic.StoreInt64(&s.udpPacketCount, 0)
|
||||
atomic.StoreInt64(&s.httpPacketCount, 0)
|
||||
atomic.StoreInt64(&s.resourceExhaustedCount, 0)
|
||||
s.packetCount.Store(0)
|
||||
s.tcpPacketCount.Store(0)
|
||||
s.tcpSuccessPacketCount.Store(0)
|
||||
s.tcpFailedPacketCount.Store(0)
|
||||
s.udpPacketCount.Store(0)
|
||||
s.httpPacketCount.Store(0)
|
||||
s.resourceExhaustedCount.Store(0)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -161,32 +161,32 @@ func (s *State) ResetPacketCounters() {
|
||||
|
||||
// GetEnd 获取结束计数
|
||||
func (s *State) GetEnd() int64 {
|
||||
return atomic.LoadInt64(&s.end)
|
||||
return s.end.Load()
|
||||
}
|
||||
|
||||
// GetNum 获取数量计数
|
||||
func (s *State) GetNum() int64 {
|
||||
return atomic.LoadInt64(&s.num)
|
||||
return s.num.Load()
|
||||
}
|
||||
|
||||
// IncrementEnd 增加结束计数
|
||||
func (s *State) IncrementEnd() int64 {
|
||||
return atomic.AddInt64(&s.end, 1)
|
||||
return s.end.Add(1)
|
||||
}
|
||||
|
||||
// IncrementNum 增加数量计数
|
||||
func (s *State) IncrementNum() int64 {
|
||||
return atomic.AddInt64(&s.num, 1)
|
||||
return s.num.Add(1)
|
||||
}
|
||||
|
||||
// SetEnd 设置结束计数
|
||||
func (s *State) SetEnd(val int64) {
|
||||
atomic.StoreInt64(&s.end, val)
|
||||
s.end.Store(val)
|
||||
}
|
||||
|
||||
// SetNum 设置数量计数
|
||||
func (s *State) SetNum(val int64) {
|
||||
atomic.StoreInt64(&s.num, val)
|
||||
s.num.Store(val)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -271,10 +271,10 @@ type PerfStatsData struct {
|
||||
func (s *State) GetPerfStats() PerfStatsData {
|
||||
duration := time.Since(s.startTime)
|
||||
durationMs := duration.Milliseconds()
|
||||
totalPackets := atomic.LoadInt64(&s.packetCount)
|
||||
tcpSuccess := atomic.LoadInt64(&s.tcpSuccessPacketCount)
|
||||
tcpFailed := atomic.LoadInt64(&s.tcpFailedPacketCount)
|
||||
tcpTotal := atomic.LoadInt64(&s.tcpPacketCount)
|
||||
totalPackets := s.packetCount.Load()
|
||||
tcpSuccess := s.tcpSuccessPacketCount.Load()
|
||||
tcpFailed := s.tcpFailedPacketCount.Load()
|
||||
tcpTotal := s.tcpPacketCount.Load()
|
||||
|
||||
var pps float64
|
||||
if durationMs > 0 {
|
||||
@@ -291,13 +291,13 @@ func (s *State) GetPerfStats() PerfStatsData {
|
||||
TCPPackets: tcpTotal,
|
||||
TCPSuccess: tcpSuccess,
|
||||
TCPFailed: tcpFailed,
|
||||
UDPPackets: atomic.LoadInt64(&s.udpPacketCount),
|
||||
HTTPPackets: atomic.LoadInt64(&s.httpPacketCount),
|
||||
ResourceExhausted: atomic.LoadInt64(&s.resourceExhaustedCount),
|
||||
UDPPackets: s.udpPacketCount.Load(),
|
||||
HTTPPackets: s.httpPacketCount.Load(),
|
||||
ResourceExhausted: s.resourceExhaustedCount.Load(),
|
||||
ScanDurationMs: durationMs,
|
||||
PacketsPerSecond: pps,
|
||||
SuccessRate: successRate,
|
||||
TargetsScanned: atomic.LoadInt64(&s.num),
|
||||
TargetsScanned: s.num.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-8
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/panjf2000/ants/v2"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// AdaptivePool 自适应线程池
|
||||
@@ -23,7 +24,7 @@ type AdaptivePool struct {
|
||||
|
||||
// 监控参数
|
||||
checkInterval time.Duration
|
||||
lastCheck time.Time
|
||||
lastCheckNano atomic.Int64 // UnixNano
|
||||
lastExhaustedCount int64
|
||||
lastPacketCount int64
|
||||
|
||||
@@ -67,20 +68,22 @@ func (ap *AdaptivePool) Invoke(task interface{}) error {
|
||||
}
|
||||
|
||||
// maybeAdjust 检查并可能调整线程池大小
|
||||
// 使用原子 CAS 进行时间检查,99%+ 的调用零锁开销
|
||||
func (ap *AdaptivePool) maybeAdjust() {
|
||||
now := time.Now()
|
||||
|
||||
ap.mu.Lock()
|
||||
if now.Sub(ap.lastCheck) < ap.checkInterval {
|
||||
ap.mu.Unlock()
|
||||
lastCheck := ap.lastCheckNano.Load()
|
||||
now := time.Now().UnixNano()
|
||||
if now-lastCheck < int64(ap.checkInterval) {
|
||||
return
|
||||
}
|
||||
ap.lastCheck = now
|
||||
if !ap.lastCheckNano.CompareAndSwap(lastCheck, now) {
|
||||
return // 其他 goroutine 已在检查
|
||||
}
|
||||
|
||||
// 获取当前计数
|
||||
currentExhausted := ap.state.GetResourceExhaustedCount()
|
||||
currentPackets := ap.state.GetPacketCount()
|
||||
|
||||
ap.mu.Lock()
|
||||
// 计算增量(本周期内的耗尽率)
|
||||
deltaExhausted := currentExhausted - ap.lastExhaustedCount
|
||||
deltaPackets := currentPackets - ap.lastPacketCount
|
||||
@@ -104,7 +107,7 @@ func (ap *AdaptivePool) maybeAdjust() {
|
||||
newSize = ap.minSize
|
||||
}
|
||||
ap.tune(newSize)
|
||||
common.LogInfo(fmt.Sprintf("[AdaptivePool] 资源耗尽率 %.1f%%, 线程数 %d -> %d", rate*100, currentSize, newSize))
|
||||
common.LogInfo(i18n.Tr("adaptive_pool_resource_exhausted", fmt.Sprintf("%.1f", rate*100), currentSize, newSize))
|
||||
} else if rate < ap.recoveryThreshold && currentSize < ap.maxSize {
|
||||
// 恢复:增加 10% 线程(保守恢复)
|
||||
newSize := int(float64(currentSize) * 1.1)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AdaptiveTimeout 基于 RTT 采样的自适应超时计算器
|
||||
// 算法:timeout = mean(RTT) + 4 * stddev(RTT),clamp 到 [min, max]
|
||||
// 冷启动阶段(样本不足)返回用户配置的固定超时
|
||||
type AdaptiveTimeout struct {
|
||||
mu sync.Mutex
|
||||
samples []float64 // 环形缓冲区,单位 ms
|
||||
pos int // 写入位置
|
||||
count int // 已采集总数
|
||||
size int // 缓冲区容量
|
||||
minTO time.Duration
|
||||
maxTO time.Duration
|
||||
warmup int // 冷启动所需最小样本数
|
||||
cachedTO time.Duration
|
||||
dirty bool
|
||||
}
|
||||
|
||||
// NewAdaptiveTimeout 创建自适应超时计算器
|
||||
// maxTimeout: 用户配置的超时上限(即原始固定超时)
|
||||
func NewAdaptiveTimeout(maxTimeout time.Duration) *AdaptiveTimeout {
|
||||
return &AdaptiveTimeout{
|
||||
samples: make([]float64, 64),
|
||||
size: 64,
|
||||
minTO: 100 * time.Millisecond,
|
||||
maxTO: maxTimeout,
|
||||
warmup: 10,
|
||||
}
|
||||
}
|
||||
|
||||
// Record 记录一次成功连接的 RTT
|
||||
func (a *AdaptiveTimeout) Record(rtt time.Duration) {
|
||||
a.mu.Lock()
|
||||
a.samples[a.pos%a.size] = float64(rtt.Milliseconds())
|
||||
a.pos++
|
||||
a.count++
|
||||
a.dirty = true
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// Timeout 获取当前推荐超时值
|
||||
// 样本不足时返回 maxTO(冷启动)
|
||||
// 锁外执行均值/标准差计算,减少锁持有时间
|
||||
func (a *AdaptiveTimeout) Timeout() time.Duration {
|
||||
a.mu.Lock()
|
||||
if a.count < a.warmup {
|
||||
a.mu.Unlock()
|
||||
return a.maxTO
|
||||
}
|
||||
|
||||
if !a.dirty {
|
||||
cached := a.cachedTO
|
||||
a.mu.Unlock()
|
||||
return cached
|
||||
}
|
||||
|
||||
n := a.size
|
||||
if a.count < a.size {
|
||||
n = a.count
|
||||
}
|
||||
|
||||
// 拷贝样本到本地,释放锁后再计算
|
||||
localSamples := make([]float64, n)
|
||||
start := a.pos % a.size
|
||||
if a.count < a.size {
|
||||
copy(localSamples, a.samples[:n])
|
||||
} else {
|
||||
copy(localSamples[:a.size-start], a.samples[start:])
|
||||
copy(localSamples[a.size-start:], a.samples[:start])
|
||||
}
|
||||
a.mu.Unlock()
|
||||
|
||||
// 锁外计算
|
||||
var sum float64
|
||||
for _, s := range localSamples {
|
||||
sum += s
|
||||
}
|
||||
mean := sum / float64(n)
|
||||
|
||||
var variance float64
|
||||
for _, s := range localSamples {
|
||||
d := s - mean
|
||||
variance += d * d
|
||||
}
|
||||
stddev := math.Sqrt(variance / float64(n))
|
||||
|
||||
ms := mean + 4*stddev
|
||||
to := time.Duration(ms) * time.Millisecond
|
||||
|
||||
if to < a.minTO {
|
||||
to = a.minTO
|
||||
}
|
||||
if to > a.maxTO {
|
||||
to = a.maxTO
|
||||
}
|
||||
|
||||
// 短暂加锁更新缓存
|
||||
a.mu.Lock()
|
||||
a.cachedTO = to
|
||||
a.dirty = false
|
||||
a.mu.Unlock()
|
||||
|
||||
return to
|
||||
}
|
||||
+37
-33
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -37,7 +38,7 @@ type AliveStats struct {
|
||||
// NewAliveScanStrategy 创建新的存活探测扫描策略
|
||||
func NewAliveScanStrategy() *AliveScanStrategy {
|
||||
return &AliveScanStrategy{
|
||||
BaseScanStrategy: NewBaseScanStrategy("存活探测", FilterNone),
|
||||
BaseScanStrategy: NewBaseScanStrategy(i18n.GetText("scan_strategy_alive_name"), FilterNone),
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
@@ -53,65 +54,68 @@ func (s *AliveScanStrategy) Description() string {
|
||||
}
|
||||
|
||||
// Execute 执行存活探测扫描策略
|
||||
func (s *AliveScanStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func (s *AliveScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
|
||||
fv := common.GetFlagVars()
|
||||
if info.Host == "" && fv.HostsFile == "" {
|
||||
common.LogError(i18n.GetText("parse_error_target_empty"))
|
||||
if info.Host == "" && session.Params.HostsFile == "" {
|
||||
session.LogError(i18n.GetText("parse_error_target_empty"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 执行存活探测
|
||||
s.performAliveScan(info, config, state)
|
||||
|
||||
// 输出统计信息
|
||||
s.outputStats()
|
||||
s.performAliveScan(ctx, info, session)
|
||||
}
|
||||
|
||||
// performAliveScan 执行存活探测
|
||||
func (s *AliveScanStrategy) performAliveScan(info common.HostInfo, config *common.Config, state *common.State) {
|
||||
// 解析目标主机
|
||||
fv := common.GetFlagVars()
|
||||
hosts, err := parsers.ParseIP(info.Host, fv.HostsFile, fv.ExcludeHosts)
|
||||
func (s *AliveScanStrategy) performAliveScan(ctx context.Context, info common.HostInfo, session *common.ScanSession) {
|
||||
excludes, err := loadHostExcludes(session.Params)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("parse_target_failed", err))
|
||||
session.LogError(i18n.Tr("parse_target_failed", err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
common.LogError(i18n.GetText("parse_error_no_hosts"))
|
||||
iter, err := parsers.NewHostIterator(info.Host, session.Params.HostsFile, excludes...)
|
||||
if err != nil {
|
||||
session.LogError(i18n.Tr("parse_target_failed", err))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = iter.Close()
|
||||
}()
|
||||
|
||||
// 初始化统计信息
|
||||
s.stats.TotalHosts = len(hosts)
|
||||
s.stats.TotalHosts = 0
|
||||
s.stats.AliveHosts = 0
|
||||
s.stats.DeadHosts = 0
|
||||
|
||||
for {
|
||||
hosts, err := iter.NextBatch(ctx, targetHostBatchSize(session.Config))
|
||||
if err != nil {
|
||||
session.LogError(i18n.Tr("parse_target_failed", err))
|
||||
return
|
||||
}
|
||||
if len(hosts) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// 执行存活检测
|
||||
aliveList := CheckLive(hosts, false, config, state) // 使用ICMP探测
|
||||
s.stats.TotalHosts += len(hosts)
|
||||
aliveList := CheckLive(ctx, hosts, false, session)
|
||||
s.stats.AliveHosts += len(aliveList)
|
||||
for _, host := range aliveList {
|
||||
session.LogSuccess(fmt.Sprintf("alive %s", host))
|
||||
}
|
||||
}
|
||||
|
||||
if s.stats.TotalHosts == 0 {
|
||||
session.LogError(i18n.GetText("parse_error_no_hosts"))
|
||||
return
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
s.stats.AliveHosts = len(aliveList)
|
||||
s.stats.DeadHosts = s.stats.TotalHosts - s.stats.AliveHosts
|
||||
s.stats.ScanDuration = time.Since(s.startTime)
|
||||
s.stats.AliveHostList = aliveList // 存储存活主机列表
|
||||
|
||||
if s.stats.TotalHosts > 0 {
|
||||
s.stats.SuccessRate = float64(s.stats.AliveHosts) / float64(s.stats.TotalHosts) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// outputStats 输出统计信息(精简版)
|
||||
func (s *AliveScanStrategy) outputStats() {
|
||||
// 只输出存活主机列表,不输出冗余统计
|
||||
for _, host := range s.stats.AliveHostList {
|
||||
common.LogSuccess(fmt.Sprintf("alive %s", host))
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareTargets 存活探测不需要准备扫描目标
|
||||
func (s *AliveScanStrategy) PrepareTargets(info common.HostInfo) []common.HostInfo {
|
||||
// 存活探测不需要返回目标列表,因为它不进行后续扫描
|
||||
|
||||
@@ -81,6 +81,11 @@ func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetHos
|
||||
return false
|
||||
}
|
||||
|
||||
// 显式指定插件时,尊重调用方选择,不再强制使用插件默认端口过滤。
|
||||
if isCustomMode {
|
||||
return b.isPluginPassesFilterType(pluginName, isCustomMode, config)
|
||||
}
|
||||
|
||||
// 检查端口匹配和过滤器类型
|
||||
return b.isPluginApplicableToPortWithHost(pluginName, targetHost, targetPort) && b.isPluginPassesFilterType(pluginName, isCustomMode, config)
|
||||
}
|
||||
@@ -101,6 +106,10 @@ func (b *BaseScanStrategy) isLocalPlugin(pluginName string) bool {
|
||||
return plugins.HasType(pluginName, plugins.PluginTypeLocal)
|
||||
}
|
||||
|
||||
func (b *BaseScanStrategy) isUDPPlugin(pluginName string) bool {
|
||||
return plugins.IsUDP(pluginName)
|
||||
}
|
||||
|
||||
func (b *BaseScanStrategy) isLocalPluginExplicitlySpecified(pluginName string, config *common.Config) bool {
|
||||
return config.LocalPlugin == pluginName
|
||||
}
|
||||
@@ -136,6 +145,11 @@ func (b *BaseScanStrategy) isPluginApplicableToPort(pluginName string, targetPor
|
||||
|
||||
// isPluginPassesFilterType 检查插件是否通过过滤器类型检查
|
||||
func (b *BaseScanStrategy) isPluginPassesFilterType(pluginName string, isCustomMode bool, config *common.Config) bool {
|
||||
// UDP 插件有独立分发路径,不参与 TCP 端口匹配流水线
|
||||
if b.isUDPPlugin(pluginName) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 自定义模式下强制运行所有明确指定的插件
|
||||
if isCustomMode {
|
||||
return true
|
||||
@@ -150,8 +164,8 @@ func (b *BaseScanStrategy) isPluginPassesFilterType(pluginName string, isCustomM
|
||||
}
|
||||
return false
|
||||
case FilterService:
|
||||
// 服务扫描策略:排除本地插件
|
||||
return !b.isLocalPlugin(pluginName)
|
||||
// 服务扫描策略:排除本地插件和UDP插件(UDP有独立分发路径)
|
||||
return !b.isLocalPlugin(pluginName) && !b.isUDPPlugin(pluginName)
|
||||
case FilterWeb:
|
||||
// Web扫描策略:只允许Web插件
|
||||
return b.isWebPlugin(pluginName)
|
||||
@@ -165,7 +179,7 @@ func (b *BaseScanStrategy) isPluginPassesFilterType(pluginName string, isCustomM
|
||||
}
|
||||
|
||||
// LogPluginInfo 输出插件信息
|
||||
func (b *BaseScanStrategy) LogPluginInfo(config *common.Config) {
|
||||
func (b *BaseScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) {
|
||||
allPlugins, isCustomMode := b.GetPlugins(config)
|
||||
|
||||
var prefix string
|
||||
@@ -184,6 +198,7 @@ func (b *BaseScanStrategy) LogPluginInfo(config *common.Config) {
|
||||
_ = allPlugins
|
||||
_ = isCustomMode
|
||||
_ = prefix
|
||||
_ = session
|
||||
}
|
||||
|
||||
// formatPluginList 格式化插件列表(超过5个时精简显示)
|
||||
@@ -191,7 +206,7 @@ func formatPluginList(plugins []string) string {
|
||||
if len(plugins) <= 5 {
|
||||
return strings.Join(plugins, ", ")
|
||||
}
|
||||
return fmt.Sprintf("%s ... 等%d个", strings.Join(plugins[:5], ", "), len(plugins))
|
||||
return i18n.Tr("plugin_list_summary", strings.Join(plugins[:5], ", "), len(plugins))
|
||||
}
|
||||
|
||||
// ValidateConfiguration 验证扫描配置
|
||||
@@ -200,14 +215,14 @@ func (b *BaseScanStrategy) ValidateConfiguration() error {
|
||||
}
|
||||
|
||||
// LogScanStart 输出扫描开始信息(已精简,仅在非服务扫描模式下显示)
|
||||
func (b *BaseScanStrategy) LogScanStart() {
|
||||
func (b *BaseScanStrategy) LogScanStart(session *common.ScanSession) {
|
||||
// 服务扫描模式下不显示(插件信息已足够说明)
|
||||
// 仅在本地/Web等特殊模式下显示
|
||||
switch b.filterType {
|
||||
case FilterLocal:
|
||||
common.LogInfo(i18n.GetText("start_local_scan"))
|
||||
session.LogInfo(i18n.GetText("start_local_scan"))
|
||||
case FilterWeb:
|
||||
common.LogInfo(i18n.GetText("start_web_scan"))
|
||||
session.LogInfo(i18n.GetText("start_web_scan"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,9 +240,9 @@ func (b *BaseScanStrategy) getPluginsByFilterType() []string {
|
||||
}
|
||||
}
|
||||
case FilterService:
|
||||
// 服务扫描策略:排除本地插件和纯Web插件,保留服务插件
|
||||
// 服务扫描策略:排除本地插件和UDP插件,保留TCP服务插件
|
||||
for _, pluginName := range allPlugins {
|
||||
if !b.isLocalPlugin(pluginName) {
|
||||
if !b.isLocalPlugin(pluginName) && !b.isUDPPlugin(pluginName) {
|
||||
filteredPlugins = append(filteredPlugins, pluginName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"hash/fnv"
|
||||
)
|
||||
|
||||
// BloomFilter 布隆过滤器,用于ICMP包去重
|
||||
type BloomFilter struct {
|
||||
bits []bool
|
||||
size uint32
|
||||
k uint32 // hash函数数量
|
||||
}
|
||||
|
||||
// NewBloomFilter 创建布隆过滤器
|
||||
// size: 预期元素数量
|
||||
// falsePositiveRate: 期望的误判率(通常0.01即1%)
|
||||
func NewBloomFilter(size int, falsePositiveRate float64) *BloomFilter {
|
||||
// 计算最优bit数组大小: m = -n*ln(p) / (ln(2)^2)
|
||||
// 简化计算:m ≈ n * 10 for p=0.01
|
||||
m := uint32(size * 10)
|
||||
if m < 1024 {
|
||||
m = 1024 // 最小1KB
|
||||
}
|
||||
|
||||
// 计算最优hash函数数量: k = (m/n) * ln(2)
|
||||
// 简化:k ≈ 7 for p=0.01
|
||||
k := uint32(7)
|
||||
|
||||
return &BloomFilter{
|
||||
bits: make([]bool, m),
|
||||
size: m,
|
||||
k: k,
|
||||
}
|
||||
}
|
||||
|
||||
// Add 添加元素到过滤器
|
||||
func (bf *BloomFilter) Add(data string) {
|
||||
for i := uint32(0); i < bf.k; i++ {
|
||||
pos := bf.hash(data, i)
|
||||
bf.bits[pos] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Contains 检查元素是否可能存在
|
||||
// 返回true:可能存在(有误判可能)
|
||||
// 返回false:一定不存在
|
||||
func (bf *BloomFilter) Contains(data string) bool {
|
||||
for i := uint32(0); i < bf.k; i++ {
|
||||
pos := bf.hash(data, i)
|
||||
if !bf.bits[pos] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// hash 计算hash值
|
||||
func (bf *BloomFilter) hash(data string, seed uint32) uint32 {
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(data))
|
||||
// 添加seed实现多个hash函数
|
||||
for i := uint32(0); i < seed; i++ {
|
||||
_, _ = h.Write([]byte{byte(i)})
|
||||
}
|
||||
return h.Sum32() % bf.size
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/*
|
||||
bloom_filter_test.go - BloomFilter 高价值测试
|
||||
|
||||
测试重点:
|
||||
1. 基本正确性 - Add后Contains返回true,未添加的返回false
|
||||
2. 误判率验证 - 实际误判率应接近理论值(1%)
|
||||
3. 大规模数据 - 模拟真实ICMP去重场景
|
||||
|
||||
不测试:
|
||||
- 内部哈希实现细节
|
||||
- 精确的数学公式验证
|
||||
*/
|
||||
|
||||
// TestBloomFilter_BasicCorrectness 基本正确性测试
|
||||
func TestBloomFilter_BasicCorrectness(t *testing.T) {
|
||||
bf := NewBloomFilter(1000, 0.01)
|
||||
|
||||
// 添加元素后应该能找到
|
||||
testData := []string{
|
||||
"192.168.1.1",
|
||||
"10.0.0.1",
|
||||
"172.16.0.1",
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
bf.Add(data)
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !bf.Contains(data) {
|
||||
t.Errorf("已添加的元素 %s 应该返回 true", data)
|
||||
}
|
||||
}
|
||||
|
||||
// 未添加的元素(大概率)返回false
|
||||
notAdded := []string{
|
||||
"8.8.8.8",
|
||||
"1.1.1.1",
|
||||
"255.255.255.255",
|
||||
}
|
||||
|
||||
falsePositives := 0
|
||||
for _, data := range notAdded {
|
||||
if bf.Contains(data) {
|
||||
falsePositives++
|
||||
}
|
||||
}
|
||||
|
||||
// 3个未添加元素全部误判的概率极低(<0.0001%)
|
||||
if falsePositives == len(notAdded) {
|
||||
t.Error("所有未添加元素都返回true,布隆过滤器可能有问题")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBloomFilter_FalsePositiveRate 误判率验证
|
||||
//
|
||||
// 对于 n=10000, p=0.01 的布隆过滤器:
|
||||
// 实际误判率应该在 0.5% - 2% 之间(允许统计波动)
|
||||
func TestBloomFilter_FalsePositiveRate(t *testing.T) {
|
||||
n := 10000 // 添加的元素数
|
||||
bf := NewBloomFilter(n, 0.01)
|
||||
|
||||
// 添加n个元素
|
||||
for i := 0; i < n; i++ {
|
||||
bf.Add(fmt.Sprintf("added_%d", i))
|
||||
}
|
||||
|
||||
// 测试n个未添加的元素
|
||||
falsePositives := 0
|
||||
testCount := n
|
||||
for i := 0; i < testCount; i++ {
|
||||
if bf.Contains(fmt.Sprintf("not_added_%d", i)) {
|
||||
falsePositives++
|
||||
}
|
||||
}
|
||||
|
||||
actualRate := float64(falsePositives) / float64(testCount)
|
||||
|
||||
// 允许的误判率范围:0.1% - 3%(考虑统计波动)
|
||||
if actualRate > 0.03 {
|
||||
t.Errorf("误判率过高: %.2f%% (期望 < 3%%)", actualRate*100)
|
||||
}
|
||||
|
||||
t.Logf("实际误判率: %.2f%% (%d/%d)", actualRate*100, falsePositives, testCount)
|
||||
}
|
||||
|
||||
// TestBloomFilter_LargeScale 大规模数据测试
|
||||
//
|
||||
// 模拟真实的ICMP去重场景:100万个IP地址
|
||||
func TestBloomFilter_LargeScale(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("跳过大规模测试")
|
||||
}
|
||||
|
||||
n := 1000000 // 100万
|
||||
bf := NewBloomFilter(n, 0.01)
|
||||
|
||||
// 添加100万个元素
|
||||
for i := 0; i < n; i++ {
|
||||
bf.Add(fmt.Sprintf("192.168.%d.%d", i/256, i%256))
|
||||
}
|
||||
|
||||
// 验证已添加的元素
|
||||
sampleSize := 1000
|
||||
for i := 0; i < sampleSize; i++ {
|
||||
idx := i * (n / sampleSize)
|
||||
data := fmt.Sprintf("192.168.%d.%d", idx/256, idx%256)
|
||||
if !bf.Contains(data) {
|
||||
t.Errorf("已添加的元素 %s 返回 false", data)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试未添加元素的误判率
|
||||
falsePositives := 0
|
||||
for i := 0; i < sampleSize; i++ {
|
||||
if bf.Contains(fmt.Sprintf("10.%d.%d.%d", i/65536, (i/256)%256, i%256)) {
|
||||
falsePositives++
|
||||
}
|
||||
}
|
||||
|
||||
actualRate := float64(falsePositives) / float64(sampleSize)
|
||||
if actualRate > 0.03 {
|
||||
t.Errorf("大规模场景误判率过高: %.2f%%", actualRate*100)
|
||||
}
|
||||
|
||||
t.Logf("100万元素场景误判率: %.2f%%", actualRate*100)
|
||||
}
|
||||
|
||||
// TestBloomFilter_NoFalseNegative 验证无假阴性
|
||||
//
|
||||
// 布隆过滤器的核心保证:已添加的元素必定返回true
|
||||
func TestBloomFilter_NoFalseNegative(t *testing.T) {
|
||||
bf := NewBloomFilter(10000, 0.01)
|
||||
|
||||
// 添加5000个元素
|
||||
added := make([]string, 5000)
|
||||
for i := range added {
|
||||
added[i] = fmt.Sprintf("element_%d", i)
|
||||
bf.Add(added[i])
|
||||
}
|
||||
|
||||
// 全部验证
|
||||
for _, data := range added {
|
||||
if !bf.Contains(data) {
|
||||
t.Fatalf("假阴性!已添加的元素 %s 返回 false", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBloomFilter_EmptyFilter 空过滤器测试
|
||||
func TestBloomFilter_EmptyFilter(t *testing.T) {
|
||||
bf := NewBloomFilter(100, 0.01)
|
||||
|
||||
// 空过滤器应该对任何查询返回false
|
||||
testCases := []string{"anything", "192.168.1.1", ""}
|
||||
for _, tc := range testCases {
|
||||
if bf.Contains(tc) {
|
||||
t.Errorf("空过滤器对 %q 返回 true", tc)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/parsers"
|
||||
)
|
||||
|
||||
const maxHostBatchSize = 65536
|
||||
|
||||
func targetHostBatchSize(config *common.Config) int {
|
||||
size := parsers.DefaultHostBatchSize
|
||||
if config != nil && config.ThreadNum > 0 {
|
||||
threadWindow := config.ThreadNum * 8
|
||||
if threadWindow > size {
|
||||
size = threadWindow
|
||||
}
|
||||
}
|
||||
if size > maxHostBatchSize {
|
||||
return maxHostBatchSize
|
||||
}
|
||||
return size
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/parsers"
|
||||
)
|
||||
|
||||
func loadHostExcludes(params *common.FlagVars) ([]string, error) {
|
||||
if params == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
excludes := make([]string, 0, 1)
|
||||
if strings.TrimSpace(params.ExcludeHosts) != "" {
|
||||
excludes = append(excludes, params.ExcludeHosts)
|
||||
}
|
||||
if strings.TrimSpace(params.ExcludeHostsFile) == "" {
|
||||
return excludes, nil
|
||||
}
|
||||
|
||||
lines, err := parsers.ReadLinesFromFile(params.ExcludeHostsFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(excludes, lines...), nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
func TestLoadHostExcludesIncludesExcludeFile(t *testing.T) {
|
||||
path := t.TempDir() + "/exclude.txt"
|
||||
if err := os.WriteFile(path, []byte("192.168.1.2\n# comment\n192.168.1.3\n"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile error = %v", err)
|
||||
}
|
||||
|
||||
got, err := loadHostExcludes(&common.FlagVars{
|
||||
ExcludeHosts: "192.168.1.1",
|
||||
ExcludeHostsFile: path,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("loadHostExcludes error = %v", err)
|
||||
}
|
||||
|
||||
want := []string{"192.168.1.1", "192.168.1.2", "192.168.1.3"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("loadHostExcludes = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
+76
-48
@@ -2,11 +2,13 @@ package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -39,7 +41,9 @@ var pingErrorKeywords = []string{
|
||||
|
||||
// CheckLive 检测主机存活状态
|
||||
// 支持 ICMP/Ping 探测,并在响应率过低时自动启用 TCP 补充探测
|
||||
func CheckLive(hostslist []string, Ping bool, config *common.Config, state *common.State) []string {
|
||||
func CheckLive(ctx context.Context, hostslist []string, Ping bool, session *common.ScanSession) []string {
|
||||
config := session.Config
|
||||
state := session.State
|
||||
// 创建局部WaitGroup
|
||||
var livewg sync.WaitGroup
|
||||
|
||||
@@ -52,7 +56,7 @@ func CheckLive(hostslist []string, Ping bool, config *common.Config, state *comm
|
||||
chanHosts := make(chan string, len(hostslist))
|
||||
|
||||
// 处理存活主机
|
||||
go handleAliveHosts(chanHosts, hostslist, Ping, &aliveHosts, &aliveHostsMu, existHosts, config, &livewg)
|
||||
go handleAliveHosts(chanHosts, hostslist, Ping, &aliveHosts, &aliveHostsMu, existHosts, config, session, &livewg)
|
||||
|
||||
// 根据Ping参数选择检测方式
|
||||
if Ping {
|
||||
@@ -68,7 +72,7 @@ func CheckLive(hostslist []string, Ping bool, config *common.Config, state *comm
|
||||
|
||||
// TCP 补充探测:当 ICMP/Ping 响应率过低时自动启用
|
||||
// 这对防火墙过滤 ICMP 的环境特别有用
|
||||
aliveHosts = tcpSupplementaryProbe(hostslist, aliveHosts, config)
|
||||
aliveHosts = tcpSupplementaryProbe(ctx, hostslist, aliveHosts, session)
|
||||
|
||||
// 输出存活统计信息
|
||||
printAliveStats(aliveHosts, hostslist)
|
||||
@@ -78,7 +82,11 @@ func CheckLive(hostslist []string, Ping bool, config *common.Config, state *comm
|
||||
|
||||
// tcpSupplementaryProbe TCP 补充探测
|
||||
// 当 ICMP 响应率过低时(<10%),对未响应主机进行 TCP 探测
|
||||
func tcpSupplementaryProbe(allHosts []string, aliveHosts []string, config *common.Config) []string {
|
||||
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
|
||||
@@ -99,15 +107,15 @@ func tcpSupplementaryProbe(allHosts []string, aliveHosts []string, config *commo
|
||||
}
|
||||
|
||||
// 提示用户正在进行 TCP 补充探测
|
||||
common.LogInfo(i18n.Tr("tcp_probe_low_icmp_rate", fmt.Sprintf("%.1f%%", responseRate*100), len(unrespondedHosts)))
|
||||
session.LogInfo(i18n.Tr("tcp_probe_low_icmp_rate", fmt.Sprintf("%.1f%%", responseRate*100), len(unrespondedHosts)))
|
||||
|
||||
// 执行 TCP 补充探测
|
||||
tcpAliveHosts := runTcpProbeForHosts(unrespondedHosts, config)
|
||||
tcpAliveHosts := runTcpProbeForHosts(ctx, unrespondedHosts, session)
|
||||
|
||||
// 合并结果
|
||||
if len(tcpAliveHosts) > 0 {
|
||||
aliveHosts = append(aliveHosts, tcpAliveHosts...)
|
||||
common.LogInfo(i18n.Tr("tcp_probe_found", len(tcpAliveHosts)))
|
||||
session.LogInfo(i18n.Tr("tcp_probe_found", len(tcpAliveHosts)))
|
||||
}
|
||||
|
||||
return aliveHosts
|
||||
@@ -123,7 +131,7 @@ func IsContain(items []string, item string) bool {
|
||||
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) {
|
||||
func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool, aliveHosts *[]string, aliveHostsMu *sync.Mutex, existHosts map[string]struct{}, config *common.Config, session *common.ScanSession, livewg *sync.WaitGroup) {
|
||||
for ip := range chanHosts {
|
||||
if _, ok := existHosts[ip]; !ok && IsContain(hostslist, ip) {
|
||||
existHosts[ip] = struct{}{}
|
||||
@@ -148,12 +156,9 @@ func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool, al
|
||||
"protocol": protocol,
|
||||
},
|
||||
}
|
||||
_ = common.SaveResult(result)
|
||||
_ = session.SaveResult(result)
|
||||
|
||||
// 保留原有的控制台输出
|
||||
if !config.Output.Silent {
|
||||
common.LogInfo(i18n.Tr("host_alive", ip, protocol))
|
||||
}
|
||||
session.LogInfo(i18n.Tr("host_alive", ip, protocol))
|
||||
}
|
||||
livewg.Done()
|
||||
}
|
||||
@@ -282,13 +287,13 @@ func waitAdaptive(hostslist []string, aliveHosts *[]string, aliveHostsMu *sync.M
|
||||
|
||||
// 条件1:所有主机都已响应,立即结束
|
||||
if aliveCount >= totalHosts {
|
||||
common.LogDebug(fmt.Sprintf("[ICMP] 全部响应,耗时 %v", elapsed.Round(time.Millisecond)))
|
||||
common.LogDebug(i18n.Tr("icmp_debug_all_responded", elapsed.Round(time.Millisecond)))
|
||||
break
|
||||
}
|
||||
|
||||
// 条件2:超过最大等待时间,兜底结束
|
||||
if elapsed >= maxWait {
|
||||
common.LogDebug(fmt.Sprintf("[ICMP] 达到最大等待时间 %v,存活 %d/%d", maxWait, aliveCount, totalHosts))
|
||||
common.LogDebug(i18n.Tr("icmp_debug_max_wait", maxWait, aliveCount, totalHosts))
|
||||
break
|
||||
}
|
||||
|
||||
@@ -301,8 +306,7 @@ func waitAdaptive(hostslist []string, aliveHosts *[]string, aliveHostsMu *sync.M
|
||||
lastAliveCount = aliveCount
|
||||
} else if time.Since(lastChangeTime) >= icmpStableThreshold {
|
||||
// 连续 500ms 没有新响应,认为响应已稳定,提前结束
|
||||
common.LogDebug(fmt.Sprintf("[ICMP] 响应稳定,提前结束,耗时 %v,存活 %d/%d",
|
||||
elapsed.Round(time.Millisecond), aliveCount, totalHosts))
|
||||
common.LogDebug(i18n.Tr("icmp_debug_stable_done", elapsed.Round(time.Millisecond), aliveCount, totalHosts))
|
||||
break
|
||||
}
|
||||
} else {
|
||||
@@ -321,8 +325,8 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string,
|
||||
var endflag atomic.Bool
|
||||
var listenerWg sync.WaitGroup
|
||||
|
||||
// 创建布隆过滤器用于去重(自动根据主机数量调整大小)
|
||||
bloomFilter := NewBloomFilter(len(hostslist), 0.01)
|
||||
// 去重集合:过滤重复的ICMP响应
|
||||
seen := make(map[string]struct{}, len(hostslist))
|
||||
|
||||
// 启动监听协程
|
||||
listenerWg.Add(1)
|
||||
@@ -358,11 +362,10 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string,
|
||||
if sourceIP != nil && !endflag.Load() {
|
||||
ipStr := sourceIP.String()
|
||||
|
||||
// 使用布隆过滤器去重,过滤重复的ICMP响应和杂包
|
||||
if bloomFilter.Contains(ipStr) {
|
||||
if _, dup := seen[ipStr]; dup {
|
||||
continue
|
||||
}
|
||||
bloomFilter.Add(ipStr)
|
||||
seen[ipStr] = struct{}{}
|
||||
|
||||
livewg.Add(1)
|
||||
select {
|
||||
@@ -376,13 +379,22 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string,
|
||||
}
|
||||
}()
|
||||
|
||||
// 发送ICMP请求(应用令牌桶限速)
|
||||
limiter := state.GetICMPLimiter(config.Network.ICMPRate)
|
||||
// 发送ICMP请求(批量预构建 + 令牌桶限速)
|
||||
// 预构建所有 ICMP 包和目标地址,减少发送循环中的开销
|
||||
type icmpPacket struct {
|
||||
data []byte
|
||||
dst net.Addr
|
||||
}
|
||||
packets := make([]icmpPacket, 0, len(hostslist))
|
||||
for _, host := range hostslist {
|
||||
limiter.Wait(1) // 等待令牌,控制发包速率
|
||||
dst, _ := net.ResolveIPAddr("ip", host)
|
||||
IcmpByte := makemsg(host)
|
||||
_, _ = conn.WriteTo(IcmpByte, dst)
|
||||
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)
|
||||
}
|
||||
|
||||
// 自适应等待响应
|
||||
@@ -470,8 +482,12 @@ func icmpalive(host string) bool {
|
||||
// RunPing 使用系统Ping命令并发探测主机存活
|
||||
func RunPing(hostslist []string, chanHosts chan string, livewg *sync.WaitGroup) {
|
||||
var wg sync.WaitGroup
|
||||
// 限制并发数为50
|
||||
limiter := make(chan struct{}, 50)
|
||||
// 并发数根据主机数动态调整,上限 200
|
||||
concurrency := len(hostslist)
|
||||
if concurrency > 200 {
|
||||
concurrency = 200
|
||||
}
|
||||
limiter := make(chan struct{}, concurrency)
|
||||
|
||||
// 并发探测
|
||||
for _, host := range hostslist {
|
||||
@@ -674,20 +690,34 @@ func ArrayCountValueTop(arrInit []string, length int, flag bool) (arrTop []strin
|
||||
var tcpProbeCommonPorts = []int{80, 443, 22, 445}
|
||||
|
||||
// tcpProbeTimeout TCP 探测超时时间(较短,只做存活判断)
|
||||
const tcpProbeTimeout = 2 * time.Second
|
||||
const tcpProbeTimeout = 1 * time.Second
|
||||
|
||||
// tcpProbeThreshold TCP 补充探测触发阈值
|
||||
// 当 ICMP 响应率低于此值时,自动启用 TCP 补充探测
|
||||
const tcpProbeThreshold = 0.1 // 10%
|
||||
|
||||
// tcpProbeAlive 使用 TCP 探测主机是否存活
|
||||
// 尝试连接常用端口,任一端口响应即认为存活
|
||||
func tcpProbeAlive(host string) bool {
|
||||
// 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 {
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, tcpProbeTimeout)
|
||||
if err == nil {
|
||||
_ = conn.Close()
|
||||
go func(p int) {
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(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
|
||||
}
|
||||
}
|
||||
@@ -696,7 +726,7 @@ func tcpProbeAlive(host string) bool {
|
||||
|
||||
// runTcpProbeForHosts 对指定主机列表进行 TCP 补充探测
|
||||
// 返回存活的主机列表
|
||||
func runTcpProbeForHosts(hosts []string, config *common.Config) []string {
|
||||
func runTcpProbeForHosts(ctx context.Context, hosts []string, session *common.ScanSession) []string {
|
||||
if len(hosts) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -705,10 +735,10 @@ func runTcpProbeForHosts(hosts []string, config *common.Config) []string {
|
||||
var mu sync.Mutex
|
||||
aliveHosts := make([]string, 0)
|
||||
|
||||
// 并发控制,避免资源耗尽
|
||||
concurrency := 50
|
||||
if len(hosts) < concurrency {
|
||||
concurrency = len(hosts)
|
||||
// 并发控制,根据主机数动态调整,上限 200
|
||||
concurrency := len(hosts)
|
||||
if concurrency > 200 {
|
||||
concurrency = 200
|
||||
}
|
||||
limiter := make(chan struct{}, concurrency)
|
||||
|
||||
@@ -722,7 +752,7 @@ func runTcpProbeForHosts(hosts []string, config *common.Config) []string {
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
if tcpProbeAlive(h) {
|
||||
if tcpProbeAlive(ctx, session, h) {
|
||||
mu.Lock()
|
||||
aliveHosts = append(aliveHosts, h)
|
||||
mu.Unlock()
|
||||
@@ -737,11 +767,9 @@ func runTcpProbeForHosts(hosts []string, config *common.Config) []string {
|
||||
"protocol": "TCP",
|
||||
},
|
||||
}
|
||||
_ = common.SaveResult(result)
|
||||
_ = session.SaveResult(result)
|
||||
|
||||
if !config.Output.Silent {
|
||||
common.LogInfo(i18n.Tr("host_alive", h, "TCP"))
|
||||
}
|
||||
session.LogInfo(i18n.Tr("host_alive", h, "TCP"))
|
||||
}
|
||||
}(host)
|
||||
}
|
||||
|
||||
+13
-10
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
@@ -16,17 +17,17 @@ type LocalScanStrategy struct {
|
||||
// NewLocalScanStrategy 创建新的本地扫描策略
|
||||
func NewLocalScanStrategy() *LocalScanStrategy {
|
||||
return &LocalScanStrategy{
|
||||
BaseScanStrategy: NewBaseScanStrategy("本地扫描", FilterLocal),
|
||||
BaseScanStrategy: NewBaseScanStrategy(i18n.GetText("scan_strategy_local_name"), FilterLocal),
|
||||
}
|
||||
}
|
||||
|
||||
// LogPluginInfo 重写以只显示通过-local指定的插件
|
||||
func (s *LocalScanStrategy) LogPluginInfo(config *common.Config) {
|
||||
func (s *LocalScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) {
|
||||
localPlugin := config.LocalPlugin
|
||||
if localPlugin != "" {
|
||||
common.LogInfo(i18n.Tr("local_plugin_info", localPlugin))
|
||||
session.LogInfo(i18n.Tr("local_plugin_info", localPlugin))
|
||||
} else {
|
||||
common.LogError(i18n.GetText("local_plugin_not_specified"))
|
||||
session.LogError(i18n.GetText("local_plugin_not_specified"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,32 +42,34 @@ func (s *LocalScanStrategy) Description() string {
|
||||
}
|
||||
|
||||
// Execute 执行本地扫描策略
|
||||
func (s *LocalScanStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func (s *LocalScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
config := session.Config
|
||||
|
||||
// 输出扫描开始信息
|
||||
s.LogScanStart()
|
||||
s.LogScanStart(session)
|
||||
|
||||
// 验证插件配置
|
||||
if err := s.ValidateConfiguration(); err != nil {
|
||||
common.LogError(err.Error())
|
||||
session.LogError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 验证本地插件是否存在
|
||||
if config.LocalPlugin != "" {
|
||||
if !plugins.Exists(config.LocalPlugin) {
|
||||
common.LogError(i18n.Tr("local_plugin_not_found", config.LocalPlugin))
|
||||
session.LogError(i18n.Tr("local_plugin_not_found", config.LocalPlugin))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 输出插件信息
|
||||
s.LogPluginInfo(config)
|
||||
s.LogPluginInfo(config, session)
|
||||
|
||||
// 准备目标(本地扫描通常只有一个目标,即本机)
|
||||
targets := s.PrepareTargets(info)
|
||||
|
||||
// 执行扫描任务
|
||||
ExecuteScanTasks(config, state, targets, s, ch, wg)
|
||||
ExecuteScanTasks(ctx, session, targets, s, ch, wg)
|
||||
}
|
||||
|
||||
// PrepareTargets 准备本地扫描目标
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Benchmark: containsFold vs strings.ToLower + strings.Contains
|
||||
// =============================================================================
|
||||
|
||||
func BenchmarkContainsFold(b *testing.B) {
|
||||
err := errors.New("connection reset by peer: 192.168.1.1:445")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
containsFold(err.Error(), "connection reset")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStringsToLowerContains(b *testing.B) {
|
||||
err := errors.New("connection reset by peer: 192.168.1.1:445")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
strings.Contains(strings.ToLower(err.Error()), "connection reset")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Benchmark: fmt.Sprintf vs net.JoinHostPort + fmtPort
|
||||
// =============================================================================
|
||||
|
||||
func BenchmarkFmtSprintfAddr(b *testing.B) {
|
||||
host := "192.168.1.1"
|
||||
port := 445
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("%s:%d", host, port)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJoinHostPortFmtPort(b *testing.B) {
|
||||
host := "192.168.1.1"
|
||||
port := 445
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = net.JoinHostPort(host, fmtPort(port))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFmtPort(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmtPort(445)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Benchmark: readFromConn buffer pre-allocation
|
||||
// =============================================================================
|
||||
|
||||
func BenchmarkAppendFromNil(b *testing.B) {
|
||||
data := []byte("HTTP/1.1 200 OK\r\nServer: nginx")
|
||||
chunk := data[:10]
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var result []byte
|
||||
result = append(result, chunk...)
|
||||
result = append(result, chunk...)
|
||||
_ = result
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendPreAllocated(b *testing.B) {
|
||||
data := []byte("HTTP/1.1 200 OK\r\nServer: nginx")
|
||||
chunk := data[:10]
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := make([]byte, 0, 4096)
|
||||
result = append(result, chunk...)
|
||||
result = append(result, chunk...)
|
||||
_ = result
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Benchmark: AdaptiveTimeout computation under lock vs outside lock
|
||||
// =============================================================================
|
||||
|
||||
func BenchmarkAdaptiveTimeoutComputation(b *testing.B) {
|
||||
at := NewAdaptiveTimeout(3000 * 1000000) // 3s in ns
|
||||
// Warm up: add 64 samples
|
||||
for i := 0; i < 64; i++ {
|
||||
at.Record(10 * 1000000) // 10ms in ns
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = at.Timeout()
|
||||
}
|
||||
}
|
||||
+350
-111
@@ -1,8 +1,10 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -34,31 +36,59 @@ var resourceExhaustedPatterns = []string{
|
||||
"no buffer space available",
|
||||
"cannot assign requested address",
|
||||
"connection reset by peer",
|
||||
"发包受限",
|
||||
i18n.GetText("network_rate_limited_pattern"),
|
||||
"rate limited",
|
||||
}
|
||||
|
||||
// closedPatterns 连接已关闭的错误模式
|
||||
var closedPatterns = []string{
|
||||
"broken pipe",
|
||||
"connection reset",
|
||||
"connection refused",
|
||||
"use of closed network connection",
|
||||
"connection was forcibly closed",
|
||||
}
|
||||
|
||||
// proxyErrorTexts 代理错误响应文本模式
|
||||
var proxyErrorTexts = []string{
|
||||
"connection refused",
|
||||
"host unreachable",
|
||||
"network unreachable",
|
||||
"connection timed out",
|
||||
"proxy error",
|
||||
"gateway error",
|
||||
"bad gateway",
|
||||
"502",
|
||||
"503",
|
||||
}
|
||||
|
||||
// resultCollector 结果收集器,用于并发安全地收集扫描结果
|
||||
// 使用 map 实现:O(1) 的添加和删除,无顺序依赖问题
|
||||
type resultCollector struct {
|
||||
mu sync.Mutex
|
||||
addrs map[string]struct{}
|
||||
mu sync.Mutex
|
||||
addrs map[string]struct{}
|
||||
stream chan<- string
|
||||
}
|
||||
|
||||
// newResultCollector 创建结果收集器
|
||||
func newResultCollector() *resultCollector {
|
||||
func newResultCollector(stream chan<- string) *resultCollector {
|
||||
return &resultCollector{
|
||||
addrs: make(map[string]struct{}),
|
||||
addrs: make(map[string]struct{}),
|
||||
stream: stream,
|
||||
}
|
||||
}
|
||||
|
||||
// Add 添加一个扫描结果
|
||||
func (c *resultCollector) Add(addr string) {
|
||||
c.mu.Lock()
|
||||
if _, dup := c.addrs[addr]; dup {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.addrs[addr] = struct{}{}
|
||||
c.mu.Unlock()
|
||||
if c.stream != nil {
|
||||
c.stream <- addr
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll 获取所有结果
|
||||
func (c *resultCollector) GetAll() []string {
|
||||
c.mu.Lock()
|
||||
result := make([]string, 0, len(c.addrs))
|
||||
@@ -73,6 +103,7 @@ func (c *resultCollector) GetAll() []string {
|
||||
type portScanTask struct {
|
||||
host string
|
||||
port int
|
||||
addr string // 预格式化的 host:port,避免 fmt.Sprintf 热路径分配
|
||||
semaphore chan struct{} // 完成时释放窗口槽位
|
||||
}
|
||||
|
||||
@@ -110,16 +141,34 @@ func (f *failedPortCollector) Count() int {
|
||||
|
||||
// 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))
|
||||
// stream: 可选,非 nil 时每发现开放端口立即发送 addr,扫描结束后关闭
|
||||
func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout int64, session *common.ScanSession, stream chan<- string) []string {
|
||||
config := session.Config
|
||||
state := session.State
|
||||
session.LogDebug(i18n.Tr("port_scan_debug_start", len(hosts), config.ThreadNum))
|
||||
|
||||
// 大规模扫描预筛:跨多个 /24 时先做网段探活,跳过空网段
|
||||
if len(hosts) > subnetProbeThreshold {
|
||||
hosts = probeSubnets(ctx, hosts, time.Duration(timeout)*time.Second, session)
|
||||
if len(hosts) == 0 {
|
||||
session.LogInfo(i18n.GetText("port_scan_no_alive_subnet"))
|
||||
if stream != nil {
|
||||
close(stream)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 解析端口和排除端口
|
||||
portList := parsers.ParsePort(ports)
|
||||
if len(portList) == 0 {
|
||||
common.LogError(i18n.Tr("invalid_port", ports))
|
||||
session.LogError(i18n.Tr("invalid_port", ports))
|
||||
if stream != nil {
|
||||
close(stream)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
common.LogDebug(fmt.Sprintf("[PortScan] 端口解析完成: %d个端口", len(portList)))
|
||||
session.LogDebug(i18n.Tr("port_scan_debug_ports_parsed", len(portList)))
|
||||
|
||||
// 使用config中的排除端口配置
|
||||
excludePorts := parsers.ParsePort(config.Target.ExcludePorts)
|
||||
@@ -129,44 +178,45 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
|
||||
}
|
||||
|
||||
// 检查代理可靠性,如果存在全回显问题则警告
|
||||
if common.IsProxyEnabled() && !common.IsProxyReliable() {
|
||||
common.LogError("检测到代理存在全回显问题,端口扫描结果可能不准确")
|
||||
if session.ProxyEnabled() && !session.ProxyReliable() {
|
||||
session.LogError(i18n.GetText("proxy_echo_warning"))
|
||||
}
|
||||
|
||||
// 创建流式迭代器(O(1) 内存,端口喷洒策略)
|
||||
iter := NewSocketIterator(hosts, portList, exclude)
|
||||
totalTasks := iter.Total()
|
||||
common.LogDebug(fmt.Sprintf("[PortScan] 总任务数: %d", totalTasks))
|
||||
session.LogDebug(i18n.Tr("port_scan_debug_total_tasks", totalTasks))
|
||||
|
||||
// 使用传入的配置
|
||||
threadNum := config.ThreadNum
|
||||
|
||||
// 大规模扫描警告和线程数自动调整
|
||||
if totalTasks > 100000 {
|
||||
common.LogInfo(fmt.Sprintf("大规模扫描: %d 个目标 (%d主机 × %d端口)", totalTasks, len(hosts), len(portList)))
|
||||
session.LogInfo(i18n.Tr("large_scan_notice", totalTasks, len(hosts), len(portList)))
|
||||
// 如果任务数超过100万且线程数大于300,自动降低线程数
|
||||
if totalTasks > 1000000 && threadNum > 300 {
|
||||
oldThreadNum := threadNum
|
||||
threadNum = 300
|
||||
common.LogInfo(fmt.Sprintf("自动调整线程数: %d -> %d (大规模扫描优化)", oldThreadNum, threadNum))
|
||||
session.LogInfo(i18n.Tr("large_scan_thread_adjusted", oldThreadNum, threadNum))
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化端口扫描进度条
|
||||
if totalTasks > 0 && config.Output.ShowProgress {
|
||||
description := fmt.Sprintf("端口扫描中(%d线程)", threadNum)
|
||||
common.InitProgressBar(int64(totalTasks), description)
|
||||
description := i18n.Tr("port_scan_progress_description", threadNum)
|
||||
common.InitProgressBar(totalTasks, description)
|
||||
}
|
||||
common.LogDebug("[PortScan] 进度条初始化完成")
|
||||
session.LogDebug(i18n.GetText("port_scan_debug_progress_ready"))
|
||||
|
||||
// 初始化并发控制
|
||||
to := time.Duration(timeout) * time.Second
|
||||
var count int64
|
||||
collector := newResultCollector()
|
||||
adaptiveTO := NewAdaptiveTimeout(to)
|
||||
var count atomic.Int64
|
||||
collector := newResultCollector(stream)
|
||||
failedCollector := &failedPortCollector{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
common.LogDebug(fmt.Sprintf("[PortScan] 开始创建线程池, size=%d", threadNum))
|
||||
session.LogDebug(i18n.Tr("port_scan_debug_pool_create", threadNum))
|
||||
// 创建自适应线程池(支持动态调整)
|
||||
pool, err := NewAdaptivePool(threadNum, func(task interface{}) {
|
||||
taskInfo, ok := task.(portScanTask)
|
||||
@@ -178,31 +228,38 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", taskInfo.host, taskInfo.port)
|
||||
scanSinglePort(taskInfo.host, taskInfo.port, addr, to, &count, collector, failedCollector, config, state)
|
||||
scanSinglePort(ctx, taskInfo.host, taskInfo.port, taskInfo.addr, adaptiveTO, &count, collector, failedCollector, session)
|
||||
common.UpdateProgressBar(1)
|
||||
}, state)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("thread_pool_create_failed", err))
|
||||
session.LogError(i18n.Tr("thread_pool_create_failed", err))
|
||||
if stream != nil {
|
||||
close(stream)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
common.LogDebug("[PortScan] 线程池创建成功")
|
||||
session.LogDebug(i18n.GetText("port_scan_debug_pool_created"))
|
||||
defer pool.Release()
|
||||
|
||||
common.LogDebug("[PortScan] 开始滑动窗口调度")
|
||||
session.LogDebug(i18n.GetText("port_scan_debug_schedule_start"))
|
||||
// 滑动窗口调度:维护固定数量的"飞行中"任务
|
||||
slidingWindowSchedule(iter, pool, &wg, threadNum)
|
||||
common.LogDebug("[PortScan] 滑动窗口调度完成")
|
||||
session.LogDebug(i18n.GetText("port_scan_debug_schedule_done"))
|
||||
|
||||
// 收集结果
|
||||
aliveAddrs := collector.GetAll()
|
||||
|
||||
// 关闭流式通知 channel
|
||||
if stream != nil {
|
||||
close(stream)
|
||||
}
|
||||
|
||||
// 完成端口扫描进度条
|
||||
if common.IsProgressActive() {
|
||||
common.FinishProgressBar()
|
||||
}
|
||||
|
||||
common.LogInfo(i18n.Tr("port_scan_complete", count))
|
||||
session.LogInfo(i18n.Tr("port_scan_complete", count.Load()))
|
||||
|
||||
// 检查扫描失败率,如果过高则警告用户
|
||||
resourceErrors := state.GetResourceExhaustedCount()
|
||||
@@ -213,18 +270,18 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
|
||||
|
||||
if failureRate > 20 {
|
||||
// 失败率超过20%,严重警告
|
||||
common.LogError(i18n.Tr("scan_failure_rate_high", fmt.Sprintf("%.1f%%", failureRate), failedCount, totalTasks))
|
||||
common.LogError(i18n.GetText("scan_failure_reason"))
|
||||
common.LogError(i18n.Tr("scan_reduce_threads_suggestion", threadNum))
|
||||
session.LogError(i18n.Tr("scan_failure_rate_high", fmt.Sprintf("%.1f%%", failureRate), failedCount, totalTasks))
|
||||
session.LogError(i18n.GetText("scan_failure_reason"))
|
||||
session.LogError(i18n.Tr("scan_reduce_threads_suggestion", threadNum))
|
||||
} else if failureRate > 5 {
|
||||
// 失败率5-20%,一般警告
|
||||
common.LogInfo(i18n.Tr("scan_partial_failure", fmt.Sprintf("%.1f%%", failureRate), failedCount, totalTasks))
|
||||
common.LogInfo(i18n.Tr("scan_reduce_threads_accuracy", threadNum))
|
||||
session.LogInfo(i18n.Tr("scan_partial_failure", fmt.Sprintf("%.1f%%", failureRate), failedCount, totalTasks))
|
||||
session.LogInfo(i18n.Tr("scan_reduce_threads_accuracy", threadNum))
|
||||
}
|
||||
}
|
||||
|
||||
if resourceErrors > 0 {
|
||||
common.LogError(i18n.Tr("resource_exhausted_warning", resourceErrors))
|
||||
session.LogError(i18n.Tr("resource_exhausted_warning", resourceErrors))
|
||||
}
|
||||
|
||||
return aliveAddrs
|
||||
@@ -250,21 +307,41 @@ func slidingWindowSchedule(iter *SocketIterator, pool *AdaptivePool, wg *sync.Wa
|
||||
task := portScanTask{
|
||||
host: host,
|
||||
port: port,
|
||||
addr: net.JoinHostPort(host, fmtPort(port)),
|
||||
semaphore: semaphore,
|
||||
}
|
||||
_ = pool.Invoke(task)
|
||||
if err := pool.Invoke(task); err != nil {
|
||||
<-semaphore
|
||||
wg.Done()
|
||||
}
|
||||
}
|
||||
|
||||
// 等待所有任务完成
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// fmtPort 无分配的端口号格式化
|
||||
func fmtPort(port int) string {
|
||||
if port < 0 || port > 65535 {
|
||||
return "0"
|
||||
}
|
||||
// 预分配足够大的缓冲区
|
||||
var buf [6]byte
|
||||
i := len(buf)
|
||||
for port > 0 || i == len(buf) {
|
||||
i--
|
||||
buf[i] = byte(port%10) + '0'
|
||||
port /= 10
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
|
||||
// connectWithRetry 带重试的TCP连接 - 只对资源耗尽错误重试
|
||||
func connectWithRetry(addr string, timeout time.Duration, maxRetries int, state *common.State) (net.Conn, error) {
|
||||
func connectWithRetry(ctx context.Context, session *common.ScanSession, addr string, timeout time.Duration, maxRetries int) (net.Conn, error) {
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
||||
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
|
||||
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
@@ -278,11 +355,11 @@ func connectWithRetry(addr string, timeout time.Duration, maxRetries int, state
|
||||
}
|
||||
|
||||
// 记录资源耗尽错误
|
||||
state.IncrementResourceExhaustedCount()
|
||||
session.State.IncrementResourceExhaustedCount()
|
||||
|
||||
// 指数退避:第1次等50ms,第2次等150ms
|
||||
// 指数退避:200ms → 600ms → 1200ms
|
||||
if attempt < maxRetries-1 {
|
||||
waitTime := time.Duration(50*(attempt+1)) * time.Millisecond
|
||||
waitTime := time.Duration(200*(1<<uint(attempt))) * time.Millisecond
|
||||
time.Sleep(waitTime)
|
||||
}
|
||||
}
|
||||
@@ -298,7 +375,7 @@ func isResourceExhaustedError(err error) bool {
|
||||
|
||||
errStr := err.Error()
|
||||
for _, pattern := range resourceExhaustedPatterns {
|
||||
if strings.Contains(errStr, pattern) {
|
||||
if containsFold(errStr, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -306,14 +383,50 @@ func isResourceExhaustedError(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// containsFold 忽略大小写的子串匹配,避免 strings.ToLower 分配
|
||||
func containsFold(s, substr string) bool {
|
||||
if len(substr) == 0 {
|
||||
return true
|
||||
}
|
||||
if len(substr) > len(s) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if matchFold(s[i:i+len(substr)], substr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchFold 忽略大小写逐字节比较
|
||||
func matchFold(a, b string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a); i++ {
|
||||
ca, cb := a[i], b[i]
|
||||
if ca >= 'A' && ca <= 'Z' {
|
||||
ca += 'a' - 'A'
|
||||
}
|
||||
if cb >= 'A' && cb <= 'Z' {
|
||||
cb += 'a' - 'A'
|
||||
}
|
||||
if ca != cb {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// buildServiceLogMessage 构建服务识别的日志信息
|
||||
// 格式: addr service [Product:xxx ||Version:xxx] Banner:(xxx)
|
||||
func buildServiceLogMessage(addr string, serviceInfo *ServiceInfo, isWeb bool) string {
|
||||
var msg strings.Builder
|
||||
msg.WriteString(fmt.Sprintf("%-21s", addr))
|
||||
fmt.Fprintf(&msg, "%-21s", addr)
|
||||
|
||||
if serviceInfo.Name != "unknown" {
|
||||
msg.WriteString(fmt.Sprintf(" %-8s", serviceInfo.Name))
|
||||
fmt.Fprintf(&msg, " %-8s", serviceInfo.Name)
|
||||
}
|
||||
|
||||
// 构建 [Product:xxx ||Version:xxx] 格式
|
||||
@@ -325,7 +438,7 @@ func buildServiceLogMessage(addr string, serviceInfo *ServiceInfo, isWeb bool) s
|
||||
info = append(info, fmt.Sprintf("Version:%s", serviceInfo.Version))
|
||||
}
|
||||
if len(info) > 0 {
|
||||
msg.WriteString(fmt.Sprintf(" [%s]", strings.Join(info, " ||")))
|
||||
fmt.Fprintf(&msg, " [%s]", strings.Join(info, " ||"))
|
||||
}
|
||||
|
||||
// Banner 信息
|
||||
@@ -334,35 +447,39 @@ func buildServiceLogMessage(addr string, serviceInfo *ServiceInfo, isWeb bool) s
|
||||
if len(banner) > 80 {
|
||||
banner = banner[:80] + "..."
|
||||
}
|
||||
msg.WriteString(fmt.Sprintf(" Banner:(%s)", banner))
|
||||
fmt.Fprintf(&msg, " Banner:(%s)", banner)
|
||||
}
|
||||
|
||||
return msg.String()
|
||||
}
|
||||
|
||||
// scanSinglePort 扫描单个端口并进行服务识别(重构后的简洁版本)
|
||||
func scanSinglePort(host string, port int, addr string, timeout time.Duration, count *int64, collector *resultCollector, failedCollector *failedPortCollector, config *common.Config, state *common.State) {
|
||||
func scanSinglePort(ctx context.Context, host string, port int, addr string, adaptiveTO *AdaptiveTimeout, count *atomic.Int64, collector *resultCollector, failedCollector *failedPortCollector, session *common.ScanSession) {
|
||||
config := session.Config
|
||||
timeout := adaptiveTO.Timeout()
|
||||
// 步骤1:建立连接
|
||||
conn, err := connectWithRetry(addr, timeout, 3, state)
|
||||
start := time.Now()
|
||||
conn, err := connectWithRetry(ctx, session, addr, timeout, 2)
|
||||
if err != nil {
|
||||
handleConnectionFailure(err, host, port, addr, failedCollector)
|
||||
return
|
||||
}
|
||||
adaptiveTO.Record(time.Since(start))
|
||||
|
||||
// 步骤1.5:代理连接深度验证(防止透明代理/全回显代理的假连接问题)
|
||||
valid, verifyMethod := verifyProxyConnectionDeep(conn, addr)
|
||||
valid, verifyMethod := verifyProxyConnectionDeep(conn, addr, session)
|
||||
if !valid {
|
||||
common.LogDebug(fmt.Sprintf("代理验证失败 %s: %s", addr, verifyMethod))
|
||||
session.LogDebug(i18n.Tr("proxy_verify_failed", addr, verifyMethod))
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// 步骤1.6:如果使用了代理且进行了数据交互,需要重建连接
|
||||
// 因为验证阶段可能读取了Banner或发送了HTTP GET探测,污染了连接状态
|
||||
if common.IsProxyEnabled() && verifyMethod != "direct" {
|
||||
if session.ProxyEnabled() && verifyMethod != "direct" {
|
||||
_ = conn.Close()
|
||||
// 重新建立干净的连接用于服务识别
|
||||
conn, err = connectWithRetry(addr, timeout, 3, state)
|
||||
conn, err = connectWithRetry(ctx, session, addr, timeout, 2)
|
||||
if err != nil {
|
||||
handleConnectionFailure(err, host, port, addr, failedCollector)
|
||||
return
|
||||
@@ -370,31 +487,36 @@ func scanSinglePort(host string, port int, addr string, timeout time.Duration, c
|
||||
}
|
||||
|
||||
// 步骤2:记录开放端口
|
||||
atomic.AddInt64(count, 1)
|
||||
count.Add(1)
|
||||
collector.Add(addr)
|
||||
saveOpenPort(host, port)
|
||||
saveOpenPort(session, host, port)
|
||||
|
||||
// 步骤3:服务识别(Scanner负责关闭连接,包括探测中可能创建的新连接)
|
||||
scanner := NewSmartPortInfoScanner(host, port, conn, timeout, config)
|
||||
scanner := NewSmartPortInfoScanner(ctx, host, port, conn, timeout, config, session)
|
||||
// 服务探测超时自适应:用 RTT 采样值约束读超时上限
|
||||
// 下限 500ms:服务处理需要时间,不能太激进
|
||||
if rttTO := adaptiveTO.Timeout(); rttTO < timeout {
|
||||
maxMS := int(rttTO.Milliseconds()) * 6
|
||||
if maxMS < 500 {
|
||||
maxMS = 500
|
||||
}
|
||||
scanner.info.maxReadTimeoutMS = maxMS
|
||||
}
|
||||
defer scanner.Close()
|
||||
serviceInfo, _ := scanner.SmartIdentify()
|
||||
|
||||
// 步骤4:处理结果
|
||||
processServiceResult(host, port, addr, serviceInfo, config)
|
||||
processServiceResult(ctx, host, port, addr, serviceInfo, config, session)
|
||||
}
|
||||
|
||||
// handleConnectionFailure 处理连接失败
|
||||
// 只收集资源耗尽类错误,timeout 是正常的扫描结果(防火墙 drop)不计入失败
|
||||
func handleConnectionFailure(err error, host string, port int, addr string, failedCollector *failedPortCollector) {
|
||||
if isResourceExhaustedError(err) || isTimeoutError(err) {
|
||||
if isResourceExhaustedError(err) {
|
||||
failedCollector.Add(host, port, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// isTimeoutError 判断是否为超时错误
|
||||
func isTimeoutError(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "i/o timeout")
|
||||
}
|
||||
|
||||
// verifyProxyConnectionDeep 深度验证代理连接是否真正可用
|
||||
// 防止透明代理/全回显代理的假连接问题
|
||||
// 返回: (是否有效, 验证方式)
|
||||
@@ -403,9 +525,10 @@ func isTimeoutError(err error) bool {
|
||||
// 1. 快速 Banner 检测 (100ms) - 大部分服务会主动发送数据
|
||||
// 2. 轻量探测 (发送 \r\n) - 触发某些服务响应,同时不污染协议状态
|
||||
// 3. 短超时等待 (500ms) - 平衡准确性和性能
|
||||
func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
|
||||
// 如果没有使用代理,跳过验证
|
||||
if !common.IsProxyEnabled() {
|
||||
func verifyProxyConnectionDeep(conn net.Conn, addr string, session *common.ScanSession) (bool, string) {
|
||||
// 无代理或SOCKS5代理:跳过深度验证
|
||||
// SOCKS5协议层已验证连接可达性,连接成功即端口开放
|
||||
if !session.ProxyEnabled() || session.IsSOCKS5Proxy() {
|
||||
return true, "direct"
|
||||
}
|
||||
|
||||
@@ -420,7 +543,7 @@ func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
|
||||
|
||||
if n > 0 {
|
||||
if isProxyErrorResponse(buf[:n]) {
|
||||
common.LogDebug(fmt.Sprintf("代理返回错误响应 %s", addr))
|
||||
common.LogDebug(i18n.Tr("proxy_error_response", addr))
|
||||
return false, "proxy_error"
|
||||
}
|
||||
return true, "banner"
|
||||
@@ -437,7 +560,7 @@ func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
|
||||
_ = conn.SetWriteDeadline(time.Time{})
|
||||
|
||||
if writeErr != nil && isConnectionClosed(writeErr) {
|
||||
common.LogDebug(fmt.Sprintf("探测写入失败 %s: %v", addr, writeErr))
|
||||
common.LogDebug(i18n.Tr("proxy_probe_write_failed", addr, writeErr))
|
||||
return false, "write_failed"
|
||||
}
|
||||
|
||||
@@ -449,7 +572,7 @@ func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
|
||||
|
||||
if n > 0 {
|
||||
if isProxyErrorResponse(buf[:n]) {
|
||||
common.LogDebug(fmt.Sprintf("代理探测返回错误 %s", addr))
|
||||
common.LogDebug(i18n.Tr("proxy_probe_error_response", addr))
|
||||
return false, "proxy_error"
|
||||
}
|
||||
return true, "probe"
|
||||
@@ -457,10 +580,10 @@ func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
|
||||
|
||||
// 阶段4: 最终判断
|
||||
if readErr != nil {
|
||||
errLower := strings.ToLower(readErr.Error())
|
||||
errStr := readErr.Error()
|
||||
for _, pattern := range proxyFailurePatterns {
|
||||
if strings.Contains(errLower, pattern) {
|
||||
common.LogDebug(fmt.Sprintf("代理连接被拒绝 %s: %v", addr, readErr))
|
||||
if containsFold(errStr, pattern) {
|
||||
common.LogDebug(i18n.Tr("proxy_connection_rejected", addr, readErr))
|
||||
return false, "proxy_reject"
|
||||
}
|
||||
}
|
||||
@@ -470,7 +593,7 @@ func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
|
||||
// 在透明代理环境下,ProxyReliable 检测可能被污染,不可信
|
||||
// 因此采用更保守的策略:无响应一律判定为关闭
|
||||
// 这样可以避免透明代理导致的全端口误报问题
|
||||
common.LogDebug(fmt.Sprintf("代理连接无响应,判定为端口关闭 %s", addr))
|
||||
common.LogDebug(i18n.Tr("proxy_no_response_closed", addr))
|
||||
return false, "no_response"
|
||||
}
|
||||
|
||||
@@ -492,21 +615,9 @@ func isProxyErrorResponse(data []byte) bool {
|
||||
}
|
||||
|
||||
// 检查常见的代理错误文本
|
||||
dataStr := strings.ToLower(string(data))
|
||||
proxyErrorTexts := []string{
|
||||
"connection refused",
|
||||
"host unreachable",
|
||||
"network unreachable",
|
||||
"connection timed out",
|
||||
"proxy error",
|
||||
"gateway error",
|
||||
"bad gateway",
|
||||
"502",
|
||||
"503",
|
||||
}
|
||||
|
||||
dataStr := string(data)
|
||||
for _, errText := range proxyErrorTexts {
|
||||
if strings.Contains(dataStr, errText) {
|
||||
if containsFold(dataStr, errText) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -520,17 +631,9 @@ func isConnectionClosed(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
errStr := strings.ToLower(err.Error())
|
||||
closedPatterns := []string{
|
||||
"broken pipe",
|
||||
"connection reset",
|
||||
"connection refused",
|
||||
"use of closed network connection",
|
||||
"connection was forcibly closed",
|
||||
}
|
||||
|
||||
errStr := err.Error()
|
||||
for _, pattern := range closedPatterns {
|
||||
if strings.Contains(errStr, pattern) {
|
||||
if containsFold(errStr, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -539,8 +642,8 @@ func isConnectionClosed(err error) bool {
|
||||
}
|
||||
|
||||
// saveOpenPort 保存开放端口结果
|
||||
func saveOpenPort(host string, port int) {
|
||||
_ = common.SaveResult(&output.ScanResult{
|
||||
func saveOpenPort(session *common.ScanSession, host string, port int) {
|
||||
_ = session.SaveResult(&output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypePort,
|
||||
Target: host,
|
||||
@@ -550,11 +653,11 @@ func saveOpenPort(host string, port int) {
|
||||
}
|
||||
|
||||
// processServiceResult 处理服务识别结果
|
||||
func processServiceResult(host string, port int, addr string, serviceInfo *ServiceInfo, config *common.Config) {
|
||||
func processServiceResult(ctx context.Context, host string, port int, addr string, serviceInfo *ServiceInfo, config *common.Config, session *common.ScanSession) {
|
||||
if serviceInfo == nil {
|
||||
// 服务识别失败,尝试 HTTP 回退探测
|
||||
if !tryHTTPFallbackDetection(host, port, addr, config) {
|
||||
common.LogInfo(i18n.Tr("port_open", addr))
|
||||
if !tryHTTPFallbackDetection(ctx, host, port, addr, config, session) {
|
||||
session.LogInfo(i18n.Tr("port_open", addr))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -568,15 +671,15 @@ func processServiceResult(host string, port int, addr string, serviceInfo *Servi
|
||||
MarkAsWebService(host, port, serviceInfo)
|
||||
}
|
||||
|
||||
_ = common.SaveResult(&output.ScanResult{
|
||||
_ = session.SaveResult(&output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypeService,
|
||||
Target: fmt.Sprintf("%s:%d", host, port),
|
||||
Target: net.JoinHostPort(host, strconv.Itoa(port)),
|
||||
Status: "identified",
|
||||
Details: details,
|
||||
})
|
||||
|
||||
common.LogInfo(buildServiceLogMessage(addr, serviceInfo, isWeb))
|
||||
session.LogInfo(buildServiceLogMessage(addr, serviceInfo, isWeb))
|
||||
}
|
||||
|
||||
// buildServiceDetails 构建服务详情 map
|
||||
@@ -613,10 +716,10 @@ func buildServiceDetails(port int, info *ServiceInfo) map[string]interface{} {
|
||||
}
|
||||
|
||||
// tryHTTPFallbackDetection 尝试HTTP回退探测,返回是否成功识别为HTTP服务
|
||||
func tryHTTPFallbackDetection(host string, port int, addr string, config *common.Config) bool {
|
||||
func tryHTTPFallbackDetection(ctx context.Context, host string, port int, addr string, config *common.Config, session *common.ScanSession) bool {
|
||||
// 使用WebDetection进行HTTP协议探测
|
||||
webDetector := GetWebPortDetector()
|
||||
if !webDetector.DetectHTTPServiceOnly(host, port, config) {
|
||||
if !webDetector.DetectHTTPServiceOnlyContext(ctx, host, port, config, session) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -636,14 +739,150 @@ func tryHTTPFallbackDetection(host string, port int, addr string, config *common
|
||||
"is_web": true,
|
||||
"detected_by": "http_probe",
|
||||
}
|
||||
_ = common.SaveResult(&output.ScanResult{
|
||||
_ = session.SaveResult(&output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypeService,
|
||||
Target: fmt.Sprintf("%s:%d", host, port),
|
||||
Target: net.JoinHostPort(host, strconv.Itoa(port)),
|
||||
Status: "identified",
|
||||
Details: details,
|
||||
})
|
||||
|
||||
common.LogInfo(i18n.Tr("port_open_http", addr))
|
||||
session.LogInfo(i18n.Tr("port_open_http", addr))
|
||||
return true
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 网段预筛 — 大规模扫描时跳过空 /24 网段
|
||||
// =============================================================================
|
||||
|
||||
// subnetProbeThreshold 触发网段预筛的主机数阈值(超过 1 个 /24)
|
||||
const subnetProbeThreshold = 256
|
||||
|
||||
// subnetProbePorts 逐主机探活用的端口(轮换)
|
||||
var subnetProbePorts = []int{80, 443, 22, 445, 3389, 8080, 3306, 6379}
|
||||
|
||||
// gatewayProbePorts 网关启发式探测端口(网关常开的服务)
|
||||
var gatewayProbePorts = []int{22, 80, 443, 23, 8080, 161, 53, 3389}
|
||||
|
||||
// gatewayOffsets 网关候选地址偏移量
|
||||
var gatewayOffsets = []string{".1", ".254"}
|
||||
|
||||
// subnetProbeTimeout 每个探测的超时
|
||||
const subnetProbeTimeout = 1500 * time.Millisecond
|
||||
|
||||
// subnetProbeConcurrency 网段探活全局并发数
|
||||
const subnetProbeConcurrency = 500
|
||||
|
||||
// probeSubnets 对每个 /24 网段做探活,返回属于存活网段的主机列表
|
||||
// 两阶段策略:
|
||||
//
|
||||
// 阶段 1(快速):对每个子网的 .1/.254 网关做多端口探测,命中即标记存活
|
||||
// 阶段 2(兜底):未命中的子网,逐主机单端口轮换扫描
|
||||
func probeSubnets(ctx context.Context, hosts []string, timeout time.Duration, session *common.ScanSession) []string {
|
||||
// 按 /24 分组
|
||||
subnets := make(map[string][]string)
|
||||
for _, h := range hosts {
|
||||
prefix := subnetPrefix(h)
|
||||
if prefix != "" {
|
||||
subnets[prefix] = append(subnets[prefix], h)
|
||||
}
|
||||
}
|
||||
|
||||
if len(subnets) <= 1 {
|
||||
return hosts
|
||||
}
|
||||
|
||||
session.LogInfo(i18n.Tr("subnet_prefilter_start", len(subnets), len(hosts)))
|
||||
|
||||
aliveSubnets := sync.Map{}
|
||||
var wg sync.WaitGroup
|
||||
limiter := make(chan struct{}, subnetProbeConcurrency)
|
||||
|
||||
// ── 阶段 1:网关启发式 ──────────────────────────────────
|
||||
// 对每个子网的 .1 和 .254 打多个端口,命中率高且速度极快
|
||||
for prefix := range subnets {
|
||||
for _, suffix := range gatewayOffsets {
|
||||
gw := prefix + suffix
|
||||
for _, port := range gatewayProbePorts {
|
||||
wg.Add(1)
|
||||
limiter <- struct{}{}
|
||||
go func(pfx, addr string) {
|
||||
defer func() { <-limiter; wg.Done() }()
|
||||
conn, err := session.DialTCP(ctx, "tcp", addr, subnetProbeTimeout)
|
||||
if err == nil {
|
||||
_ = conn.Close()
|
||||
aliveSubnets.Store(pfx, true)
|
||||
}
|
||||
}(prefix, net.JoinHostPort(gw, strconv.Itoa(port)))
|
||||
}
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// 统计阶段 1 命中
|
||||
gwHits := 0
|
||||
aliveSubnets.Range(func(_, _ interface{}) bool { gwHits++; return true })
|
||||
|
||||
// ── 阶段 2:逐主机兜底(仅对网关未命中的子网)──────────
|
||||
for prefix, subnetHosts := range subnets {
|
||||
if _, alive := aliveSubnets.Load(prefix); alive {
|
||||
continue // 网关已命中,跳过
|
||||
}
|
||||
|
||||
for i, host := range subnetHosts {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
goto done
|
||||
default:
|
||||
}
|
||||
|
||||
if _, alive := aliveSubnets.Load(prefix); alive {
|
||||
break
|
||||
}
|
||||
|
||||
port := subnetProbePorts[i%len(subnetProbePorts)]
|
||||
wg.Add(1)
|
||||
limiter <- struct{}{}
|
||||
|
||||
go func(pfx, h string, p int) {
|
||||
defer func() { <-limiter; wg.Done() }()
|
||||
conn, err := session.DialTCP(ctx, "tcp", net.JoinHostPort(h, strconv.Itoa(p)), subnetProbeTimeout)
|
||||
if err == nil {
|
||||
_ = conn.Close()
|
||||
aliveSubnets.Store(pfx, true)
|
||||
}
|
||||
}(prefix, host, port)
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
wg.Wait()
|
||||
|
||||
// 统计
|
||||
aliveCount := 0
|
||||
aliveSubnets.Range(func(_, _ interface{}) bool { aliveCount++; return true })
|
||||
|
||||
if aliveCount == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(hosts))
|
||||
for _, h := range hosts {
|
||||
if _, alive := aliveSubnets.Load(subnetPrefix(h)); alive {
|
||||
result = append(result, h)
|
||||
}
|
||||
}
|
||||
|
||||
skipped := len(subnets) - aliveCount
|
||||
session.LogInfo(i18n.Tr("subnet_prefilter_done", aliveCount, gwHits, skipped, len(result)))
|
||||
return result
|
||||
}
|
||||
|
||||
// subnetPrefix 提取 IP 的 /24 前缀(如 "10.1.1")
|
||||
func subnetPrefix(ip string) string {
|
||||
lastDot := strings.LastIndex(ip, ".")
|
||||
if lastDot <= 0 {
|
||||
return ""
|
||||
}
|
||||
return ip[:lastDot]
|
||||
}
|
||||
|
||||
@@ -662,7 +662,7 @@ func TestIsResourceExhaustedError_EdgeCases(t *testing.T) {
|
||||
{
|
||||
name: "大小写混合",
|
||||
err: fmt.Errorf("Too Many Open Files"),
|
||||
expected: false, // 当前实现区分大小写
|
||||
expected: true, // containsFold 不区分大小写
|
||||
},
|
||||
{
|
||||
name: "错误信息包含但不完全匹配",
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// BytesToRegexSafeString 将字节切片转换为 Go regexp 安全的正则表达式模式字符串
|
||||
@@ -13,7 +15,7 @@ func BytesToRegexSafeString(b []byte) string {
|
||||
for _, c := range b {
|
||||
if c < 32 || c >= 128 {
|
||||
// 控制字符和高位字节转换为 \x{NN} 格式
|
||||
result.WriteString(fmt.Sprintf("\\x{%02x}", c))
|
||||
fmt.Fprintf(&result, "\\x{%02x}", c)
|
||||
} else {
|
||||
result.WriteByte(c)
|
||||
}
|
||||
@@ -43,7 +45,7 @@ func (p *Probe) parseMatchDirective(data, prefix string, isSoft bool) (Match, er
|
||||
// 分割文本获取pattern和版本信息
|
||||
textSplited := strings.Split(directive.DirectiveStr, directive.Delimiter)
|
||||
if len(textSplited) == 0 {
|
||||
return match, fmt.Errorf("无效的%s指令格式", prefix)
|
||||
return match, fmt.Errorf("%s", i18n.Tr("portfinger_match_directive_invalid", prefix))
|
||||
}
|
||||
|
||||
pattern := textSplited[0]
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// 解析指令语法,返回指令结构
|
||||
@@ -29,22 +31,20 @@ func (p *Probe) getDirectiveSyntax(data string) (directive Directive) {
|
||||
return directive
|
||||
}
|
||||
|
||||
// 解析探测器信息
|
||||
func (p *Probe) parseProbeInfo(probeStr string) {
|
||||
// parseProbeInfo 解析探测器信息,返回错误替代 panic
|
||||
func (p *Probe) parseProbeInfo(probeStr string) error {
|
||||
// 提取协议和其他信息
|
||||
proto := probeStr[:4]
|
||||
other := probeStr[4:]
|
||||
|
||||
// 验证协议类型
|
||||
if proto != "TCP " && proto != "UDP " {
|
||||
errMsg := "探测器协议必须是 TCP 或 UDP"
|
||||
panic(errMsg)
|
||||
return fmt.Errorf("%s", i18n.GetText("portfinger_probe_protocol_invalid"))
|
||||
}
|
||||
|
||||
// 验证其他信息不为空
|
||||
if len(other) == 0 {
|
||||
errMsg := "nmap-service-probes - 探测器名称无效"
|
||||
panic(errMsg)
|
||||
return fmt.Errorf("%s", i18n.GetText("portfinger_probe_name_invalid"))
|
||||
}
|
||||
|
||||
// 解析指令
|
||||
@@ -55,6 +55,7 @@ func (p *Probe) parseProbeInfo(probeStr string) {
|
||||
p.Data = strings.Split(directive.DirectiveStr, directive.Delimiter)[0]
|
||||
p.Protocol = strings.ToLower(strings.TrimSpace(proto))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 从字符串解析探测器信息
|
||||
@@ -65,11 +66,13 @@ func (p *Probe) fromString(data string) error {
|
||||
data = strings.TrimSpace(data)
|
||||
lines := strings.Split(data, "\n")
|
||||
if len(lines) == 0 {
|
||||
return fmt.Errorf("输入数据为空")
|
||||
return fmt.Errorf("%s", i18n.GetText("portfinger_input_empty"))
|
||||
}
|
||||
|
||||
probeStr := lines[0]
|
||||
p.parseProbeInfo(probeStr)
|
||||
if err := p.parseProbeInfo(probeStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析匹配规则和其他配置
|
||||
var matchs []Match
|
||||
@@ -154,8 +157,8 @@ func (p *Probe) parseFallback(data string) {
|
||||
p.Fallback = data[len("fallback")+1:]
|
||||
}
|
||||
|
||||
// 从内容解析探测器规则
|
||||
func (v *VScan) parseProbesFromContent(content string) {
|
||||
// parseProbesFromContent 从内容解析探测器规则,返回错误替代 panic
|
||||
func (v *VScan) parseProbesFromContent(content string) error {
|
||||
var probes []Probe
|
||||
var lines []string
|
||||
|
||||
@@ -171,8 +174,7 @@ func (v *VScan) parseProbesFromContent(content string) {
|
||||
|
||||
// 验证文件内容
|
||||
if len(lines) == 0 {
|
||||
errMsg := "读取nmap-service-probes文件失败: 内容为空"
|
||||
panic(errMsg)
|
||||
return fmt.Errorf("%s", i18n.GetText("portfinger_probe_file_empty"))
|
||||
}
|
||||
|
||||
// 检查Exclude指令
|
||||
@@ -182,16 +184,14 @@ func (v *VScan) parseProbesFromContent(content string) {
|
||||
excludeCount++
|
||||
}
|
||||
if excludeCount > 1 {
|
||||
errMsg := "nmap-service-probes文件中只允许有一个Exclude指令"
|
||||
panic(errMsg)
|
||||
return fmt.Errorf("%s", i18n.GetText("portfinger_probe_exclude_duplicate"))
|
||||
}
|
||||
}
|
||||
|
||||
// 验证第一行格式
|
||||
firstLine := lines[0]
|
||||
if !strings.HasPrefix(firstLine, "Exclude ") && !strings.HasPrefix(firstLine, "Probe ") {
|
||||
errMsg := "解析错误: 首行必须以\"Probe \"或\"Exclude \"开头"
|
||||
panic(errMsg)
|
||||
return fmt.Errorf("%s", i18n.GetText("portfinger_probe_first_line_invalid"))
|
||||
}
|
||||
|
||||
// 处理Exclude指令
|
||||
@@ -214,6 +214,7 @@ func (v *VScan) parseProbesFromContent(content string) {
|
||||
}
|
||||
|
||||
v.AllProbes = probes
|
||||
return nil
|
||||
}
|
||||
|
||||
// 将探测器转换为名称映射
|
||||
|
||||
@@ -19,12 +19,32 @@ var (
|
||||
vscanOnce sync.Once
|
||||
)
|
||||
|
||||
// Init 初始化VScan对象
|
||||
func (vs *VScan) Init() {
|
||||
vs.parseProbesFromContent(ProbeString)
|
||||
// Init 初始化VScan对象,返回错误替代panic
|
||||
func (vs *VScan) Init() error {
|
||||
if err := vs.parseProbesFromContent(ProbeString); err != nil {
|
||||
return err
|
||||
}
|
||||
vs.parseProbesToMapKName()
|
||||
vs.SetusedProbes()
|
||||
vs.compileFallbacks() // 编译 fallback 数组
|
||||
vs.compileFallbacks()
|
||||
vs.preDecodeProbeData()
|
||||
return nil
|
||||
}
|
||||
|
||||
// preDecodeProbeData 预解码所有探针的 Data 字段,避免运行时重复解码
|
||||
func (vs *VScan) preDecodeProbeData() {
|
||||
for i := range vs.Probes {
|
||||
if vs.Probes[i].Data != "" {
|
||||
decoded, err := DecodeData(vs.Probes[i].Data)
|
||||
if err == nil {
|
||||
vs.Probes[i].DecodedData = decoded
|
||||
}
|
||||
}
|
||||
}
|
||||
// 同步到 map
|
||||
for i := range vs.Probes {
|
||||
vs.ProbesMapKName[vs.Probes[i].Name] = vs.Probes[i]
|
||||
}
|
||||
}
|
||||
|
||||
// compileFallbacks 编译所有探测器的 fallback 数组
|
||||
@@ -83,10 +103,14 @@ func (vs *VScan) compileFallbacks() {
|
||||
}
|
||||
|
||||
// InitializeGlobalVScan 初始化全局VScan实例(线程安全,只执行一次)
|
||||
func InitializeGlobalVScan() {
|
||||
func InitializeGlobalVScan() error {
|
||||
var initErr error
|
||||
vscanOnce.Do(func() {
|
||||
globalVScan = VScan{}
|
||||
globalVScan.Init()
|
||||
if err := globalVScan.Init(); err != nil {
|
||||
initErr = err
|
||||
return
|
||||
}
|
||||
|
||||
// 获取并检查 NULL 探测器
|
||||
if nullProbe, ok := globalVScan.ProbesMapKName["NULL"]; ok {
|
||||
@@ -98,26 +122,27 @@ func InitializeGlobalVScan() {
|
||||
globalCommon = &genericProbe
|
||||
}
|
||||
})
|
||||
return initErr
|
||||
}
|
||||
|
||||
// GetGlobalVScan 获取全局VScan实例
|
||||
func GetGlobalVScan() *VScan {
|
||||
InitializeGlobalVScan() // 确保已初始化
|
||||
_ = InitializeGlobalVScan() // 确保已初始化
|
||||
return &globalVScan
|
||||
}
|
||||
|
||||
// GetNullProbe 获取NULL探测器
|
||||
func GetNullProbe() *Probe {
|
||||
InitializeGlobalVScan() // 确保已初始化
|
||||
_ = InitializeGlobalVScan() // 确保已初始化
|
||||
return globalNull
|
||||
}
|
||||
|
||||
// GetCommonProbe 获取通用探测器
|
||||
func GetCommonProbe() *Probe {
|
||||
InitializeGlobalVScan() // 确保已初始化
|
||||
_ = InitializeGlobalVScan() // 确保已初始化
|
||||
return globalCommon
|
||||
}
|
||||
|
||||
func init() {
|
||||
InitializeGlobalVScan()
|
||||
_ = InitializeGlobalVScan()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ const MaxFallbacks = 20
|
||||
type Probe struct {
|
||||
Name string // 探测器名称
|
||||
Data string // 探测数据
|
||||
DecodedData []byte // 预解码的探测数据
|
||||
Protocol string // 协议
|
||||
Ports string // 端口范围
|
||||
SSLPorts string // SSL端口范围
|
||||
|
||||
+150
-27
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
@@ -16,9 +17,23 @@ import (
|
||||
"github.com/shadow1ng/fscan/webscan/lib"
|
||||
)
|
||||
|
||||
// ScanReport summarizes one scan execution.
|
||||
type ScanReport struct {
|
||||
Duration time.Duration
|
||||
TasksTotal int64
|
||||
TasksCompleted int64
|
||||
Packets int64
|
||||
TCPPackets int64
|
||||
TCPSuccessPackets int64
|
||||
TCPFailedPackets int64
|
||||
UDPPackets int64
|
||||
HTTPPackets int64
|
||||
ResourceExhausted int64
|
||||
}
|
||||
|
||||
// ScanStrategy 定义扫描策略接口
|
||||
type ScanStrategy interface {
|
||||
Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup)
|
||||
Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup)
|
||||
GetPlugins(config *common.Config) ([]string, bool)
|
||||
IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool, config *common.Config) bool
|
||||
}
|
||||
@@ -53,6 +68,10 @@ func determineScanMode(config *common.Config, state *common.State) ScanMode {
|
||||
return ScanModeAlive
|
||||
case config.LocalMode:
|
||||
return ScanModeLocal
|
||||
case common.IsLocalMode != nil && common.IsLocalMode(config.Mode):
|
||||
config.LocalMode = true
|
||||
config.LocalPlugin = config.Mode
|
||||
return ScanModeLocal
|
||||
case len(state.GetURLs()) > 0:
|
||||
return ScanModeWeb
|
||||
default:
|
||||
@@ -73,11 +92,18 @@ func selectStrategy(config *common.Config, state *common.State, info common.Host
|
||||
}
|
||||
|
||||
// RunScan 执行整体扫描流程
|
||||
func RunScan(info common.HostInfo, config *common.Config, state *common.State) {
|
||||
func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSession) (ScanReport, error) {
|
||||
start := time.Now()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
config := session.Config
|
||||
state := session.State
|
||||
|
||||
// 初始化HTTP客户端(静默,无需日志)
|
||||
if err := lib.Inithttp(config); err != nil {
|
||||
common.LogError(i18n.Tr("http_client_init_failed", err))
|
||||
os.Exit(1)
|
||||
session.LogError(i18n.Tr("http_client_init_failed", err))
|
||||
return buildScanReport(state, start), fmt.Errorf("initialize http client: %w", err)
|
||||
}
|
||||
|
||||
// 选择策略
|
||||
@@ -88,7 +114,7 @@ func RunScan(info common.HostInfo, config *common.Config, state *common.State) {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
// 执行策略
|
||||
strategy.Execute(config, state, info, ch, &wg)
|
||||
strategy.Execute(ctx, session, info, ch, &wg)
|
||||
|
||||
// 等待所有扫描完成
|
||||
wg.Wait()
|
||||
@@ -96,36 +122,63 @@ func RunScan(info common.HostInfo, config *common.Config, state *common.State) {
|
||||
// 检查是否有活跃的连接需要维持
|
||||
if state.IsReverseShellActive() || state.IsSocks5ProxyActive() || state.IsForwardShellActive() {
|
||||
if state.IsReverseShellActive() {
|
||||
common.LogInfo(i18n.GetText("active_reverse_shell"))
|
||||
session.LogInfo(i18n.GetText("active_reverse_shell"))
|
||||
}
|
||||
if state.IsSocks5ProxyActive() {
|
||||
common.LogInfo(i18n.GetText("active_socks5_proxy"))
|
||||
session.LogInfo(i18n.GetText("active_socks5_proxy"))
|
||||
}
|
||||
if state.IsForwardShellActive() {
|
||||
common.LogInfo(i18n.GetText("active_forward_shell"))
|
||||
session.LogInfo(i18n.GetText("active_forward_shell"))
|
||||
}
|
||||
common.LogInfo(i18n.GetText("press_ctrl_c_exit"))
|
||||
session.LogInfo(i18n.GetText("press_ctrl_c_exit"))
|
||||
|
||||
// 优雅等待信号
|
||||
// 优雅等待信号或 context 取消(Web Stop)
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
common.LogInfo(i18n.GetText("received_exit_signal"))
|
||||
select {
|
||||
case <-sigChan:
|
||||
session.LogInfo(i18n.GetText("received_exit_signal"))
|
||||
case <-ctx.Done():
|
||||
}
|
||||
cancel()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 完成扫描
|
||||
finishScan(config, state)
|
||||
finishScan(session)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return buildScanReport(state, start), err
|
||||
}
|
||||
return buildScanReport(state, start), nil
|
||||
}
|
||||
|
||||
func buildScanReport(state *common.State, start time.Time) ScanReport {
|
||||
return ScanReport{
|
||||
Duration: time.Since(start),
|
||||
TasksTotal: state.GetEnd(),
|
||||
TasksCompleted: state.GetNum(),
|
||||
Packets: state.GetPacketCount(),
|
||||
TCPPackets: state.GetTCPPacketCount(),
|
||||
TCPSuccessPackets: state.GetTCPSuccessPacketCount(),
|
||||
TCPFailedPackets: state.GetTCPFailedPacketCount(),
|
||||
UDPPackets: state.GetUDPPacketCount(),
|
||||
HTTPPackets: state.GetHTTPPacketCount(),
|
||||
ResourceExhausted: state.GetResourceExhaustedCount(),
|
||||
}
|
||||
}
|
||||
|
||||
// finishScan 完成扫描并输出结果
|
||||
func finishScan(config *common.Config, state *common.State) {
|
||||
func finishScan(session *common.ScanSession) {
|
||||
config := session.Config
|
||||
state := session.State
|
||||
|
||||
// 确保进度条正确完成
|
||||
if common.IsProgressActive() {
|
||||
common.FinishProgressBar()
|
||||
}
|
||||
|
||||
// 输出扫描完成信息
|
||||
common.LogInfo(i18n.Tr("scan_task_complete", time.Since(state.GetStartTime()).Round(time.Millisecond), state.GetNum()))
|
||||
session.LogInfo(i18n.Tr("scan_task_complete", time.Since(state.GetStartTime()).Round(time.Millisecond), state.GetNum()))
|
||||
|
||||
// 输出性能统计 JSON(如果启用)
|
||||
if config.Output.PerfStats {
|
||||
@@ -134,7 +187,9 @@ func finishScan(config *common.Config, state *common.State) {
|
||||
}
|
||||
|
||||
// ExecuteScanTasks 任务执行通用框架
|
||||
func ExecuteScanTasks(config *common.Config, state *common.State, targets []common.HostInfo, strategy ScanStrategy, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func ExecuteScanTasks(ctx context.Context, session *common.ScanSession, targets []common.HostInfo, strategy ScanStrategy, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
config := session.Config
|
||||
|
||||
// 获取要执行的插件
|
||||
pluginsToRun, isCustomMode := strategy.GetPlugins(config)
|
||||
|
||||
@@ -149,6 +204,19 @@ func ExecuteScanTasks(config *common.Config, state *common.State, targets []comm
|
||||
|
||||
// 流式执行任务,避免预构建大量任务对象
|
||||
for _, target := range targets {
|
||||
// 检查取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if session.PauseGate != nil {
|
||||
if err := session.PauseGate(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
targetPort := target.Port
|
||||
|
||||
for _, pluginName := range pluginsToRun {
|
||||
@@ -159,7 +227,7 @@ func ExecuteScanTasks(config *common.Config, state *common.State, targets []comm
|
||||
|
||||
// 检查插件是否适用于当前目标
|
||||
if strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode, config) {
|
||||
executeScanTask(config, state, pluginName, target, ch, wg)
|
||||
executeScanTask(ctx, session, pluginName, target, ch, wg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,10 +250,58 @@ func countApplicableTasks(targets []common.HostInfo, pluginsToRun []string, isCu
|
||||
return count
|
||||
}
|
||||
|
||||
// longRunningPlugins 长驻插件,不加入 scan WaitGroup,通过 ctx 取消退出
|
||||
var longRunningPlugins = map[string]bool{
|
||||
"forwardshell": true,
|
||||
"socks5proxy": true,
|
||||
"reverseshell": true,
|
||||
}
|
||||
|
||||
// executeScanTask 执行单个扫描任务
|
||||
func executeScanTask(config *common.Config, state *common.State, pluginName string, target common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func executeScanTask(ctx context.Context, session *common.ScanSession, pluginName string, target common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
state := session.State
|
||||
|
||||
// 检查取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if session.PauseGate != nil {
|
||||
if err := session.PauseGate(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 长驻插件不进 WaitGroup,通过 ctx 管理生命周期
|
||||
if longRunningPlugins[pluginName] {
|
||||
ready := make(chan struct{}, 1)
|
||||
go func() {
|
||||
plugin := plugins.Get(pluginName)
|
||||
if plugin != nil {
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
ready <- struct{}{}
|
||||
}()
|
||||
plugin.Scan(ctx, &target, session)
|
||||
} else {
|
||||
ready <- struct{}{}
|
||||
}
|
||||
}()
|
||||
<-ready
|
||||
return
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
ch <- struct{}{} // 获取并发槽位
|
||||
|
||||
// 获取并发槽位,支持取消
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
// 开始监控插件任务
|
||||
@@ -195,7 +311,7 @@ func executeScanTask(config *common.Config, state *common.State, pluginName stri
|
||||
defer func() {
|
||||
// 捕获并记录任何可能的panic
|
||||
if r := recover(); r != nil {
|
||||
common.LogError(i18n.Tr("plugin_panic", pluginName, target.Host, target.Port, r))
|
||||
session.LogError(i18n.Tr("plugin_panic", pluginName, target.Host, target.Port, r))
|
||||
}
|
||||
|
||||
// 更新统计和进度(任务真正完成时才更新)
|
||||
@@ -210,17 +326,17 @@ func executeScanTask(config *common.Config, state *common.State, pluginName stri
|
||||
|
||||
plugin := plugins.Get(pluginName)
|
||||
if plugin != nil {
|
||||
result := plugin.Scan(context.Background(), &target, config, state)
|
||||
result := plugin.Scan(ctx, &target, session)
|
||||
if result != nil {
|
||||
if result.Success {
|
||||
// 保存成功的扫描结果到文件
|
||||
savePluginResult(&target, pluginName, result)
|
||||
savePluginResult(session, &target, pluginName, result)
|
||||
} else if result.Type == plugins.ResultTypeCredential {
|
||||
// 凭据测试完成但未发现弱密码,在error级别输出提示
|
||||
common.LogError(i18n.Tr("brute_no_weak_pass", target.Host, target.Port, pluginName))
|
||||
session.LogError(i18n.Tr("brute_no_weak_pass", target.Host, target.Port, pluginName))
|
||||
} else if result.Error != nil {
|
||||
// 其他类型的错误
|
||||
common.LogError(i18n.Tr("plugin_scan_error", target.Host, target.Port, result.Error))
|
||||
session.LogError(i18n.Tr("plugin_scan_error", target.Host, target.Port, result.Error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,9 +402,16 @@ var resultSerializers = map[plugins.ResultType]resultSerializer{
|
||||
plugins.ResultTypeWeb: {
|
||||
outputType: output.TypeService,
|
||||
getStatus: func(_ *plugins.Result, _ *common.HostInfo) string { return "web" },
|
||||
fillDetail: func(_ *plugins.Result, info *common.HostInfo, d map[string]interface{}) {
|
||||
fillDetail: func(r *plugins.Result, info *common.HostInfo, d map[string]interface{}) {
|
||||
d["is_web"] = true
|
||||
d["port"] = info.Port
|
||||
if r.Output == "" {
|
||||
return
|
||||
}
|
||||
d["url"] = r.Output
|
||||
if parsed, err := url.Parse(r.Output); err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https") {
|
||||
d["protocol"] = parsed.Scheme
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -308,7 +431,7 @@ var defaultSerializer = resultSerializer{
|
||||
}
|
||||
|
||||
// savePluginResult 保存插件扫描结果
|
||||
func savePluginResult(info *common.HostInfo, pluginName string, result *plugins.Result) {
|
||||
func savePluginResult(session *common.ScanSession, info *common.HostInfo, pluginName string, result *plugins.Result) {
|
||||
if result == nil || !result.Success || result.Skipped {
|
||||
return
|
||||
}
|
||||
@@ -328,7 +451,7 @@ func savePluginResult(info *common.HostInfo, pluginName string, result *plugins.
|
||||
|
||||
// 保存结果
|
||||
target := info.Target()
|
||||
_ = common.SaveResult(&output.ScanResult{
|
||||
_ = session.SaveResult(&output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: serializer.outputType,
|
||||
Target: target,
|
||||
|
||||
+23
-1
@@ -1,11 +1,13 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -34,6 +36,26 @@ scanner_test.go - Scanner核心逻辑测试
|
||||
// 核心逻辑测试:策略选择
|
||||
// =============================================================================
|
||||
|
||||
func TestWebResultSerializerPreservesDetectedProtocol(t *testing.T) {
|
||||
serializer := resultSerializers[plugins.ResultTypeWeb]
|
||||
details := map[string]interface{}{}
|
||||
result := &plugins.Result{
|
||||
Type: plugins.ResultTypeWeb,
|
||||
Success: true,
|
||||
Output: "https://192.168.1.1:8443",
|
||||
}
|
||||
info := &common.HostInfo{Host: "192.168.1.1", Port: 8443}
|
||||
|
||||
serializer.fillDetail(result, info, details)
|
||||
|
||||
if details["protocol"] != "https" {
|
||||
t.Fatalf("protocol = %v, 期望 https", details["protocol"])
|
||||
}
|
||||
if details["url"] != "https://192.168.1.1:8443" {
|
||||
t.Fatalf("url = %v, 期望检测出的URL", details["url"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelectStrategy 测试策略选择逻辑
|
||||
func TestSelectStrategy(t *testing.T) {
|
||||
// 保存原始配置
|
||||
@@ -214,7 +236,7 @@ type mockStrategy struct {
|
||||
applicablePlugins map[string]bool // pluginName -> isApplicable
|
||||
}
|
||||
|
||||
func (m *mockStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func (m *mockStrategy) Execute(_ context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
}
|
||||
|
||||
func (m *mockStrategy) GetPlugins() ([]string, bool) {
|
||||
|
||||
+65
-29
@@ -1,21 +1,23 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/core/portfinger"
|
||||
)
|
||||
|
||||
// 默认超时时间常量
|
||||
const (
|
||||
defaultTotalWaitMS = 6000 // Nmap 默认等待时间
|
||||
defaultTotalWaitMS = 3000 // 服务探测默认等待时间
|
||||
defaultIntensity = 7 // 默认探测强度 (1-9)
|
||||
)
|
||||
|
||||
@@ -70,13 +72,16 @@ type Service struct {
|
||||
|
||||
// Info 定义单个端口探测的上下文信息
|
||||
type Info struct {
|
||||
Address string // 目标IP地址
|
||||
Port int // 目标端口
|
||||
Conn net.Conn // 网络连接
|
||||
Result Result // 探测结果
|
||||
Found bool // 是否成功识别服务
|
||||
config *common.Config // 配置引用
|
||||
readTimeoutMS int // 当前读取超时时间(毫秒)
|
||||
Address string // 目标IP地址
|
||||
Port int // 目标端口
|
||||
Conn net.Conn // 网络连接
|
||||
Result Result // 探测结果
|
||||
Found bool // 是否成功识别服务
|
||||
ctx context.Context // 扫描级 context
|
||||
config *common.Config // 配置引用
|
||||
session *common.ScanSession // 会话引用
|
||||
readTimeoutMS int // 当前读取超时时间(毫秒)
|
||||
maxReadTimeoutMS int // RTT 自适应上限(毫秒),0 表示不限制
|
||||
}
|
||||
|
||||
// SmartPortInfoScanner 智能服务识别器:保持nmap准确性,优化网络交互
|
||||
@@ -86,24 +91,28 @@ type SmartPortInfoScanner struct {
|
||||
Conn net.Conn
|
||||
Timeout time.Duration
|
||||
info *Info
|
||||
config *common.Config // 配置引用
|
||||
config *common.Config // 配置引用
|
||||
session *common.ScanSession // 会话引用
|
||||
}
|
||||
|
||||
// 预定义的基础探测器已在PortFinger.go中定义,这里不再重复定义
|
||||
|
||||
// NewSmartPortInfoScanner 创建智能服务识别器
|
||||
func NewSmartPortInfoScanner(addr string, port int, conn net.Conn, timeout time.Duration, config *common.Config) *SmartPortInfoScanner {
|
||||
func NewSmartPortInfoScanner(ctx context.Context, addr string, port int, conn net.Conn, timeout time.Duration, config *common.Config, session *common.ScanSession) *SmartPortInfoScanner {
|
||||
return &SmartPortInfoScanner{
|
||||
Address: addr,
|
||||
Port: port,
|
||||
Conn: conn,
|
||||
Timeout: timeout,
|
||||
config: config,
|
||||
session: session,
|
||||
info: &Info{
|
||||
Address: addr,
|
||||
Port: port,
|
||||
Conn: conn,
|
||||
ctx: ctx,
|
||||
config: config,
|
||||
session: session,
|
||||
Result: Result{
|
||||
Service: Service{},
|
||||
},
|
||||
@@ -163,7 +172,6 @@ func (s *SmartPortInfoScanner) tryInitialBanner() ([]byte, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
|
||||
// smartProbeStrategy 智能探测策略
|
||||
// 改进版:使用 nmap-service-probes.txt 中的 ports 字段和 rarity 排序
|
||||
func (s *SmartPortInfoScanner) smartProbeStrategy() {
|
||||
@@ -211,9 +219,14 @@ func (s *SmartPortInfoScanner) tryProbeList(probes []*Probe, usedProbes map[stri
|
||||
}
|
||||
usedProbes[probe.Name] = struct{}{}
|
||||
|
||||
probeData, err := DecodeData(probe.Data)
|
||||
if err != nil {
|
||||
continue
|
||||
// 优先使用预解码数据
|
||||
probeData := probe.DecodedData
|
||||
if probeData == nil {
|
||||
var err error
|
||||
probeData, err = DecodeData(probe.Data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 TotalWaitMS 设置动态超时
|
||||
@@ -251,7 +264,7 @@ func (s *SmartPortInfoScanner) reconnectIfNeeded() {
|
||||
}
|
||||
|
||||
// 重新建立连接
|
||||
newConn, err := common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%d", s.Address, s.Port), s.Timeout)
|
||||
newConn, err := s.session.DialTCP(s.info.ctx, "tcp", net.JoinHostPort(s.Address, strconv.Itoa(s.Port)), s.Timeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -274,8 +287,15 @@ func (s *SmartPortInfoScanner) performSSLSecondStage(serviceInfo *ServiceInfo) *
|
||||
continue
|
||||
}
|
||||
|
||||
probeData, err := DecodeData(probe.Data)
|
||||
if err != nil || len(probeData) == 0 {
|
||||
probeData := probe.DecodedData
|
||||
if probeData == nil {
|
||||
var decErr error
|
||||
probeData, decErr = DecodeData(probe.Data)
|
||||
if decErr != nil || len(probeData) == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(probeData) == 0 {
|
||||
continue
|
||||
}
|
||||
response := s.info.Connect(probeData)
|
||||
@@ -309,8 +329,15 @@ func (s *SmartPortInfoScanner) tryHTTPSProbe() *ServiceInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
probeData, err := DecodeData(probe.Data)
|
||||
if err != nil || len(probeData) == 0 {
|
||||
probeData := probe.DecodedData
|
||||
if probeData == nil {
|
||||
var decErr error
|
||||
probeData, decErr = DecodeData(probe.Data)
|
||||
if decErr != nil || len(probeData) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if len(probeData) == 0 {
|
||||
return nil
|
||||
}
|
||||
response := s.info.Connect(probeData)
|
||||
@@ -365,7 +392,7 @@ func (i *Info) tryProbes(response []byte, probes []*Probe) bool {
|
||||
func (i *Info) GetInfo(response []byte, probe *Probe) {
|
||||
// 响应数据有效性检查
|
||||
if len(response) <= 0 {
|
||||
common.LogDebug("响应数据为空")
|
||||
common.LogDebug(i18n.GetText("service_probe_empty_response"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -433,12 +460,12 @@ func (i *Info) handleHardMatch(response []byte, match *Match) {
|
||||
|
||||
// 特殊处理 microsoft-ds 服务
|
||||
if result.Service.Name == "microsoft-ds" {
|
||||
common.LogDebug("特殊处理 microsoft-ds 服务")
|
||||
common.LogDebug(i18n.GetText("service_probe_microsoft_ds"))
|
||||
result.Service.Extras["hostname"] = result.Banner
|
||||
}
|
||||
|
||||
i.Found = true
|
||||
common.LogDebug(fmt.Sprintf("服务识别结果: %s, Banner: %s", result.Service.Name, result.Banner))
|
||||
common.LogDebug(i18n.Tr("service_probe_identified", result.Service.Name, result.Banner))
|
||||
}
|
||||
|
||||
// handleNoMatch 处理未找到匹配的情况
|
||||
@@ -450,10 +477,10 @@ func (i *Info) handleNoMatch(response []byte, result *Result, softFound bool, so
|
||||
bannerLower := strings.ToLower(result.Banner)
|
||||
if strings.Contains(bannerLower, "http/") ||
|
||||
strings.Contains(bannerLower, "html") {
|
||||
common.LogDebug("识别为HTTP服务")
|
||||
common.LogDebug(i18n.GetText("service_probe_http_identified"))
|
||||
result.Service.Name = "http"
|
||||
} else {
|
||||
common.LogDebug("未知服务")
|
||||
common.LogDebug(i18n.GetText("service_probe_unknown"))
|
||||
result.Service.Name = "unknown"
|
||||
}
|
||||
} else {
|
||||
@@ -461,7 +488,7 @@ func (i *Info) handleNoMatch(response []byte, result *Result, softFound bool, so
|
||||
result.Service.Extras = extras.ToMap()
|
||||
result.Service.Name = softMatch.Service
|
||||
i.Found = true
|
||||
common.LogDebug(fmt.Sprintf("软匹配服务: %s", result.Service.Name))
|
||||
common.LogDebug(i18n.Tr("service_probe_soft_match", result.Service.Name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,10 +508,14 @@ func (i *Info) setReadTimeout(ms int) {
|
||||
|
||||
// getReadTimeout 获取当前读取超时时间
|
||||
func (i *Info) getReadTimeout() time.Duration {
|
||||
ms := defaultReadTimeoutMS
|
||||
if i.readTimeoutMS > 0 {
|
||||
return time.Duration(i.readTimeoutMS) * time.Millisecond
|
||||
ms = i.readTimeoutMS
|
||||
}
|
||||
return time.Duration(defaultReadTimeoutMS) * time.Millisecond
|
||||
if i.maxReadTimeoutMS > 0 && ms > i.maxReadTimeoutMS {
|
||||
ms = i.maxReadTimeoutMS
|
||||
}
|
||||
return time.Duration(ms) * time.Millisecond
|
||||
}
|
||||
|
||||
// WrTimeout 默认读写超时时间(秒)
|
||||
@@ -511,7 +542,7 @@ func (i *Info) Write(msg []byte) error {
|
||||
_ = oldConn.Close()
|
||||
|
||||
// 尝试重新连接 - 支持SOCKS5代理
|
||||
newConn, retryErr := common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%d", i.Address, i.Port), time.Duration(6)*time.Second)
|
||||
newConn, retryErr := i.session.DialTCP(i.ctx, "tcp", net.JoinHostPort(i.Address, strconv.Itoa(i.Port)), time.Duration(6)*time.Second)
|
||||
if retryErr != nil {
|
||||
return retryErr
|
||||
}
|
||||
@@ -576,6 +607,11 @@ func readFromConn(conn net.Conn) ([]byte, error) {
|
||||
|
||||
var result []byte
|
||||
|
||||
// 预分配 4KB,消除大部分服务 Banner 场景下的 append 扩容
|
||||
if cap(buf) > 0 {
|
||||
result = make([]byte, 0, 4096)
|
||||
}
|
||||
|
||||
for {
|
||||
count, err := conn.Read(buf)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ service_probe_strategy_test.go - SmartProbeStrategy 策略逻辑测试
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -155,7 +156,7 @@ func TestSmartPortInfoScanner_Creation(t *testing.T) {
|
||||
}
|
||||
|
||||
// 使用 nil 连接(实际测试中会使用真实连接)
|
||||
scanner := NewSmartPortInfoScanner("127.0.0.1", 80, nil, 3*time.Second, config)
|
||||
scanner := NewSmartPortInfoScanner(context.Background(), "127.0.0.1", 80, nil, 3*time.Second, config, nil)
|
||||
|
||||
if scanner == nil {
|
||||
t.Fatal("Scanner 创建失败")
|
||||
@@ -175,8 +176,8 @@ func TestSmartPortInfoScanner_Creation(t *testing.T) {
|
||||
// TestDefaultConstants 验证默认常量值
|
||||
func TestDefaultConstants(t *testing.T) {
|
||||
// 验证默认等待时间
|
||||
if defaultTotalWaitMS != 6000 {
|
||||
t.Errorf("defaultTotalWaitMS 应该是 6000,实际是 %d", defaultTotalWaitMS)
|
||||
if defaultTotalWaitMS != 3000 {
|
||||
t.Errorf("defaultTotalWaitMS 应该是 3000,实际是 %d", defaultTotalWaitMS)
|
||||
}
|
||||
|
||||
// 验证默认 intensity
|
||||
|
||||
+207
-51
@@ -1,7 +1,9 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -9,6 +11,7 @@ import (
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/common/parsers"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// ServiceScanStrategy 服务扫描策略
|
||||
@@ -19,32 +22,32 @@ type ServiceScanStrategy struct {
|
||||
// NewServiceScanStrategy 创建新的服务扫描策略
|
||||
func NewServiceScanStrategy() *ServiceScanStrategy {
|
||||
return &ServiceScanStrategy{
|
||||
BaseScanStrategy: NewBaseScanStrategy("服务扫描", FilterService),
|
||||
BaseScanStrategy: NewBaseScanStrategy(i18n.GetText("scan_strategy_service_name"), FilterService),
|
||||
}
|
||||
}
|
||||
|
||||
// LogPluginInfo 重写以提供基于端口的插件过滤
|
||||
func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config) {
|
||||
func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) {
|
||||
// 需要从命令行参数获取端口信息来进行过滤
|
||||
// 如果没有指定端口,使用默认端口进行过滤显示
|
||||
ports := common.GetFlagVars().Ports
|
||||
ports := config.Target.Ports
|
||||
if ports == "" || ports == "all" {
|
||||
// 默认端口扫描:显示所有插件
|
||||
s.BaseScanStrategy.LogPluginInfo(config)
|
||||
s.BaseScanStrategy.LogPluginInfo(config, session)
|
||||
} else {
|
||||
// 指定端口扫描:只显示匹配的插件
|
||||
s.showPluginsForSpecifiedPorts(config)
|
||||
s.showPluginsForSpecifiedPorts(config, session)
|
||||
}
|
||||
}
|
||||
|
||||
// showPluginsForSpecifiedPorts 显示指定端口的匹配插件
|
||||
func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config) {
|
||||
func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config, session *common.ScanSession) {
|
||||
allPlugins, isCustomMode := s.GetPlugins(config)
|
||||
|
||||
// 解析端口
|
||||
ports := s.parsePortList(common.GetFlagVars().Ports)
|
||||
ports := s.parsePortList(config.Target.Ports)
|
||||
if len(ports) == 0 {
|
||||
s.BaseScanStrategy.LogPluginInfo(config)
|
||||
s.BaseScanStrategy.LogPluginInfo(config, session)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,12 +73,12 @@ func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config
|
||||
if len(applicablePlugins) > 0 {
|
||||
pluginStr := formatPluginList(applicablePlugins)
|
||||
if isCustomMode {
|
||||
common.LogInfo(i18n.Tr("service_plugin_custom", pluginStr))
|
||||
session.LogInfo(i18n.Tr("service_plugin_custom", pluginStr))
|
||||
} else {
|
||||
common.LogInfo(i18n.Tr("service_plugin_info", pluginStr))
|
||||
session.LogInfo(i18n.Tr("service_plugin_info", pluginStr))
|
||||
}
|
||||
} else {
|
||||
common.LogInfo(i18n.GetText("service_plugin_none"))
|
||||
session.LogInfo(i18n.GetText("service_plugin_none"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,51 +115,176 @@ func (s *ServiceScanStrategy) Description() string {
|
||||
}
|
||||
|
||||
// Execute 执行服务扫描策略
|
||||
func (s *ServiceScanStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func (s *ServiceScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
config := session.Config
|
||||
|
||||
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
|
||||
fv := common.GetFlagVars()
|
||||
if info.Host == "" && fv.HostsFile == "" {
|
||||
common.LogError(i18n.GetText("parse_error_target_empty"))
|
||||
if info.Host == "" && session.Params.HostsFile == "" {
|
||||
session.LogError(i18n.GetText("parse_error_target_empty"))
|
||||
return
|
||||
}
|
||||
|
||||
// 输出扫描开始信息
|
||||
s.LogScanStart()
|
||||
s.LogScanStart(session)
|
||||
|
||||
// 验证插件配置
|
||||
if err := s.ValidateConfiguration(); err != nil {
|
||||
common.LogError(err.Error())
|
||||
session.LogError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 输出插件信息(重写以提供端口过滤)
|
||||
s.LogPluginInfo(config)
|
||||
s.LogPluginInfo(config, session)
|
||||
|
||||
// 执行主机扫描流程
|
||||
s.performHostScan(config, state, info, ch, wg)
|
||||
s.performHostScan(ctx, session, info, ch, wg)
|
||||
}
|
||||
|
||||
// performHostScan 执行主机扫描的完整流程
|
||||
func (s *ServiceScanStrategy) performHostScan(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
// 发现目标主机和端口
|
||||
targetInfos, err := s.discoverTargets(info.Host, info, config, state)
|
||||
// pipeline 模式:端口扫描和插件执行并行,扫到开放端口立即开始跑插件
|
||||
func (s *ServiceScanStrategy) performHostScan(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
config := session.Config
|
||||
state := session.State
|
||||
|
||||
excludes, err := loadHostExcludes(session.Params)
|
||||
if err != nil {
|
||||
common.LogError(err.Error())
|
||||
session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err))
|
||||
return
|
||||
}
|
||||
iter, err := parsers.NewHostIterator(info.Host, session.Params.HostsFile, excludes...)
|
||||
if err != nil {
|
||||
session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = iter.Close()
|
||||
}()
|
||||
|
||||
pluginsToRun, isCustomMode := s.GetPlugins(config)
|
||||
totalAlive := 0
|
||||
sawHosts := false
|
||||
performedLiveness := false
|
||||
|
||||
for {
|
||||
hosts, err := iter.NextBatch(ctx, targetHostBatchSize(config))
|
||||
if err != nil {
|
||||
session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err))
|
||||
return
|
||||
}
|
||||
if len(hosts) == 0 {
|
||||
break
|
||||
}
|
||||
sawHosts = true
|
||||
|
||||
if s.shouldPerformLivenessCheck(hosts, config) {
|
||||
performedLiveness = true
|
||||
hosts = CheckLive(ctx, hosts, false, session)
|
||||
}
|
||||
totalAlive += len(hosts)
|
||||
if len(hosts) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
s.dispatchUDPPlugins(ctx, session, hosts, info, config, ch, wg)
|
||||
s.scanHostBatch(ctx, session, hosts, info, pluginsToRun, isCustomMode, ch, wg)
|
||||
}
|
||||
|
||||
if sawHosts && performedLiveness {
|
||||
session.LogInfo(i18n.Tr("alive_hosts_count_info", totalAlive))
|
||||
}
|
||||
|
||||
if !sawHosts && len(state.GetHostPorts()) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 执行漏洞扫描
|
||||
if len(targetInfos) > 0 {
|
||||
ExecuteScanTasks(config, state, targetInfos, s, ch, wg)
|
||||
// 合并预设的 host:port
|
||||
hostPorts := state.GetHostPorts()
|
||||
if len(hostPorts) > 0 {
|
||||
merged := mergeHostPorts(nil, hostPorts)
|
||||
targets := s.convertToTargetInfos(merged, info)
|
||||
for _, target := range targets {
|
||||
for _, pluginName := range pluginsToRun {
|
||||
if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) {
|
||||
executeScanTask(ctx, session, pluginName, target, ch, wg)
|
||||
}
|
||||
}
|
||||
}
|
||||
state.ClearHostPorts()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServiceScanStrategy) scanHostBatch(ctx context.Context, session *common.ScanSession, hosts []string, info common.HostInfo, pluginsToRun []string, isCustomMode bool, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
config := session.Config
|
||||
stream := make(chan string, 64)
|
||||
|
||||
go EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, stream)
|
||||
|
||||
cancelled := false
|
||||
for addr := range stream {
|
||||
if cancelled {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancelled = true
|
||||
continue
|
||||
default:
|
||||
}
|
||||
|
||||
infos := s.convertToTargetInfos([]string{addr}, info)
|
||||
for _, target := range infos {
|
||||
for _, pluginName := range pluginsToRun {
|
||||
if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) {
|
||||
executeScanTask(ctx, session, pluginName, target, ch, wg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dispatchUDPPlugins 分发UDP协议插件,跳过TCP端口扫描链路
|
||||
func (s *ServiceScanStrategy) dispatchUDPPlugins(ctx context.Context, session *common.ScanSession, hosts []string, baseInfo common.HostInfo, config *common.Config, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
_, isCustomMode := s.GetPlugins(config)
|
||||
|
||||
var udpPlugins []string
|
||||
if isCustomMode {
|
||||
// custom mode: 只跑用户指定的 UDP 插件
|
||||
requested, _ := s.GetPlugins(config)
|
||||
for _, name := range requested {
|
||||
if plugins.IsUDP(name) {
|
||||
udpPlugins = append(udpPlugins, name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// auto mode: 跑所有已注册的安全 UDP 插件
|
||||
for _, name := range plugins.All() {
|
||||
if plugins.IsUDP(name) && plugins.IsSafe(name) {
|
||||
udpPlugins = append(udpPlugins, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(udpPlugins) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
for _, pluginName := range udpPlugins {
|
||||
for _, port := range plugins.GetPluginPorts(pluginName) {
|
||||
target := baseInfo
|
||||
target.Host = host
|
||||
target.Port = port
|
||||
executeScanTask(ctx, session, pluginName, target, ch, wg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareTargets 准备目标信息
|
||||
func (s *ServiceScanStrategy) PrepareTargets(info common.HostInfo, config *common.Config, state *common.State) []common.HostInfo {
|
||||
func (s *ServiceScanStrategy) PrepareTargets(info common.HostInfo, session *common.ScanSession) []common.HostInfo {
|
||||
// 发现目标主机和端口
|
||||
targetInfos, err := s.discoverTargets(info.Host, info, config, state)
|
||||
targetInfos, err := s.discoverTargets(context.Background(), info.Host, info, session)
|
||||
if err != nil {
|
||||
common.LogError(err.Error())
|
||||
session.LogError(err.Error())
|
||||
return nil
|
||||
}
|
||||
return targetInfos
|
||||
@@ -213,10 +341,15 @@ func (s *ServiceScanStrategy) LogVulnerabilityPluginInfo(targets []common.HostIn
|
||||
// =============================================================================
|
||||
|
||||
// discoverTargets 发现目标主机和端口
|
||||
func (s *ServiceScanStrategy) discoverTargets(hostInput string, baseInfo common.HostInfo, config *common.Config, state *common.State) ([]common.HostInfo, error) {
|
||||
func (s *ServiceScanStrategy) discoverTargets(ctx context.Context, hostInput string, baseInfo common.HostInfo, session *common.ScanSession) ([]common.HostInfo, error) {
|
||||
config := session.Config
|
||||
state := session.State
|
||||
// 标准流程:解析目标主机
|
||||
fv := common.GetFlagVars()
|
||||
hosts, err := parsers.ParseIP(hostInput, fv.HostsFile, fv.ExcludeHosts)
|
||||
excludes, err := loadHostExcludes(session.Params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", i18n.GetText("parse_target_failed"), err)
|
||||
}
|
||||
hosts, err := parsers.ParseIP(hostInput, session.Params.HostsFile, excludes...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", i18n.GetText("parse_target_failed"), err)
|
||||
}
|
||||
@@ -227,12 +360,12 @@ func (s *ServiceScanStrategy) discoverTargets(hostInput string, baseInfo common.
|
||||
if len(hosts) > 0 || len(state.GetHostPorts()) > 0 {
|
||||
// 主机存活检测
|
||||
if s.shouldPerformLivenessCheck(hosts, config) {
|
||||
hosts = CheckLive(hosts, false, config, state)
|
||||
common.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts)))
|
||||
hosts = CheckLive(ctx, hosts, false, session)
|
||||
session.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts)))
|
||||
}
|
||||
|
||||
// 端口扫描
|
||||
alivePorts := s.discoverAlivePorts(hosts, config, state)
|
||||
alivePorts := s.discoverAlivePorts(ctx, hosts, session)
|
||||
if len(alivePorts) > 0 {
|
||||
targetInfos = s.convertToTargetInfos(alivePorts, baseInfo)
|
||||
}
|
||||
@@ -247,40 +380,64 @@ func (s *ServiceScanStrategy) shouldPerformLivenessCheck(hosts []string, config
|
||||
}
|
||||
|
||||
// discoverAlivePorts 发现存活的端口
|
||||
func (s *ServiceScanStrategy) discoverAlivePorts(hosts []string, config *common.Config, state *common.State) []string {
|
||||
// 执行正常端口扫描后,合并预设的 host:port(来自项目缓存或 CLI),确保不遗漏
|
||||
func (s *ServiceScanStrategy) discoverAlivePorts(ctx context.Context, hosts []string, session *common.ScanSession) []string {
|
||||
config := session.Config
|
||||
state := session.State
|
||||
var alivePorts []string
|
||||
|
||||
// 如果已经有明确指定的host:port,直接使用(让后续SmartIdentify统一验证和识别)
|
||||
hostPorts := state.GetHostPorts()
|
||||
if len(hostPorts) > 0 {
|
||||
alivePorts = hostPorts
|
||||
common.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts)))
|
||||
state.ClearHostPorts()
|
||||
return alivePorts
|
||||
// 正常端口扫描
|
||||
if len(hosts) > 0 {
|
||||
alivePorts = EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, nil)
|
||||
}
|
||||
|
||||
// 根据扫描模式选择端口扫描方式
|
||||
if len(hosts) > 0 {
|
||||
alivePorts = EnhancedPortScan(hosts, config.Target.Ports, int64(config.Timeout.Seconds()), config, state)
|
||||
// 合并预设的 host:port(项目缓存 / CLI 注入)
|
||||
hostPorts := state.GetHostPorts()
|
||||
if len(hostPorts) > 0 {
|
||||
alivePorts = mergeHostPorts(alivePorts, hostPorts)
|
||||
session.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts)))
|
||||
state.ClearHostPorts()
|
||||
}
|
||||
|
||||
return alivePorts
|
||||
}
|
||||
|
||||
// mergeHostPorts 合并两个 host:port 列表并去重
|
||||
func mergeHostPorts(a, b []string) []string {
|
||||
seen := make(map[string]struct{}, len(a)+len(b))
|
||||
for _, s := range a {
|
||||
seen[s] = struct{}{}
|
||||
}
|
||||
for _, s := range b {
|
||||
seen[s] = struct{}{}
|
||||
}
|
||||
result := make([]string, 0, len(seen))
|
||||
for s := range seen {
|
||||
result = append(result, s)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// convertToTargetInfos 将端口列表转换为目标信息
|
||||
func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo common.HostInfo) []common.HostInfo {
|
||||
var infos []common.HostInfo
|
||||
|
||||
for _, targetIP := range ports {
|
||||
hostParts := strings.Split(targetIP, ":")
|
||||
if len(hostParts) != 2 {
|
||||
targetIP = strings.TrimSpace(targetIP)
|
||||
host, portStr, err := net.SplitHostPort(targetIP)
|
||||
if err != nil && strings.Count(targetIP, ":") == 1 {
|
||||
parts := strings.SplitN(targetIP, ":", 2)
|
||||
host, portStr = parts[0], parts[1]
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("invalid_target_format", targetIP))
|
||||
continue
|
||||
}
|
||||
|
||||
// 去除空格并过滤空值
|
||||
host := strings.TrimSpace(hostParts[0])
|
||||
portStr := strings.TrimSpace(hostParts[1])
|
||||
host = strings.TrimSpace(host)
|
||||
portStr = strings.TrimSpace(portStr)
|
||||
if host == "" || portStr == "" {
|
||||
common.LogError(i18n.Tr("invalid_target_format", targetIP))
|
||||
continue
|
||||
@@ -309,4 +466,3 @@ func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo comm
|
||||
|
||||
return infos
|
||||
}
|
||||
|
||||
|
||||
@@ -545,12 +545,26 @@ func TestConvertToTargetInfos(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IPv6地址",
|
||||
name: "裸IPv6地址缺少方括号",
|
||||
ports: []string{"::1:8080"},
|
||||
baseInfo: common.HostInfo{},
|
||||
expectedLen: 0, // Split会产生多个部分,被判定为非法
|
||||
expectedLen: 0,
|
||||
validateFunc: nil,
|
||||
},
|
||||
{
|
||||
name: "IPv6地址",
|
||||
ports: []string{"[2001:db8::1]:8080"},
|
||||
baseInfo: common.HostInfo{},
|
||||
expectedLen: 1,
|
||||
validateFunc: func(t *testing.T, infos []common.HostInfo) {
|
||||
if infos[0].Host != "2001:db8::1" {
|
||||
t.Errorf("Host = %q, 期望 '2001:db8::1'", infos[0].Host)
|
||||
}
|
||||
if infos[0].Port != 8080 {
|
||||
t.Errorf("Port = %d, 期望 8080", infos[0].Port)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "域名+端口",
|
||||
ports: []string{"example.com:80", "test.local:443"},
|
||||
|
||||
@@ -39,7 +39,7 @@ type SocketIterator struct {
|
||||
ports []int
|
||||
hostIdx int
|
||||
portIdx int
|
||||
total int
|
||||
total int64
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func NewSocketIterator(hosts []string, ports []int, exclude map[int]struct{}) *S
|
||||
return &SocketIterator{
|
||||
hosts: hosts,
|
||||
ports: sortedPorts,
|
||||
total: len(hosts) * len(sortedPorts),
|
||||
total: int64(len(hosts)) * int64(len(sortedPorts)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func (it *SocketIterator) Next() (string, int, bool) {
|
||||
}
|
||||
|
||||
// Total 返回总任务数(用于进度条)
|
||||
func (it *SocketIterator) Total() int {
|
||||
func (it *SocketIterator) Total() int64 {
|
||||
return it.total
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,17 @@ func TestSocketIterator_EmptyInputs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSocketIteratorTotalUsesInt64(t *testing.T) {
|
||||
hosts := make([]string, 1<<20)
|
||||
ports := make([]int, 4096)
|
||||
it := NewSocketIterator(hosts, ports, nil)
|
||||
|
||||
want := int64(len(hosts)) * int64(len(ports))
|
||||
if it.Total() != want {
|
||||
t.Fatalf("Total() = %d, want %d", it.Total(), want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSocketIterator_PortPrioritySort 验证端口优先级排序
|
||||
// 高价值端口(80, 443, 22等)应该排在前面
|
||||
func TestSocketIterator_PortPrioritySort(t *testing.T) {
|
||||
|
||||
+74
-80
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -12,12 +13,9 @@ import (
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
gmtls "github.com/tjfoc/gmsm/gmtls"
|
||||
)
|
||||
|
||||
// ===============================
|
||||
// Web服务检测
|
||||
// ===============================
|
||||
|
||||
// WebPortDetector 简化的Web检测器 - 保持API兼容
|
||||
type WebPortDetector struct{}
|
||||
|
||||
@@ -27,26 +25,29 @@ func GetWebPortDetector() *WebPortDetector {
|
||||
}
|
||||
|
||||
// DetectHTTPScheme 智能检测HTTP/HTTPS协议
|
||||
// 策略:TLS握手优先(快速且准确),失败后尝试HTTP
|
||||
// 返回: "https", "http", 或 "" (都不是Web服务)
|
||||
func DetectHTTPScheme(host string, port int, config *common.Config) string {
|
||||
// 策略:TLS握手优先(快速且准确),失败后尝试GM TLS,最后HTTP
|
||||
// 返回: "https", "https-gm", "http", 或 "" (都不是Web服务)
|
||||
func DetectHTTPScheme(host string, port int, config *common.Config, session *common.ScanSession) string {
|
||||
return DetectHTTPSchemeContext(context.Background(), host, port, config, session)
|
||||
}
|
||||
|
||||
func DetectHTTPSchemeContext(ctx context.Context, host string, port int, config *common.Config, session *common.ScanSession) string {
|
||||
// 优化:先快速检测 TCP 连通性
|
||||
if !isPortReachable(host, port, config) {
|
||||
if !isPortReachable(ctx, host, port, config, session) {
|
||||
return ""
|
||||
}
|
||||
|
||||
timeout := config.Network.WebTimeout
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
|
||||
// 第一步:尝试TLS握手(优先检测HTTPS)
|
||||
// 优势:握手失败代价小,不需要发送完整HTTP请求
|
||||
// 第一步:尝试标准TLS握手(优先检测HTTPS)
|
||||
tlsDialer := &net.Dialer{Timeout: timeout}
|
||||
tlsConn, err := tls.DialWithDialer(
|
||||
tlsDialer,
|
||||
"tcp", addr,
|
||||
&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: tls.VersionTLS10, // 兼容老版本TLS
|
||||
MinVersion: tls.VersionTLS10,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -55,22 +56,33 @@ func DetectHTTPScheme(host string, port int, config *common.Config) string {
|
||||
return "https"
|
||||
}
|
||||
|
||||
// TLS握手失败,记录原因
|
||||
// 第二步:尝试国密TLS握手(GM TLS fallback)
|
||||
gmConn, gmErr := gmtls.DialWithDialer(
|
||||
tlsDialer,
|
||||
"tcp", addr,
|
||||
&gmtls.Config{
|
||||
GMSupport: gmtls.NewGMSupport(),
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
)
|
||||
|
||||
// 第二步:尝试HTTP请求(回退检测HTTP)
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse // 不跟随重定向
|
||||
},
|
||||
if gmErr == nil {
|
||||
_ = gmConn.Close()
|
||||
return "https-gm"
|
||||
}
|
||||
|
||||
// TLS和GM TLS都失败,尝试HTTP
|
||||
client := createHTTPClient(config, session)
|
||||
|
||||
// 使用HEAD请求(更轻量)
|
||||
httpURL := fmt.Sprintf("http://%s", addr)
|
||||
resp, err := client.Head(httpURL)
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", httpURL, nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
resp, err := session.HTTPDo(client, req)
|
||||
if err == nil {
|
||||
_ = resp.Body.Close()
|
||||
return "http"
|
||||
@@ -81,7 +93,7 @@ func DetectHTTPScheme(host string, port int, config *common.Config) string {
|
||||
}
|
||||
|
||||
// createHTTPClient 创建统一的HTTP客户端 - 支持HTTP/HTTPS和代理
|
||||
func createHTTPClient(config *common.Config) *http.Client {
|
||||
func createHTTPClient(config *common.Config, session *common.ScanSession) *http.Client {
|
||||
timeout := config.Network.WebTimeout
|
||||
|
||||
// 创建基础Transport,配置连接和 TLS 超时
|
||||
@@ -103,14 +115,14 @@ func createHTTPClient(config *common.Config) *http.Client {
|
||||
if proxyURL, err := url.Parse(networkConfig.HTTPProxy); err == nil {
|
||||
transport.Proxy = http.ProxyURL(proxyURL)
|
||||
} else {
|
||||
common.LogError(i18n.Tr("http_proxy_config_error", err))
|
||||
session.LogError(i18n.Tr("http_proxy_config_error", err))
|
||||
}
|
||||
} else if networkConfig.Socks5Proxy != "" {
|
||||
// 使用SOCKS5代理 - 需要特殊处理
|
||||
if _, err := url.Parse(networkConfig.Socks5Proxy); err == nil {
|
||||
// SOCKS5代理需要使用代理管理器
|
||||
// 这里先记录警告,建议使用HTTP代理进行Web检测
|
||||
common.LogError(i18n.GetText("socks5_not_supported_web"))
|
||||
session.LogError(i18n.GetText("socks5_not_supported_web"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,22 +136,26 @@ func createHTTPClient(config *common.Config) *http.Client {
|
||||
}
|
||||
|
||||
// DetectHTTPServiceOnly HTTP协议检测 - 保持API兼容,简化实现
|
||||
func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *common.Config) bool {
|
||||
func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *common.Config, session *common.ScanSession) bool {
|
||||
return w.DetectHTTPServiceOnlyContext(context.Background(), host, port, config, session)
|
||||
}
|
||||
|
||||
func (w *WebPortDetector) DetectHTTPServiceOnlyContext(ctx context.Context, host string, port int, config *common.Config, session *common.ScanSession) bool {
|
||||
// 优化:先快速检测 TCP 连通性,避免在不可达端口上浪费双倍超时时间
|
||||
// 对于不存在的端口,这可以将检测时间从 2×timeout 减少到 1×timeout
|
||||
if !isPortReachable(host, port, config) {
|
||||
if !isPortReachable(ctx, host, port, config, session) {
|
||||
return false
|
||||
}
|
||||
|
||||
client := createHTTPClient(config)
|
||||
client := createHTTPClient(config, session)
|
||||
|
||||
// 尝试HTTP
|
||||
if w.tryHTTP(client, host, port, "http") {
|
||||
if w.tryHTTP(ctx, client, session, host, port, "http") {
|
||||
return true
|
||||
}
|
||||
|
||||
// 尝试HTTPS
|
||||
if w.tryHTTP(client, host, port, "https") {
|
||||
if w.tryHTTP(ctx, client, session, host, port, "https") {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -148,11 +164,11 @@ func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *c
|
||||
|
||||
// isPortReachable 快速检测端口是否可达(TCP 连接测试)
|
||||
// 用于在 HTTP/HTTPS 检测前过滤不可达端口,避免双重超时
|
||||
func isPortReachable(host string, port int, config *common.Config) bool {
|
||||
func isPortReachable(ctx context.Context, host string, port int, config *common.Config, session *common.ScanSession) bool {
|
||||
timeout := config.Network.WebTimeout
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
|
||||
conn, err := net.DialTimeout("tcp", addr, timeout)
|
||||
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -161,26 +177,20 @@ func isPortReachable(host string, port int, config *common.Config) bool {
|
||||
}
|
||||
|
||||
// tryHTTP 尝试HTTP请求 - 简化的核心逻辑
|
||||
func (w *WebPortDetector) tryHTTP(client *http.Client, host string, port int, protocol string) bool {
|
||||
func (w *WebPortDetector) tryHTTP(ctx context.Context, client *http.Client, session *common.ScanSession, host string, port int, protocol string) bool {
|
||||
// 构造URL
|
||||
var url string
|
||||
if (port == 80 && protocol == "http") || (port == 443 && protocol == "https") {
|
||||
url = fmt.Sprintf("%s://%s", protocol, host)
|
||||
} else {
|
||||
url = fmt.Sprintf("%s://%s:%d", protocol, host, port)
|
||||
}
|
||||
targetURL := (&url.URL{Scheme: protocol, Host: net.JoinHostPort(host, strconv.Itoa(port))}).String()
|
||||
|
||||
// 发送HEAD请求
|
||||
req, err := http.NewRequest("HEAD", url, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", targetURL, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "fscan-web-detector/2.1")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
|
||||
// 使用统一的SafeHTTPDo以确保遵循限速策略和代理设置
|
||||
resp, err := common.SafeHTTPDo(client, req)
|
||||
resp, err := session.HTTPDo(client, req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -251,7 +261,7 @@ func IsWebServiceByFingerprint(serviceInfo *ServiceInfo) bool {
|
||||
|
||||
// MarkAsWebService 标记Web服务 - 保持API兼容
|
||||
func MarkAsWebService(host string, port int, serviceInfo *ServiceInfo) {
|
||||
cacheKey := fmt.Sprintf("%s:%d", host, port)
|
||||
cacheKey := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
|
||||
webCacheMutex.Lock()
|
||||
defer webCacheMutex.Unlock()
|
||||
@@ -261,7 +271,7 @@ func MarkAsWebService(host string, port int, serviceInfo *ServiceInfo) {
|
||||
|
||||
// GetWebServiceInfo 获取Web服务信息
|
||||
func GetWebServiceInfo(host string, port int) (*ServiceInfo, bool) {
|
||||
cacheKey := fmt.Sprintf("%s:%d", host, port)
|
||||
cacheKey := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
|
||||
webCacheMutex.RLock()
|
||||
defer webCacheMutex.RUnlock()
|
||||
@@ -276,30 +286,6 @@ func IsMarkedWebService(host string, port int) bool {
|
||||
return exists
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 指纹缓存
|
||||
// ===============================
|
||||
|
||||
// 指纹缓存 - 存储 host:port → 指纹列表的映射
|
||||
var (
|
||||
fingerprintCache = make(map[string][]string)
|
||||
fingerprintCacheMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// SetFingerprints 存储目标的指纹信息
|
||||
func SetFingerprints(host string, port int, fingerprints []string) {
|
||||
if len(fingerprints) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
fingerprintCacheMutex.Lock()
|
||||
defer fingerprintCacheMutex.Unlock()
|
||||
|
||||
fingerprintCache[cacheKey] = fingerprints
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// Web扫描策略
|
||||
// ===============================
|
||||
@@ -312,7 +298,7 @@ type WebScanStrategy struct {
|
||||
// NewWebScanStrategy 创建新的Web扫描策略
|
||||
func NewWebScanStrategy() *WebScanStrategy {
|
||||
return &WebScanStrategy{
|
||||
BaseScanStrategy: NewBaseScanStrategy("Web扫描", FilterWeb),
|
||||
BaseScanStrategy: NewBaseScanStrategy(i18n.GetText("scan_strategy_web_name"), FilterWeb),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,34 +313,38 @@ func (s *WebScanStrategy) Description() string {
|
||||
}
|
||||
|
||||
// Execute 执行Web扫描策略
|
||||
func (s *WebScanStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
func (s *WebScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
// 输出扫描开始信息
|
||||
s.LogScanStart()
|
||||
s.LogScanStart(session)
|
||||
|
||||
// 验证插件配置
|
||||
if err := s.ValidateConfiguration(); err != nil {
|
||||
common.LogError(err.Error())
|
||||
session.LogError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 准备URL目标
|
||||
targets := s.PrepareTargets(info, state)
|
||||
targets := s.prepareTargets(info, session.State, session)
|
||||
|
||||
// 输出插件信息
|
||||
s.LogPluginInfo(config)
|
||||
s.LogPluginInfo(session.Config, session)
|
||||
|
||||
// 执行扫描任务
|
||||
ExecuteScanTasks(config, state, targets, s, ch, wg)
|
||||
ExecuteScanTasks(ctx, session, targets, s, ch, wg)
|
||||
}
|
||||
|
||||
// PrepareTargets 准备URL目标列表
|
||||
func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common.State) []common.HostInfo {
|
||||
return s.prepareTargets(baseInfo, state, nil)
|
||||
}
|
||||
|
||||
func (s *WebScanStrategy) prepareTargets(baseInfo common.HostInfo, state *common.State, session *common.ScanSession) []common.HostInfo {
|
||||
var targetInfos []common.HostInfo
|
||||
|
||||
// 首先从State获取URL目标
|
||||
urls := state.GetURLs()
|
||||
for _, urlStr := range urls {
|
||||
urlInfo := s.createTargetFromURL(baseInfo, urlStr)
|
||||
urlInfo := s.createTargetFromURLWithSession(baseInfo, urlStr, session)
|
||||
if urlInfo != nil {
|
||||
targetInfos = append(targetInfos, *urlInfo)
|
||||
}
|
||||
@@ -362,7 +352,7 @@ func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common
|
||||
|
||||
// 如果URLs为空但baseInfo.Url有值,使用baseInfo.URL
|
||||
if len(targetInfos) == 0 && baseInfo.URL != "" {
|
||||
urlInfo := s.createTargetFromURL(baseInfo, baseInfo.URL)
|
||||
urlInfo := s.createTargetFromURLWithSession(baseInfo, baseInfo.URL, session)
|
||||
if urlInfo != nil {
|
||||
targetInfos = append(targetInfos, *urlInfo)
|
||||
}
|
||||
@@ -373,6 +363,10 @@ func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common
|
||||
|
||||
// createTargetFromURL 从URL创建目标信息
|
||||
func (s *WebScanStrategy) createTargetFromURL(baseInfo common.HostInfo, urlStr string) *common.HostInfo {
|
||||
return s.createTargetFromURLWithSession(baseInfo, urlStr, nil)
|
||||
}
|
||||
|
||||
func (s *WebScanStrategy) createTargetFromURLWithSession(baseInfo common.HostInfo, urlStr string, session *common.ScanSession) *common.HostInfo {
|
||||
// 确保URL包含协议头
|
||||
if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") {
|
||||
urlStr = "http://" + urlStr
|
||||
@@ -381,7 +375,7 @@ func (s *WebScanStrategy) createTargetFromURL(baseInfo common.HostInfo, urlStr s
|
||||
// 解析URL获取Host和Port信息
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("url_parse_failed", urlStr, err))
|
||||
session.LogError(i18n.Tr("url_parse_failed", urlStr, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -25,7 +26,6 @@ web_scanner_test.go - WebScanner核心逻辑测试
|
||||
4. 指纹缓存 - SetFingerprints, GetFingerprints
|
||||
|
||||
不测试的部分(需要集成测试):
|
||||
- createHTTPClient - 依赖全局配置
|
||||
- tryHTTP, DetectHTTPServiceOnly - 网络IO
|
||||
- Execute - 完整流程
|
||||
|
||||
@@ -33,6 +33,26 @@ web_scanner_test.go - WebScanner核心逻辑测试
|
||||
缓存操作需要验证并发安全性。"
|
||||
*/
|
||||
|
||||
func TestDetectHTTPServiceOnlyContextHonorsCancellation(t *testing.T) {
|
||||
cfg := common.GetGlobalConfig()
|
||||
oldTimeout := cfg.Network.WebTimeout
|
||||
cfg.Network.WebTimeout = 2 * time.Second
|
||||
defer func() { cfg.Network.WebTimeout = oldTimeout }()
|
||||
|
||||
session := common.NewScanSession(cfg, common.NewState(), common.GetFlagVars())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
start := time.Now()
|
||||
detected := GetWebPortDetector().DetectHTTPServiceOnlyContext(ctx, "203.0.113.1", 80, cfg, session)
|
||||
if detected {
|
||||
t.Fatal("canceled web detection should not report a service")
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed > 200*time.Millisecond {
|
||||
t.Fatalf("canceled web detection took %s", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 核心逻辑测试:Web服务识别
|
||||
// =============================================================================
|
||||
@@ -662,6 +682,8 @@ func TestDetectHTTPScheme(t *testing.T) {
|
||||
cfg.Network.WebTimeout = 2 * time.Second
|
||||
defer func() { cfg.Network.WebTimeout = oldTimeout }()
|
||||
|
||||
session := common.NewScanSession(cfg, common.NewState(), common.GetFlagVars())
|
||||
|
||||
t.Run("HTTPS服务器检测", func(t *testing.T) {
|
||||
// 创建HTTPS测试服务器
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -677,7 +699,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
|
||||
// 测试检测
|
||||
result := DetectHTTPScheme(host, port, cfg)
|
||||
result := DetectHTTPScheme(host, port, cfg, session)
|
||||
if result != "https" {
|
||||
t.Errorf("DetectHTTPScheme() = %q, 期望 'https'", result)
|
||||
}
|
||||
@@ -698,7 +720,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
|
||||
// 测试检测
|
||||
result := DetectHTTPScheme(host, port, cfg)
|
||||
result := DetectHTTPScheme(host, port, cfg, session)
|
||||
if result != "http" {
|
||||
t.Errorf("DetectHTTPScheme() = %q, 期望 'http'", result)
|
||||
}
|
||||
@@ -706,7 +728,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
||||
|
||||
t.Run("不存在的服务", func(t *testing.T) {
|
||||
// 使用127.0.0.1的一个未使用端口
|
||||
result := DetectHTTPScheme("127.0.0.1", 65534, cfg)
|
||||
result := DetectHTTPScheme("127.0.0.1", 65534, cfg, session)
|
||||
if result != "" {
|
||||
t.Errorf("不存在的服务应返回空字符串, 实际 %q", result)
|
||||
}
|
||||
@@ -736,7 +758,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
|
||||
// 测试检测
|
||||
result := DetectHTTPScheme("127.0.0.1", port, cfg)
|
||||
result := DetectHTTPScheme("127.0.0.1", port, cfg, session)
|
||||
if result != "" {
|
||||
t.Logf("非Web服务检测返回: %q (预期空字符串,但立即关闭连接可能被误判)", result)
|
||||
}
|
||||
@@ -757,9 +779,63 @@ func TestDetectHTTPScheme(t *testing.T) {
|
||||
host, portStr, _ := net.SplitHostPort(server.Listener.Addr().String())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
|
||||
result := DetectHTTPScheme(host, port, cfg)
|
||||
result := DetectHTTPScheme(host, port, cfg, session)
|
||||
if result != "https" {
|
||||
t.Errorf("TLS 1.0服务器应被检测为https, 实际 %q", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateHTTPClientUsesPerSessionProxy(t *testing.T) {
|
||||
cfgA := common.NewConfig()
|
||||
cfgA.Network.WebTimeout = time.Second
|
||||
cfgA.Network.HTTPProxy = "http://127.0.0.1:18080"
|
||||
sessionA := common.NewScanSession(cfgA, common.NewState(), &common.FlagVars{})
|
||||
|
||||
cfgB := common.NewConfig()
|
||||
cfgB.Network.WebTimeout = time.Second
|
||||
cfgB.Network.HTTPProxy = "http://127.0.0.1:28080"
|
||||
sessionB := common.NewScanSession(cfgB, common.NewState(), &common.FlagVars{})
|
||||
|
||||
clientA := createHTTPClient(cfgA, sessionA)
|
||||
clientB := createHTTPClient(cfgB, sessionB)
|
||||
if clientA == clientB {
|
||||
t.Fatal("createHTTPClient reused a process-wide client")
|
||||
}
|
||||
|
||||
proxyA := proxyForTest(t, clientA)
|
||||
proxyB := proxyForTest(t, clientB)
|
||||
if proxyA == proxyB {
|
||||
t.Fatalf("proxy URLs should be per config, both were %q", proxyA)
|
||||
}
|
||||
if proxyA != "http://127.0.0.1:18080" {
|
||||
t.Fatalf("proxyA = %q, want http://127.0.0.1:18080", proxyA)
|
||||
}
|
||||
if proxyB != "http://127.0.0.1:28080" {
|
||||
t.Fatalf("proxyB = %q, want http://127.0.0.1:28080", proxyB)
|
||||
}
|
||||
}
|
||||
|
||||
func proxyForTest(t *testing.T, client *http.Client) string {
|
||||
t.Helper()
|
||||
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
t.Fatal("client transport is not *http.Transport")
|
||||
}
|
||||
if transport.Proxy == nil {
|
||||
t.Fatal("client proxy is nil")
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
proxyURL, err := transport.Proxy(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if proxyURL == nil {
|
||||
t.Fatal("proxy URL is nil")
|
||||
}
|
||||
return proxyURL.String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
fscan "github.com/shadow1ng/fscan/pkg/fscan"
|
||||
)
|
||||
|
||||
func main() {
|
||||
scanner := fscan.NewScanner(fscan.Config{
|
||||
TaskID: "task-001",
|
||||
Timeout: 3 * time.Second,
|
||||
Threads: 64,
|
||||
DisablePing: true,
|
||||
DisableBrute: true,
|
||||
Plugins: []string{"ssh", "mysql", "redis", "ftp"},
|
||||
OnProgress: func(p fscan.ScanProgress) {
|
||||
fmt.Printf("[progress] %d/%d tasks, %d packets, paused=%v, elapsed=%s\n",
|
||||
p.TasksCompleted, p.TasksTotal, p.Packets, p.Paused, p.Duration.Round(time.Millisecond))
|
||||
},
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ctrl, reportCh, errCh := scanner.ScanWithController(ctx,
|
||||
fscan.Target{Host: "127.0.0.1", Ports: []int{21, 22, 3306, 6379}},
|
||||
)
|
||||
|
||||
// Simulate a pause command from control plane after 1 second.
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
fmt.Println("[agent] pausing scan...")
|
||||
ctrl.Pause()
|
||||
|
||||
// Check live stats while paused.
|
||||
stats := ctrl.Stats()
|
||||
fmt.Printf("[agent] stats while paused: completed=%d, packets=%d\n",
|
||||
stats.TasksCompleted, stats.Packets)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
fmt.Println("[agent] resuming scan...")
|
||||
ctrl.Resume()
|
||||
}()
|
||||
|
||||
report := <-reportCh
|
||||
if err := <-errCh; err != nil {
|
||||
fmt.Printf("[agent] scan error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n[agent] scan complete: %d results, %d vulns, %d services\n",
|
||||
report.Summary.Total, report.Summary.Vulns, report.Summary.Services)
|
||||
|
||||
for _, result := range report.Results {
|
||||
taskID, _ := result.DetailString("task_id")
|
||||
fmt.Printf(" [%s] %s %s (task=%s)\n", result.Type, result.Target, result.Status, taskID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
fscan "github.com/shadow1ng/fscan/pkg/fscan"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := fscan.Config{
|
||||
Timeout: 3 * time.Second,
|
||||
Threads: 64,
|
||||
DisablePing: true,
|
||||
DisableBrute: true,
|
||||
Plugins: []string{"ssh", "mysql", "redis"},
|
||||
}
|
||||
target := fscan.Target{
|
||||
Host: "127.0.0.1",
|
||||
Ports: []int{22, 3306, 6379},
|
||||
}
|
||||
if err := fscan.ValidateConfig(config, target); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
scanner := fscan.NewScanner(config)
|
||||
results, err := scanner.Scan(context.Background(), target)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
summary := fscan.SummarizeResults(results)
|
||||
fmt.Printf("scan finished: %+v\n", summary)
|
||||
for _, result := range results {
|
||||
if result.IsService() || result.IsVuln() {
|
||||
fmt.Printf("%s %s %s\n", result.Type, result.Target, result.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
fscan "github.com/shadow1ng/fscan/pkg/fscan"
|
||||
)
|
||||
|
||||
func main() {
|
||||
for _, plugin := range fscan.ListPlugins() {
|
||||
if plugin.Default {
|
||||
fmt.Printf("default plugin: %s ports=%v safe=%v\n", plugin.Name, plugin.Ports, plugin.Safe)
|
||||
}
|
||||
}
|
||||
|
||||
config := fscan.Config{
|
||||
Timeout: 3 * time.Second,
|
||||
Threads: 64,
|
||||
DisablePing: true,
|
||||
DisableBrute: true,
|
||||
Plugins: []string{"ssh", "mysql", "redis"},
|
||||
}
|
||||
target := fscan.Target{
|
||||
Host: "127.0.0.1",
|
||||
Ports: []int{22, 3306, 6379},
|
||||
}
|
||||
|
||||
var summary fscan.ResultSummary
|
||||
scanner := fscan.NewScanner(config)
|
||||
err := scanner.ScanEach(context.Background(), func(result fscan.Result) error {
|
||||
summary.Add(result)
|
||||
if service, ok := result.Service(); ok {
|
||||
fmt.Printf("service=%s target=%s\n", service, result.Target)
|
||||
}
|
||||
if result.IsCredential() {
|
||||
username, _ := result.Username()
|
||||
password, _ := result.Password()
|
||||
fmt.Printf("credential target=%s username=%s password=%s\n", result.Target, username, password)
|
||||
}
|
||||
return nil
|
||||
}, target)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("stream summary: %+v\n", summary)
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
# fscan-lite
|
||||
|
||||
极简但极致兼容的TCP内网端口扫描器
|
||||
|
||||
## 设计理念
|
||||
|
||||
**兼容性第一,简洁至上**
|
||||
|
||||
- 支持从 Windows 98 到 Windows 11
|
||||
- 支持从 Ubuntu 8.04 到最新版本
|
||||
- 使用 C89 标准,最大兼容性
|
||||
- 静态编译,零依赖运行
|
||||
- 单个可执行文件 < 1MB
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ TCP端口连接扫描
|
||||
- ✅ 支持端口范围 (1-65535, 80,443)
|
||||
- ✅ 可配置超时时间
|
||||
- ✅ 静态编译,零依赖
|
||||
- ✅ 跨平台兼容
|
||||
|
||||
## 编译
|
||||
|
||||
### Linux/Unix
|
||||
|
||||
```bash
|
||||
# 动态编译
|
||||
make
|
||||
|
||||
# 静态编译(推荐)
|
||||
make static
|
||||
|
||||
# 最小化编译
|
||||
make small
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
# MinGW 编译
|
||||
mingw32-make -f Makefile
|
||||
|
||||
# 或使用MSVC
|
||||
cl /TC src/*.c /Febin/fscan-lite.exe ws2_32.lib
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
```bash
|
||||
# 基本用法
|
||||
./bin/fscan-lite -h 192.168.1.1 -p 22,80,443
|
||||
|
||||
# 扫描端口范围
|
||||
./bin/fscan-lite -h 10.0.0.1 -p 1-1000
|
||||
|
||||
# 自定义超时
|
||||
./bin/fscan-lite -h 192.168.1.100 -p 80,443 -t 2
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| -h HOST | 目标主机IP | -h 192.168.1.1 |
|
||||
| -p PORTS | 端口列表 | -p 80,443,8000-8080 |
|
||||
| -t TIMEOUT | 超时时间(秒) | -t 3 |
|
||||
| --help | 显示帮助 | --help |
|
||||
| --version | 显示版本 | --version |
|
||||
|
||||
## 二进制大小对比
|
||||
|
||||
| 版本 | 大小 | 说明 |
|
||||
|------|------|------|
|
||||
| fscan (Go) | ~30MB | 包含运行时 |
|
||||
| fscan-lite | ~900KB | 静态编译 |
|
||||
| fscan-lite (strip) | ~700KB | 去除调试信息 |
|
||||
|
||||
## 兼容性测试
|
||||
|
||||
### Linux 发行版
|
||||
- ✅ Ubuntu 8.04 - 24.04
|
||||
- ✅ CentOS 5 - 9
|
||||
- ✅ Debian 5 - 12
|
||||
- ✅ RHEL 5 - 9
|
||||
|
||||
### Windows 版本
|
||||
- ✅ Windows 98 SE
|
||||
- ✅ Windows XP
|
||||
- ✅ Windows 7/8/10/11
|
||||
- ✅ Windows Server 2003-2022
|
||||
|
||||
## 技术实现
|
||||
|
||||
- **语言**: C89 (最大兼容性)
|
||||
- **网络**: 原生socket API
|
||||
- **编译**: GCC/MSVC/Clang
|
||||
- **链接**: 静态链接,零依赖
|
||||
- **大小**: < 1MB 单文件
|
||||
|
||||
## 性能对比
|
||||
|
||||
| 指标 | fscan | fscan-lite |
|
||||
|------|-------|------------|
|
||||
| 启动时间 | ~50ms | ~5ms |
|
||||
| 内存占用 | ~20MB | ~2MB |
|
||||
| 扫描速度 | 1000 ports/s | 1000 ports/s |
|
||||
| 兼容性 | 现代系统 | 25年跨度 |
|
||||
|
||||
## 构建配置
|
||||
|
||||
```bash
|
||||
# 查看构建信息
|
||||
make info
|
||||
|
||||
# 所有构建选项
|
||||
make help
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
与 fscan 主项目保持一致
|
||||
|
||||
---
|
||||
|
||||
**理念**: 一个工具应该在它设计的任何系统上都能运行,而不需要用户去寻找依赖项。
|
||||
@@ -1,156 +0,0 @@
|
||||
#ifndef PLATFORM_H
|
||||
#define PLATFORM_H
|
||||
|
||||
/*
|
||||
* platform.h - 极致兼容性的平台抽象层
|
||||
*
|
||||
* 支持范围:
|
||||
* Windows: 98/ME/NT4/2000/XP/Vista/7/8/10/11
|
||||
* Linux: glibc 2.3+ (2003年后的所有发行版)
|
||||
* 编译器: MSVC 6.0+, GCC 3.0+, Clang 3.0+
|
||||
*/
|
||||
|
||||
/* C89兼容性 - 最古老但最可靠的标准 */
|
||||
#ifndef __STDC__
|
||||
#define __STDC__ 1
|
||||
#endif
|
||||
|
||||
/* 平台检测 */
|
||||
#ifdef _WIN32
|
||||
#define PLATFORM_WINDOWS
|
||||
#ifdef _WIN64
|
||||
#define PLATFORM_WIN64
|
||||
#else
|
||||
#define PLATFORM_WIN32
|
||||
#endif
|
||||
#else
|
||||
#define PLATFORM_UNIX
|
||||
#ifdef __linux__
|
||||
#define PLATFORM_LINUX
|
||||
#elif defined(__APPLE__)
|
||||
#define PLATFORM_MACOS
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Windows头文件包含 - 兼容Win98 */
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
/* 定义最低Windows版本 - Win98 */
|
||||
#ifndef _WIN32_WINNT
|
||||
#define _WIN32_WINNT 0x0410 /* Windows 98 */
|
||||
#endif
|
||||
#ifndef WINVER
|
||||
#define WINVER 0x0410
|
||||
#endif
|
||||
|
||||
/* 必须先包含winsock2.h,否则windows.h会包含旧版winsock.h导致冲突 */
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
|
||||
/* 老版本Windows兼容性 */
|
||||
#ifdef _MSC_VER
|
||||
#if _MSC_VER < 1300 /* MSVC 6.0 */
|
||||
#pragma comment(lib, "wsock32.lib")
|
||||
#else
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Windows类型定义 */
|
||||
typedef SOCKET socket_t;
|
||||
typedef int socklen_t;
|
||||
#define INVALID_SOCKET_VALUE INVALID_SOCKET
|
||||
#define close_socket closesocket
|
||||
#define socket_errno WSAGetLastError()
|
||||
|
||||
/* Windows错误码转换 - 使用ifndef避免与errno.h冲突 */
|
||||
#ifndef EWOULDBLOCK
|
||||
#define EWOULDBLOCK WSAEWOULDBLOCK
|
||||
#endif
|
||||
#ifndef EINPROGRESS
|
||||
#define EINPROGRESS WSAEINPROGRESS
|
||||
#endif
|
||||
#ifndef ECONNREFUSED
|
||||
#define ECONNREFUSED WSAECONNREFUSED
|
||||
#endif
|
||||
|
||||
#else
|
||||
/* Unix/Linux头文件 */
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <netdb.h>
|
||||
|
||||
/* Unix类型定义 */
|
||||
typedef int socket_t;
|
||||
#define INVALID_SOCKET_VALUE (-1)
|
||||
#define close_socket close
|
||||
#define socket_errno errno
|
||||
|
||||
#endif
|
||||
|
||||
/* 标准C头文件 */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
/* 线程抽象 - 最简单的实现 */
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
typedef HANDLE thread_t;
|
||||
typedef DWORD thread_id_t;
|
||||
typedef unsigned (__stdcall *thread_func_t)(void *);
|
||||
|
||||
#define CREATE_THREAD(func, arg) \
|
||||
(HANDLE)_beginthreadex(NULL, 0, (thread_func_t)(func), (arg), 0, NULL)
|
||||
#define WAIT_THREAD(handle) WaitForSingleObject((handle), INFINITE)
|
||||
#define CLOSE_THREAD(handle) CloseHandle(handle)
|
||||
|
||||
#else
|
||||
#include <pthread.h>
|
||||
#include <fcntl.h>
|
||||
typedef pthread_t thread_t;
|
||||
typedef pthread_t thread_id_t;
|
||||
typedef void* (*thread_func_t)(void *);
|
||||
|
||||
#define CREATE_THREAD(func, arg) ({ \
|
||||
pthread_t t; \
|
||||
pthread_create(&t, NULL, (thread_func_t)(func), (arg)) == 0 ? t : 0; \
|
||||
})
|
||||
#define WAIT_THREAD(handle) pthread_join((handle), NULL)
|
||||
#define CLOSE_THREAD(handle) /* pthread handles are auto-cleaned */
|
||||
|
||||
#endif
|
||||
|
||||
/* 时间函数抽象 */
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
#define sleep_ms(ms) Sleep(ms)
|
||||
#else
|
||||
#define sleep_ms(ms) usleep((ms) * 1000)
|
||||
#endif
|
||||
|
||||
/* 编译器特定定义 */
|
||||
#ifdef _MSC_VER
|
||||
/* MSVC特定 */
|
||||
#define snprintf _snprintf
|
||||
#define vsnprintf _vsnprintf
|
||||
#define strcasecmp _stricmp
|
||||
#define strncasecmp _strnicmp
|
||||
#endif
|
||||
|
||||
/* 常用常量 */
|
||||
#define MAX_HOST_LEN 256
|
||||
#define MAX_PORT_COUNT 65536
|
||||
#define DEFAULT_TIMEOUT 3
|
||||
#define DEFAULT_THREAD_COUNT 100
|
||||
|
||||
/* 函数声明 */
|
||||
int platform_init(void);
|
||||
void platform_cleanup(void);
|
||||
int set_socket_timeout(socket_t sock, int timeout_seconds);
|
||||
int make_socket_nonblocking(socket_t sock);
|
||||
|
||||
#endif /* PLATFORM_H */
|
||||
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* main.c - fscan-lite 主程序
|
||||
*
|
||||
* 极简的TCP内网端口扫描器
|
||||
* 目标:最大兼容性,最小复杂度
|
||||
*/
|
||||
|
||||
#include "../include/platform.h"
|
||||
|
||||
/* 函数声明 */
|
||||
int tcp_connect_test(const char* host, int port, int timeout);
|
||||
int scan_host_ports(const char* host, const int* ports, int port_count, int timeout);
|
||||
int parse_ports(const char* port_str, int* ports, int max_ports);
|
||||
int parse_hosts(const char* host_str, char hosts[][MAX_HOST_LEN], int max_hosts);
|
||||
|
||||
/* 显示版本信息 */
|
||||
void show_version(void) {
|
||||
printf("fscan-lite v1.0 - Lightweight TCP Port Scanner\n");
|
||||
printf("Built for maximum compatibility (Windows 98 - Windows 11, Linux glibc 2.3+)\n");
|
||||
printf("Copyright (c) 2024\n");
|
||||
}
|
||||
|
||||
/* 显示使用帮助 */
|
||||
void show_usage(const char* program_name) {
|
||||
printf("Usage: %s [OPTIONS]\n", program_name);
|
||||
printf("\n");
|
||||
printf("Options:\n");
|
||||
printf(" -h HOST Target host (IP address)\n");
|
||||
printf(" -p PORTS Ports to scan (e.g. 80,443 or 1-1000)\n");
|
||||
printf(" -t TIMEOUT Connection timeout in seconds (default: 3)\n");
|
||||
printf(" --help Show this help message\n");
|
||||
printf(" --version Show version information\n");
|
||||
printf("\n");
|
||||
printf("Examples:\n");
|
||||
printf(" %s -h 192.168.1.1 -p 22,80,443\n", program_name);
|
||||
printf(" %s -h 10.0.0.1 -p 1-1000 -t 2\n", program_name);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
/* 主函数 */
|
||||
int main(int argc, char* argv[]) {
|
||||
char* target_host = NULL;
|
||||
char* port_string = NULL;
|
||||
int timeout = DEFAULT_TIMEOUT;
|
||||
int ports[1000]; /* 支持最多1000个端口 */
|
||||
int port_count = 0;
|
||||
int i;
|
||||
int result;
|
||||
|
||||
/* 参数解析 */
|
||||
for (i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "-h") == 0 && i + 1 < argc) {
|
||||
target_host = argv[++i];
|
||||
}
|
||||
else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
|
||||
port_string = argv[++i];
|
||||
}
|
||||
else if (strcmp(argv[i], "-t") == 0 && i + 1 < argc) {
|
||||
timeout = atoi(argv[++i]);
|
||||
if (timeout <= 0) timeout = DEFAULT_TIMEOUT;
|
||||
}
|
||||
else if (strcmp(argv[i], "--help") == 0) {
|
||||
show_usage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
else if (strcmp(argv[i], "--version") == 0) {
|
||||
show_version();
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
printf("Unknown option: %s\n", argv[i]);
|
||||
show_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 验证必需参数 */
|
||||
if (!target_host) {
|
||||
printf("Error: Target host (-h) is required\n");
|
||||
show_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!port_string) {
|
||||
printf("Error: Ports (-p) are required\n");
|
||||
show_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 初始化平台 */
|
||||
if (platform_init() != 0) {
|
||||
printf("Error: Failed to initialize platform\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 解析端口 */
|
||||
port_count = parse_ports(port_string, ports, sizeof(ports) / sizeof(ports[0]));
|
||||
if (port_count == 0) {
|
||||
printf("Error: No valid ports specified\n");
|
||||
platform_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("fscan-lite - Starting scan\n");
|
||||
printf("Target: %s\n", target_host);
|
||||
printf("Ports: %d ports to scan\n", port_count);
|
||||
printf("Timeout: %d seconds\n", timeout);
|
||||
printf("=================================\n");
|
||||
|
||||
/* 执行扫描 */
|
||||
result = scan_host_ports(target_host, ports, port_count, timeout);
|
||||
|
||||
printf("=================================\n");
|
||||
printf("Scan completed: %d open ports found\n", result);
|
||||
|
||||
/* 清理资源 */
|
||||
platform_cleanup();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* platform.c - 平台抽象层实现
|
||||
*
|
||||
* 实现最基础但最可靠的平台相关功能
|
||||
*/
|
||||
|
||||
#include "../include/platform.h"
|
||||
|
||||
/* 全局初始化标志 */
|
||||
static int platform_initialized = 0;
|
||||
|
||||
/*
|
||||
* 平台初始化
|
||||
* Windows: 初始化Winsock
|
||||
* Unix: 无需特殊初始化
|
||||
*/
|
||||
int platform_init(void) {
|
||||
if (platform_initialized) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
WSADATA wsaData;
|
||||
int result;
|
||||
|
||||
/* 初始化Winsock - 请求版本2.0,兼容Win98 */
|
||||
result = WSAStartup(MAKEWORD(2, 0), &wsaData);
|
||||
if (result != 0) {
|
||||
/* 如果2.0失败,尝试1.1(Win95/NT兼容) */
|
||||
result = WSAStartup(MAKEWORD(1, 1), &wsaData);
|
||||
if (result != 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
platform_initialized = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 平台清理
|
||||
*/
|
||||
void platform_cleanup(void) {
|
||||
if (!platform_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
WSACleanup();
|
||||
#endif
|
||||
|
||||
platform_initialized = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 设置socket超时
|
||||
* 兼容所有平台的最可靠方法
|
||||
*/
|
||||
int set_socket_timeout(socket_t sock, int timeout_seconds) {
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
DWORD timeout_ms = timeout_seconds * 1000;
|
||||
|
||||
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO,
|
||||
(char*)&timeout_ms, sizeof(timeout_ms)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO,
|
||||
(char*)&timeout_ms, sizeof(timeout_ms)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
#else
|
||||
struct timeval tv;
|
||||
tv.tv_sec = timeout_seconds;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO,
|
||||
(void*)&tv, sizeof(tv)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO,
|
||||
(void*)&tv, sizeof(tv)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 设置socket为非阻塞模式
|
||||
* 跨平台兼容实现
|
||||
*/
|
||||
int make_socket_nonblocking(socket_t sock) {
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
u_long mode = 1;
|
||||
return ioctlsocket(sock, FIONBIO, &mode);
|
||||
#else
|
||||
int flags;
|
||||
|
||||
flags = fcntl(sock, F_GETFL, 0);
|
||||
if (flags == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
flags |= O_NONBLOCK;
|
||||
return fcntl(sock, F_SETFL, flags);
|
||||
#endif
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* scanner.c - 核心TCP端口扫描实现
|
||||
*
|
||||
* 极简但可靠的端口扫描逻辑,专注于内网环境
|
||||
*/
|
||||
|
||||
#include "../include/platform.h"
|
||||
|
||||
/*
|
||||
* 基础TCP连接测试
|
||||
* 返回: 1=端口开放, 0=端口关闭, -1=错误
|
||||
*/
|
||||
int tcp_connect_test(const char* host, int port, int timeout) {
|
||||
socket_t sock;
|
||||
struct sockaddr_in addr;
|
||||
int result;
|
||||
|
||||
/* 参数验证 */
|
||||
if (!host || port <= 0 || port > 65535) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 创建socket */
|
||||
sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock == INVALID_SOCKET_VALUE) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 设置超时 */
|
||||
if (set_socket_timeout(sock, timeout) != 0) {
|
||||
close_socket(sock);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 设置目标地址 */
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons((unsigned short)port);
|
||||
|
||||
/* 转换IP地址 */
|
||||
addr.sin_addr.s_addr = inet_addr(host);
|
||||
if (addr.sin_addr.s_addr == INADDR_NONE) {
|
||||
/* 如果不是有效IP,当作域名处理 */
|
||||
struct hostent* he;
|
||||
he = gethostbyname(host);
|
||||
if (!he) {
|
||||
close_socket(sock);
|
||||
return -1;
|
||||
}
|
||||
memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
|
||||
}
|
||||
|
||||
/* 执行连接测试 */
|
||||
result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
|
||||
|
||||
/* 关闭socket */
|
||||
close_socket(sock);
|
||||
|
||||
/* 返回结果 */
|
||||
return (result == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 扫描单个主机的多个端口
|
||||
*/
|
||||
int scan_host_ports(const char* host, const int* ports, int port_count, int timeout) {
|
||||
int i;
|
||||
int open_count = 0;
|
||||
|
||||
if (!host || !ports || port_count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("Scanning %s...\n", host);
|
||||
|
||||
for (i = 0; i < port_count; i++) {
|
||||
int result = tcp_connect_test(host, ports[i], timeout);
|
||||
|
||||
if (result == 1) {
|
||||
printf("%s:%d open\n", host, ports[i]);
|
||||
open_count++;
|
||||
} else if (result == -1) {
|
||||
/* 静默处理错误,继续扫描 */
|
||||
}
|
||||
|
||||
/* 简单的进度指示 */
|
||||
if ((i + 1) % 100 == 0 || i == port_count - 1) {
|
||||
printf("Progress: %d/%d ports scanned\n", i + 1, port_count);
|
||||
}
|
||||
}
|
||||
|
||||
return open_count;
|
||||
}
|
||||
|
||||
/*
|
||||
* 解析端口范围字符串
|
||||
* 支持: "80", "80,443", "1-1000", "80,443,8000-8080"
|
||||
*/
|
||||
int parse_ports(const char* port_str, int* ports, int max_ports) {
|
||||
char* str_copy;
|
||||
char* token;
|
||||
int count = 0;
|
||||
|
||||
if (!port_str || !ports || max_ports <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 复制字符串以便修改 */
|
||||
str_copy = malloc(strlen(port_str) + 1);
|
||||
if (!str_copy) {
|
||||
return 0;
|
||||
}
|
||||
strcpy(str_copy, port_str);
|
||||
|
||||
/* 使用strtok(兼容性更好) */
|
||||
token = strtok(str_copy, ",");
|
||||
|
||||
while (token && count < max_ports) {
|
||||
char* dash = strchr(token, '-');
|
||||
|
||||
if (dash) {
|
||||
/* 处理范围 "start-end" */
|
||||
int start, end, i;
|
||||
*dash = '\0';
|
||||
start = atoi(token);
|
||||
end = atoi(dash + 1);
|
||||
|
||||
if (start > 0 && end > 0 && start <= end && end <= 65535) {
|
||||
for (i = start; i <= end && count < max_ports; i++) {
|
||||
ports[count++] = i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* 处理单个端口 */
|
||||
int port = atoi(token);
|
||||
if (port > 0 && port <= 65535) {
|
||||
ports[count++] = port;
|
||||
}
|
||||
}
|
||||
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
|
||||
free(str_copy);
|
||||
return count;
|
||||
}
|
||||
|
||||
/*
|
||||
* 简单的IP范围解析
|
||||
* 目前只支持单个IP,后续可扩展
|
||||
*/
|
||||
int parse_hosts(const char* host_str, char hosts[][MAX_HOST_LEN], int max_hosts) {
|
||||
if (!host_str || !hosts || max_hosts <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 目前简化实现:只处理单个主机 */
|
||||
if (strlen(host_str) < MAX_HOST_LEN) {
|
||||
strcpy(hosts[0], host_str);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -3,13 +3,11 @@ module github.com/shadow1ng/fscan
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/IBM/sarama v1.43.3
|
||||
github.com/denisenkom/go-mssqldb v0.12.3
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.9
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/gocql/gocql v1.7.0
|
||||
github.com/google/cel-go v0.13.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hirochachacha/go-smb2 v1.1.0
|
||||
github.com/huin/asn1ber v0.0.0-20120622192748-af09f62e6358
|
||||
@@ -21,11 +19,9 @@ require (
|
||||
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||
github.com/panjf2000/ants/v2 v2.11.3
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/sijms/go-ora/v2 v2.9.0
|
||||
github.com/stacktitan/smb v0.0.0-20190531122847-da9a425dceb8
|
||||
github.com/tjfoc/gmsm v1.4.1
|
||||
go.ciq.dev/go-rsync v0.0.0-20240304021629-0a3bb196e6d1
|
||||
go.mongodb.org/mongo-driver v1.17.4
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/net v0.32.0
|
||||
golang.org/x/sys v0.28.0
|
||||
@@ -42,17 +38,8 @@ require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/eapache/go-resiliency v1.7.0 // indirect
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
|
||||
github.com/eapache/queue v1.1.0 // indirect
|
||||
github.com/geoffgarside/ber v1.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
@@ -63,19 +50,12 @@ require (
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
||||
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
)
|
||||
|
||||
@@ -1,36 +1,26 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/IBM/sarama v1.43.3 h1:Yj6L2IaNvb2mRBop39N7mmJAHBVY3dTPncr3qGVkxPA=
|
||||
github.com/IBM/sarama v1.43.3/go.mod h1:FVIRaLrhK3Cla/9FfRF5X9Zua2KpS3SYIXxhac1H+FQ=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
||||
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
|
||||
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
|
||||
github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
|
||||
github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w=
|
||||
github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||
@@ -39,18 +29,24 @@ github.com/go-ldap/ldap/v3 v3.4.9 h1:KxX9eO44/MpqPXVVMPJDB+k/35GEePHE/Jfvl7oRMUo
|
||||
github.com/go-ldap/ldap/v3 v3.4.9/go.mod h1:+CE/4PPOOdEPGTi2B7qXKQOq+pNBvXZtlBNcVZY0AWI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus=
|
||||
github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/cel-go v0.13.0 h1:z+8OBOcmh7IeKyqwT/6IlnMvy621fYUqnTVPEdegGlU=
|
||||
github.com/google/cel-go v0.13.0/go.mod h1:K2hpQgEjDp18J76a2DKFRlPBPpgRZgi6EbnpDgIhJ8s=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -62,8 +58,6 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
@@ -95,13 +89,14 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 h1:IAukUBAVLUWBcexOYgkTD/EjMkfnNos7g7LFpyIdHJI=
|
||||
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166/go.mod h1:T4xUEny5PVedYIbkMAKYEBjMyDsOvvP0qK4s324AKA8=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lunixbochs/struc v0.0.0-20241101090106-8d528fa2c543 h1:GxMuVb9tJajC1QpbQwYNY1ZAo1EIE8I+UclBjOfjz/M=
|
||||
@@ -113,28 +108,18 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed h1:FI2NIv6fpef6BQl2u3IZX/Cj20tfypRF4yd+uaHOMtI=
|
||||
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0=
|
||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
|
||||
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
|
||||
github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sijms/go-ora/v2 v2.9.0 h1:+iQbUeTeCOFMb5BsOMgUhV8KWyrv9yjKpcK4x7+MFrg=
|
||||
github.com/sijms/go-ora/v2 v2.9.0/go.mod h1:QgFInVi3ZWyqAiJwzBQA+nbKYKH77tdp1PYoCqhR2dU=
|
||||
github.com/stacktitan/smb v0.0.0-20190531122847-da9a425dceb8 h1:GVFkBBJAEO3CpzIYcDDBdpUObzKwVW9okNWcLYL/nnU=
|
||||
github.com/stacktitan/smb v0.0.0-20190531122847-da9a425dceb8/go.mod h1:phLSETqH/UJsBtwDVBxSfJKwwkbJcGyy2Q/h4k+bmww=
|
||||
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
|
||||
@@ -142,32 +127,22 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.ciq.dev/go-rsync v0.0.0-20240304021629-0a3bb196e6d1 h1:lYxtzhvoRGnoET/RcKJDnRnmaHuGKBCUIj3D1ZubBNg=
|
||||
go.ciq.dev/go-rsync v0.0.0-20240304021629-0a3bb196e6d1/go.mod h1:xOHMiPHUTm8AQpxu4n14T8bRuT/izQISy8ycm/Q3LLY=
|
||||
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
@@ -175,17 +150,24 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
|
||||
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
@@ -195,6 +177,9 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -204,10 +189,11 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -232,9 +218,7 @@ golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
@@ -243,6 +227,10 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
@@ -250,20 +238,35 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo=
|
||||
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user