Scanner-RS 维护文档¶
项目概述¶
scanner-rs 是一个网络扫描编排器。支持扫描器注册认证、从 HTTP API 获取扫描目标、调用 masscan 发现开放端口、调用 zgrab2 执行应用层服务识别,并以 JSON 格式通过 HTTPS POST 上报结果。
- 语言:Rust(edition 2024)
- 运行时:tokio 异步
- 目标平台:Linux
可选模块化热更新的具体接口和操作见 MODULE_UPDATES.md。
模块架构¶
src/
├── main.rs # Tokio 入口,调用库中的 agent_main
├── lib.rs # CLI、编排主循环、身份注册及共享模块
├── modules/ # 独立 Worker、IPC、签名包、任务版本绑定与更新
├── config.rs # TOML 静态配置加载/验证及旧身份字段清理
├── storage.rs # SQLite schema、身份和待上传状态持久化
├── scan/ # 扫描流水线、阶段编排与扫描引擎适配器
│ ├── orchestrator.rs # 阶段顺序、取消传播和失败策略
│ ├── plan.rs # 版本化扫描计划与阶段白名单校验
│ └── engines/
│ ├── masscan.rs # masscan 端口发现、进程控制与输出解析
│ ├── zgrab2.rs # zgrab2 服务识别、进程控制与 JSON 归一化
│ ├── dns.rs # DNS 查询、解析器选择与结果归一化
│ └── nuclei.rs # Nuclei 目标生成、受控执行与 JSONL 归一化
├── cache.rs # 按任务隔离、可持久恢复的结果缓存
├── upload.rs # 结果上报:HTTPS POST JSON
├── register.rs # 扫描器注册:POST /api/scanner/register
├── http_client.rs # 底层 HTTP GET / POST
├── reconnect.rs # 连接状态与带抖动的指数退避
├── health.rs # 运行凭据健康检查
├── sysinfo.rs # 系统信息采集(OS/CPU/RAM/uptime)
├── types.rs # 全部数据结构与配置类型
├── error.rs # 统一错误类型(thiserror)
├── logging.rs # 日志初始化(tracing)+ log_event! 宏
└── network.rs # IPv4/IPv6 公网出口探测与共享能力状态
模块职责与接口¶
lib.rs 与 main.rs¶
编排层。负责解析 CLI、身份管理、加载配置、组装 pipeline、驱动扫描循环。
| 函数 | 说明 |
|---|---|
run() |
主入口:系统信息 → TOML/SQLite 加载与迁移 → 身份处理 → health check / 扫描 |
handle_identity() |
处理 --id / --token,执行注册 POST,触发 SQLite 身份写入 |
scan_loop() |
循环体:领取任务 → 扫描 → 按任务缓存 → 上传;无任务时持续轮询 |
flush() |
构造包含 scanner_id/task_id 的上传载荷并提交 |
spawn_signal_handler() |
SIGINT/SIGTERM 注册 |
config.rs¶
pub(crate) fn load(path: &str) -> Result<LoadedConfig, ScannerError>
LoadedConfig 持有解析后的 AppConfig 以及原始文件内容。所有验证规则通过后才返回成功。
impl LoadedConfig {
pub(crate) fn clear_legacy_identity(&self) -> Result<(), ScannerError>
}
静态配置始终来自 --config 指定的 TOML。升级时若 TOML 中仍包含旧
scanner_id / runtime_token 或 token,身份成功导入 SQLite 后会以临时文件、fsync
和原子改名方式清空这些字段;原有注释、空行、键顺序全部保留。新注册不再向 TOML
写入身份或令牌。
storage.rs¶
StateStore 使用内置 SQLite,schema 由 PRAGMA user_version 版本化。数据库同时保存:
- 以规范化服务端 origin 为键的
scanner_id/runtime_token; - 当前待上传任务绑定的
scanner_id、task_id、稳定upload_id和下一批序号; - 按稳定 sequence 排序、按业务 identity 唯一约束的 observation JSON。
数据库使用外键、5 秒 busy timeout、synchronous=FULL 和立即事务。Unix 数据库文件权限
设置为 0600。身份按服务端隔离,切换 c_server / c_port 不会静默复用其他服务端令牌。
scan/orchestrator.rs¶
pub(crate) async fn run_scan(
config: ScanRuntimeConfig<'_>,
plan: &ScanPlan,
target: &TargetParams,
cancel_requested: &AtomicBool,
) -> Result<ScanOutcome, ScannerError>
编排器严格按计划执行 DNS、端口发现、服务识别和漏洞检测阶段,并统一传播取消状态。
任务可携带版本化 scan_plan。IP 任务支持 legacy、port_only、
service_inventory 和 vulnerability_scan;DNS 任务支持 dns_records、dns_ports 和
dns_full。没有 scan_plan 的旧服务端任务会按本地配置生成兼容计划。计划只描述白名单阶段,
不接收二进制路径或任意 CLI 参数。
新控制端把用户模式统一为“全部”“仅端口”“服务识别”“仅 DNS”。域名选择前三种时,扫描器
收到的父任务仍是只含 dns_lookup 的 dns_records;服务端从上传的 A/AAAA 结果生成并按
地址族拆分 vulnerability_scan、port_only 或 service_inventory 子任务。域名选择“仅 DNS”
时不会生成子任务。dns_ports 与 dns_full 仅为滚动升级兼容保留。
扫描器会防御性复核域名目标:必须至少包含两个由 . 分隔的标签,只接受 ASCII 字母、数字
和非首尾连字符;单标签最多 63 octets,完整 wire name 最多 255 octets(不含末尾根点的
点分形式最多 253 octets)。
心跳通过 scan_capabilities 上报支持的计划版本、profile 和阶段。本地关闭 zgrab2 时仅
声明 legacy、port_only 和 port_discovery;启用并验证 zgrab2 后才声明
service_inventory 与 service_detection。启用并验证 Nuclei 二进制及模板目录后才声明
vulnerability_scan 与 vulnerability_detection,供服务端能力调度使用。
masscan 通过本地 scanner.masscan_output_mode 选择输出契约:
compatible(默认)保留旧启动参数,接受标准 list、官方控制台的Discovered open port <port>/<protocol> on <ip>和旧的Discovered <port> <protocol> <ip>。standard_list额外传入--nointeractive --open-only --wait <seconds>,仅接受 list 数据、空行和注释;JSON/XML、旧完成标记或其他非预期 stdout 都会报错。- 标准记录
open <protocol> <port> <ip> <timestamp>校验 IP、端口、协议与 Unix 秒时间戳; 时间戳转换为毫秒。旧格式使用接收时间。按 IP/协议/端口去重,优先保留标准时间戳。 - 非开放状态以及非 TCP/UDP 的合法 list 记录过滤;字段损坏在两种模式下都使阶段失败。
COMPLETE/DONE_OK在兼容模式下仅记为提示,继续读取后续结果。ERROR作为输出错误处理。其他兼容日志会计数并警告,JSON/XML 始终报错。
只有 stdout/stderr 正常读完、进程退出码为 0 且没有取消、超时或解析错误才算成功。 零结果可以没有头尾注释,不能要求必须出现完成标记。stdout 每行最多 4096 字节, stderr 按块排空并最多保留 4096 字节;非零退出、读取失败和解析失败复用现有外部进程诊断。 取消及默认 86400 秒的总时限覆盖读取和等待退出;没有结果行不会触发空闲超时。 错误和取消路径终止并回收子进程,清理等待与 stderr 排空分别限制为 2 秒。
masscan 不启用 banner 探测;服务识别继续由 zgrab2 完成。输出模式、收包等待和运行时限 均属于本地配置,不能被服务端远程修改。默认配置保留旧检查点的兼容键;非默认 masscan 配置加入检查点标识,配置改变时不复用原扫描阶段。已生成的上传批次不受影响。 完整的参数、迁移验证与回滚步骤见masscan 标准输出适配与迁移。
scan/engines/zgrab2.rs¶
zgrab2 使用 multiple 模式。扫描器把每个开放 TCP 端点编码成 IP,,TAG,PORT。
TAG 根据端口映射为 ssh、http、https、tls、smtp、mysql、postgres
等模块;未映射端口使用 banner。SMTPS、FTPS、IMAPS、POP3S、MQTTS 使用独立
TAG,但分别复用 zgrab2 的 smtp、ftp、imap、pop3、mqtt 模块及其隐式
TLS 参数。
status=success:写入规范化service,并提取可用的短 banner;- 每个端点只保留最终服务关联的
evidence,不上传完整探测尝试和候选列表; - 应用模块根级结果已经包含
tls时只保存一份应用握手,HTTPS 才额外关联独立 TLS; - 超时、拒绝或协议错误:
service保持为空,只保留探测次数,不保存所有失败详情; - 详细数据超过
max_result_bytes:只保留状态、错误、原大小和truncated=true。
已知隐式 TLS 端口优先执行应用协议:465 → SMTPS、990 → FTPS、993 → IMAPS、 995 → POP3S、8883 → MQTTS。636(LDAPS)、853(DNS over TLS)以及 989(FTPS 数据通道)目前仍只执行通用 TLS,因为扫描器尚无相应应用握手模块。自适应模式在非标准 端口只获得 TLS 时不会提前结束,而会在剩余连接预算中优先尝试安全应用协议。
若 required=false,zgrab2 进程启动失败或非零退出只记录警告,masscan 端口结果照常上报;
若 required=true,此类错误使任务按现有可重试失败流程处理。任务取消会终止当前 masscan、
zgrab2 或 Nuclei 子进程。
cache.rs¶
上传批次按稳定顺序同时满足最多 500 条 observation 和序列化 JSON 不超过 8 MiB。
任何单条 observation 超过 8 MiB 会在上传前返回明确错误;正常服务详情仍受更小的
max_result_bytes 限制。
impl CacheManager {
pub(crate) fn begin_task_with_observations(&mut self, task_id: Uuid, observations: &[ScanObservation]) -> Result
pub(crate) fn upload_batches(&self, scanner_id: Uuid) -> Result<Vec<ScannerResultBatchUpload>, ScannerError>
pub(crate) fn clear(&mut self) -> Result
}
缓存保存类型化的 Port、Service、Dns 与 Vulnerability observations,并按各类型的
稳定业务字段生成身份去重。漏洞身份包含端点、模板 ID、matcher 和匹配位置。每个任务在
begin_task_with_observations() 时生成一次 upload_id,并在
一个 SQLite 事务中保存任务和全部去重结果,重试期间保持不变。
upload_batches() 每 500 条生成一批;空任务也生成一个最终空批,只有最后一批设置
is_final=true。缓存同时记录下一待确认批次,重试时跳过已收到成功响应的批次;若响应
丢失,则用相同批身份重试当前批。
--database 默认为 ./scanner-state.sqlite3。启动时若数据库存在待处理状态,扫描器在领取
新任务前先恢复上传;observed_at、upload_id 和 batch_index 因而不会因重启改变。
旧 --state-file(默认 ./scanner-state.json)只作为升级迁移输入:数据库无待处理任务且
旧文件存在时,将其事务导入后改名为 .json.migrated。
服务端维护协作¶
服务端进入维护模式后停止分配新任务,并在心跳响应中携带
task_action=finish_and_cache。已经领取任务的 Scanner 不会终止扫描:服务端冻结任务
租约,Scanner 在本地完成扫描并把结果写入 SQLite 持久状态。服务端不可访问或仍处于维护
状态时不上传结果,每隔 retry_after_seconds 探测一次维护状态。
管理员手动退出维护后,Scanner 优先补传缓存结果,再恢复领取任务。服务端默认保留 24 小时补传宽限期;宽限期内任务不会分配给其他 Scanner,超期后才回收任务。
upload.rs¶
impl Uploader {
pub(crate) fn new(endpoint: String, client: ServerHttpClient) -> Self
pub(crate) async fn upload(&self, payload: &ScannerResultBatchUpload) -> Result<(), ServerError>
}
upload() 使用运行令牌作为 Bearer 凭据,顺序发送版本 3 结果批。失败时保留任务、
upload_id 和所有 observations;再次上传会产生相同批身份,由服务端幂等确认。
扩展加密:在 upload() 中加密 payload 后再发送,调用方无需改动。
register.rs¶
pub(crate) async fn register(
c_server: &str,
c_port: i64,
verify_tls: bool,
connect_timeout_seconds: u64,
request_timeout_seconds: u64,
scanner_id: &str,
token: &str,
) -> Result<RegisterOutcome, ScannerError>
使用一次性注册令牌作为 Bearer 凭据,POST { scanner_id } 到注册接口。成功时解析并返回 runtime_token;401 不保存注册令牌。
sysinfo.rs¶
pub(crate) fn gather() -> Result<SystemInfo, ScannerError>
读取 /proc/cpuinfo、/proc/meminfo、/proc/uptime、/etc/os-release,采集 OS 名称/版本、CPU 型号/核心数、内存总量、运行时间。启动时自动打印。
http_client.rs¶
pub(crate) async fn fetch_task(client: &ServerHttpClient, url: &str) -> Result<FetchTaskOutcome, ServerError>
任务领取、心跳、任务状态上报和结果上传共享一个 reqwest::Client。客户端统一设置建连与
全请求超时,并把网络错误和 HTTP 响应分类为可重试故障、认证拒绝、策略拒绝及任务语义
冲突。408、429 和 5xx 可重试;429 会读取 Retry-After。
reconnect.rs¶
ConnectionState 通过 Tokio watch 通道向扫描主循环发布 Connecting、Online、
Reconnecting、AuthRejected 和 PolicyBlocked。主循环只在 Online 时领取新任务。
Backoff 使用带抖动的指数退避,成功心跳会清零连续失败次数;所有等待都可被停止信号
打断。
自动重连与任务租约¶
服务端领取任务响应包含 lease_expires_at_ms 和 lease_ttl_ms。心跳响应对活动任务返回:
{
"lease": {
"task_id": "019c0000-0000-7000-8000-000000000001",
"status": "renewed",
"expires_at_ms": 1770000000000,
"ttl_ms": 90000
}
}
renewed:扫描端按ttl_ms刷新本地单调时钟截止时间;lost:立即以LeaseLost原因取消本地任务,不发送服务端取消确认,也不上传结果;- 旧服务端不返回
lease时,扫描端为兼容现有部署,将成功心跳视为按领取任务时的 TTL 续租; - 活动任务重连等待受
active_task_reconnect_max_delay_seconds限制;等待或请求本身不能越过lease_safety_margin_seconds定义的安全线。
任务取消原因区分为服务端主动取消、租约丢失和本地关闭,防止把断线导致的租约丢失误报为 用户取消。
health.rs¶
pub(crate) async fn health_check(client: &ServerHttpClient, url: &str) -> Result<(), ScannerError>
健康检查调用任务接口的 dryrun=true 模式,只验证 scanner_id 和运行令牌,不领取任务。
types.rs¶
所有数据结构集中定义。配置类型使用 serde::Deserialize,上传类型使用 serde::Serialize。
error.rs¶
pub(crate) enum ScannerError {
Io(std::io::Error), // 文件 I/O
Json(serde_json::Error), // JSON 解析
Toml(String), // TOML 解析
Http(String), // HTTP 请求失败
Upload(String), // 结果上传失败
Config(String), // 配置验证失败
Scan(String), // 扫描执行失败
HealthCheck(String), // 健康检查失败
}
使用 thiserror::Error derive。
网络协议自动探测¶
network.rs 先通过 UDP 套接字检查内核能否为 IPv4/IPv6 选择源地址和路由,再使用两个
独立的 reqwest::Client 查询公网出口地址。每个客户端先访问服务端鉴权接口,失败后再
访问对应的备用 IP 回显接口:
- IPv4 客户端绑定
0.0.0.0; - IPv6 客户端绑定
::。
绑定未指定地址可以限制出站套接字的地址族,同时由操作系统选择具体接口地址。UDP
connect 只执行本地路由选择,不会发送数据包。两个地址族通过 tokio::join! 并行
执行,因此某个协议超时不会串行阻塞另一个协议。运行 Token 只发送给服务端接口,不会
发送给第三方或自定义备用接口。
pub(crate) struct NetworkCapabilities {
ipv4: NetworkFamilyCapability,
ipv6: NetworkFamilyCapability,
}
每个协议包含:
| 字段 | 说明 |
|---|---|
supported |
配置没有禁用该协议 |
ready |
内核能够为该地址族选择源地址和路由 |
public_ip |
API 返回的公网地址,查询失败时为空 |
reason |
禁用、无可用路由或公网 IP 查询失败等原因;公网 IP 查询失败不影响 ready |
服务端与备用接口均支持两种响应:
{"ip":"203.0.113.10"}
或纯文本:
203.0.113.10
响应必须能够解析为 IP,并与当前探测地址族一致。错误原因最多保留 256 个字符。
生命周期¶
- 普通运行启动时,在创建心跳任务前检查本机路由并完成首次并行公网 IP 查询。
- 扫描器使用
scanner_id和运行 Token 请求服务端/api/scanner/network/public-ip。 - 服务端接口失败时,再请求该地址族配置的
probe_url,且不携带运行 Token。 SharedNetworkCapabilities使用Arc<RwLock<_>>在探测任务与心跳任务之间共享快照。- 后台任务至少每 30 秒探测一次;实际周期来自
probe_interval_seconds。 - 每次心跳读取最新快照并发送给服务端。
- 任务响应中的
address_family在执行前再次与本地快照匹配。 - 如果不匹配,扫描器立即发一次心跳刷新服务端状态,然后以可重试失败拒绝任务。
心跳中的结构:
{
"network_capabilities": {
"ipv4": {
"supported": true,
"ready": true,
"public_ip": "203.0.113.10",
"reason": null
},
"ipv6": {
"supported": true,
"ready": false,
"public_ip": null,
"reason": "route_unavailable: ..."
}
}
}
能力边界¶
本机路由检查验证的是内核能否为目标地址族选择源地址和路由,不是完整扫描验收:
- 不能证明特定私网/ULA 网段存在路由;
- 不能证明防火墙允许扫描流量或返回包;
- IPv4 NAT 出口地址通常不能直接作为 masscan 的接口源地址;
- masscan 使用自己的网络栈,实际执行仍可能因接口、网关或权限失败。
因此 public_ip 只用于能力展示,不参与可用性调度,也不会自动传给 masscan 作为
--adapter-ip。受限环境可以部署内部 IP 回显接口,避免向默认第三方接口公开出口地址。
本地部署配置与远程运行配置¶
管理员可在控制端 /scanner/{id}/configuration 发布扫描器运行参数。心跳响应通知配置 revision;扫描器通过带运行凭据的 GET /api/scanner/runtime-config?scannerid={scanner_id} 拉取文档并暂存,在扫描循环的任务边界校验、应用。正在执行的扫描不会中途切换参数。
远程文档可调整 masscan 速率、服务识别策略/探测预算/并发/超时/结果限制、DNS 开关/记录类型/查询超时,以及 Nuclei 开关/严重级别/速率/并发/超时/结果限制等参数。合并规则为:
- masscan 运行速率取远程值与本地
scanner.rate的较小值。 - 服务识别、DNS 和 Nuclei 的有效开关为“本地 enabled 且远程 enabled”,远程不能开启本地禁用的引擎。
- 二进制路径、Nuclei 模板目录、服务端连接、身份、日志目录和网络探测等部署字段继续来自本地配置;远程文档不接收任意 CLI 参数。
- 不支持的 schema version、非法 revision 或越界参数不会替换当前有效配置;拉取失败也保留已有配置。
已应用的远程文档按服务端 origin 缓存到 --database 指定的 SQLite。启动时重新按本地限制校验缓存,合法则恢复,非法则记录警告并保留本地配置。应用成功但缓存写入失败时,本进程仍使用新配置并记录警告,重启后不能保证恢复该版本。心跳上报 applied_revision、状态和错误,供控制端核对。
修改本地部署限制需要重启扫描器;已有合法远程缓存会再次合并,并不会因修改 TOML 自动消失。需要丢弃远程缓存时,在扫描器停止后使用 cache clear --include-runtime-config;该命令也会清除对应待上传结果,应先阅读扫描缓存管理。重新连接后服务端仍可再次下发配置。
配置参考¶
config.toml¶
[scanner]
# Target API server hostname
c_server = "example.com"
# Target API server port
c_port = 443
# Masscan packet rate
rate = 10000000
# Masscan source port
source_port = 61000
# Verify TLS certificates (set to false for self-signed certs)
verify_tls = true
# Path to masscan binary
scanner_path = "./a"
# Legacy migration fields; active identity is stored in scanner-state.sqlite3
scanner_id = ""
runtime_token = ""
connect_timeout_seconds = 5
request_timeout_seconds = 15
reconnect_initial_delay_seconds = 1
reconnect_max_delay_seconds = 60
active_task_reconnect_max_delay_seconds = 10
lease_safety_margin_seconds = 10
[service_detection]
enabled = true
required = false
zgrab_path = "zgrab2"
senders = 200
server_rate_limit = 20
connect_timeout_seconds = 5
target_timeout_seconds = 15
max_result_bytes = 65536
[nuclei]
enabled = false
path = "nuclei"
templates_path = "/opt/nuclei-templates/safe"
severities = ["medium", "high", "critical"]
rate_limit = 20
bulk_size = 5
concurrency = 5
timeout_seconds = 10
retries = 1
max_runtime_seconds = 1800
max_findings = 10000
max_result_bytes = 262144
[network]
ipv4_enabled = true
ipv6_enabled = true
ipv4_probe_url = "https://api.ipify.org?format=json"
ipv6_probe_url = "https://api6.ipify.org?format=json"
probe_interval_seconds = 300
probe_timeout_seconds = 5
[upload]
# Endpoint for scan result upload (HTTPS POST JSON)
endpoint = "https://example.com/api/scanner/result"
字段说明¶
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
scanner.c_server |
string | — | 目标 API 主机名 |
scanner.c_port |
int | — | 目标 API 端口 |
scanner.rate |
int | — | masscan 本地发包速率 |
scanner.verify_tls |
bool | false |
是否验证 TLS 证书 |
scanner.scanner_path |
string | "./a" |
扫描器二进制路径;也可填写能从 PATH 解析的命令名 |
scanner.masscan_output_mode |
string | "compatible" |
compatible 或 standard_list,本地决定输出契约 |
scanner.masscan_wait_seconds |
int | 10 |
官方模式的收包等待时间,范围 0-300 秒;兼容模式不传入该参数 |
scanner.masscan_max_runtime_seconds |
int | 86400 |
每次 masscan 调用的总时限,范围 1-604800 秒,必须大于收包等待 |
scanner.scanner_id |
string | "" |
仅用于从旧 TOML 迁移 UUIDv7 身份;新身份存入 SQLite |
scanner.runtime_token |
string | "" |
仅用于从旧 TOML 迁移运行令牌;新令牌存入 SQLite |
scanner.connect_timeout_seconds |
int | 5 |
建立服务端连接的超时,范围 1-60 秒 |
scanner.request_timeout_seconds |
int | 15 |
单次请求总超时,不小于建连超时且不超过 300 秒 |
scanner.reconnect_initial_delay_seconds |
int | 1 |
首次自动重连退避,范围 1-60 秒 |
scanner.reconnect_max_delay_seconds |
int | 60 |
空闲状态最大重连退避,不超过 600 秒 |
scanner.active_task_reconnect_max_delay_seconds |
int | 10 |
活动任务最大重连退避,不超过普通最大退避 |
scanner.lease_safety_margin_seconds |
int | 10 |
租约到期前停止未确认任务的安全窗口,范围 1-60 秒 |
service_detection.enabled |
bool | false |
是否在端口发现后运行 zgrab2 |
service_detection.required |
bool | false |
zgrab2 整体失败时是否使任务失败 |
service_detection.zgrab_path |
string | "zgrab2" |
zgrab2 二进制路径或 PATH 中的命令名 |
service_detection.senders |
int | 200 |
zgrab2 并发发送协程数,范围 1-10000 |
service_detection.server_rate_limit |
int | 20 |
单个目标 IP 每秒连接上限,范围 1-10000 |
service_detection.connect_timeout_seconds |
int | 5 |
建连超时,范围 1-60 秒 |
service_detection.target_timeout_seconds |
int | 15 |
单目标探测总超时,不小于建连超时且不超过 300 秒 |
service_detection.max_result_bytes |
int | 65536 |
单服务结构化详情上限,范围 1024-1048576 字节 |
nuclei.enabled |
bool | false |
是否声明并执行漏洞扫描能力 |
nuclei.path |
string | "nuclei" |
Nuclei 二进制路径或 PATH 中的命令名 |
nuclei.templates_path |
string | "" |
审核并固定版本的本地签名模板目录;启用时必填 |
nuclei.severities |
string[] | medium/high/critical | 允许 info、low、medium、high、critical,共 1–5 项 |
nuclei.rate_limit |
int | 20 |
全局每秒请求上限,范围 1–10000 |
nuclei.bulk_size |
int | 5 |
每模板主机并行度,范围 1–1000 |
nuclei.concurrency |
int | 5 |
模板并行度,范围 1–1000 |
nuclei.timeout_seconds |
int | 10 |
请求超时,范围 1–300 秒 |
nuclei.retries |
int | 1 |
失败请求重试次数,范围 0–10 |
nuclei.max_runtime_seconds |
int | 1800 |
单任务最长运行时间,范围 10–86400 秒 |
nuclei.max_findings |
int | 10000 |
单任务结果数上限,范围 1–100000 |
nuclei.max_result_bytes |
int | 262144 |
单行 JSONL 上限,范围 4096–1048576 字节 |
network.ipv4_enabled |
bool | true |
是否自动探测并接受 IPv4 任务 |
network.ipv6_enabled |
bool | true |
是否自动探测并接受 IPv6 任务 |
network.ipv4_probe_url |
string | ipify IPv4 API | 服务端接口不可用时使用的 IPv4 IP 回显接口 |
network.ipv6_probe_url |
string | ipify IPv6 API | 服务端接口不可用时使用的 IPv6 IP 回显接口 |
network.probe_interval_seconds |
int | 300 |
重新探测周期,范围 30-86400 秒 |
network.probe_timeout_seconds |
int | 5 |
单次连接及总请求超时,范围 1-60 秒 |
upload.endpoint |
string | — | 扫描结果上报地址 |
验证规则¶
| 字段 | 条件 |
|---|---|
scanner.c_server |
非空 |
scanner.c_port |
> 0 |
scanner.rate |
1-2147483647 |
scanner.scanner_path |
普通扫描启动时必须能解析到存在且可执行的普通文件 |
service_detection.zgrab_path |
enabled=true 时必须能解析到存在且可执行的普通文件 |
nuclei.path |
enabled=true 时必须能解析到存在且可执行的普通文件 |
nuclei.templates_path |
enabled=true 时必须是可访问目录 |
network.probe_interval_seconds |
30-86400 |
network.probe_timeout_seconds |
1-60 |
upload.endpoint |
非空 |
扫描器在启动扫描循环前检查 scanner_path,启用服务识别时检查 zgrab_path,启用漏洞扫描
时检查 Nuclei 二进制和模板目录。显式的相对路径或绝对路径会直接检查;不含目录的命令名会从
PATH 查找。注册、--health-check 和 --get-system-info 模式不执行扫描,因此跳过该检查。
静态配置与运行状态¶
TOML 保存本地部署配置;支持远程调整的扫描参数还会与服务端下发文档合并。SQLite 保存身份、待上传状态和已应用的远程配置。详见本地部署配置与远程运行配置。
注册时必须同时提供 --id 和 --token,其中 --token 是不落盘的一次性注册令牌。成功后
scanner_id / runtime_token 写入 --database 指定的 SQLite,而不是配置文件。旧配置中的
scanner.token 不会被当作运行令牌;旧 scanner_id / runtime_token 会在首次升级启动时
导入 SQLite。清理旧字段时保留其余注释和键顺序。
命令行¶
scanner-rs [OPTIONS]
-c, --health-check 仅执行健康检查(不扫描)
-d, --debug 启用 DEBUG 日志
-i, --get-system-info 打印系统信息后退出
-f, --config <PATH> 指定配置文件路径(默认 config.toml)
--database <PATH> 身份和持久状态数据库(默认 scanner-state.sqlite3)
--state-file <PATH> 旧 JSON 状态迁移来源(默认 scanner-state.json)
--log-directory <PATH> JSON 日志目录(默认 ./logs)
--id <UUID> 扫描器标识(UUIDv7)
--token <TOKEN> 注册令牌
-h, --help 显示帮助
扫描器注册流程¶
首次注册¶
scanner-rs --id "019eb66c-..." --token "sLJiUUS6..."
- 从 CLI 读取
--id和--token - 使用
Authorization: Bearer <registration_token>POST{ scanner_id }到注册接口 - 收到响应:
- 200 → 解析
runtime_token,与 id 一起写入 SQLite 后退出 - 401 → 凭证无效或过期,不写入配置
- 5xx/网络错误 → 返回失败,不覆盖现有运行凭据
- 后续运行无需再传
--id/--token,程序按当前服务端 origin 从 SQLite 读取
重新注册时必须再次同时提供新的 --id 和一次性 --token。若当前身份仍有 SQLite 或旧
JSON 待上传结果,不允许改成另一 scanner ID;需先用原身份完成补传或确认放弃并由管理员
处理持久状态,避免结果归属到错误扫描器。
普通运行¶
# 从 SQLite 读取已有身份,直接启动扫描
scanner-rs
数据流¶
┌─────────────┐ GET /api/task/apply + Bearer ┌──────────────┐
│ target API │ ◄────────────────────────── │ scanner-rs │
│ (c_server) │ ──────── JSON ────────────► │ │
└─────────────┘ │ ① fetch │
│ ② deserialize│
┌─────────────┐ │ ③ masscan │
│ masscan │ ◄── spawn ───────────────── │ │
│ binary │ ─── open ports ───────────► │ ④ dispatch │
└─────────────┘ │ │
┌─────────────┐ │ ⑤ zgrab2 │
│ zgrab2 │ ◄── tagged targets ──────── │ │
│ binary │ ─── JSON results ─────────► │ ⑥ normalize │
└─────────────┘ │ │
┌─────────────┐ │ ⑦ nuclei │
│ nuclei │ ◄── open endpoints ──────── │ │
│ binary │ ─── JSONL findings ───────► │ ⑧ normalize │
└─────────────┘ │ ⑨ cache │
│ payload │
┌─────────────┐ POST /api/scanner/result │ ⑩ upload │
│ upload API │ ◄──── JSON ─────────────── │ │
│ (endpoint) │ │ │
└─────────────┘ └──────────────┘
┌─ 启动时 ──────────────────────────┐
│ POST /api/scanner/register │ ← 仅 --id + --token 时
│ Bearer registration_token │
│ { scanner_id } │
└────────────────────────────────────┘
数据生命周期¶
- (可选)register — 携带
--id --token时 POST 注册 - sysinfo — 采集并打印系统信息
- fetch — 使用运行令牌领取真实任务;404 时等待并继续轮询
- deserialize — 解析 task_id、task_target 和 task_port
- port discovery — masscan 仅执行 SYN/端口发现,输出 list 格式
- service dispatch — 按开放端口选择 zgrab2 module tag,仅处理 TCP
- service detection — zgrab2 受控并发执行应用层握手,逐行输出 JSON
- vulnerability detection — 漏洞计划使用本地审核模板对开放端点运行 Nuclei
- normalize/cache — 生成 port/service/vulnerability observations 并进行稳定身份去重
- payload/upload — 每 500 条分批上传,最终批确认后任务完成
上传 JSON 格式¶
{
"protocol_version": 3,
"scanner_id": "019c0000-0000-7000-8000-000000000001",
"task_id": "019c0000-0000-7000-8000-000000000002",
"upload_id": "019c0000-0000-7000-8000-000000000003",
"batch_index": 0,
"is_final": true,
"observations": [
{
"kind": "port",
"ip": "192.168.1.1",
"port": 80,
"protocol": "tcp",
"state": "open",
"source": "masscan",
"observed_at": 1787922000000
},
{
"kind": "service",
"ip": "192.168.1.1",
"port": 80,
"protocol": "tcp",
"service": "http",
"banner": "HTTP/1.1 200 OK",
"source": "zgrab2",
"observed_at": 1787922001000,
"data": {
"schema_version": 2,
"confidence": 100,
"selected_probe": "http",
"attempt_count": 4,
"selection_reason": "protocol_handshake",
"evidence": [
{
"role": "application_handshake",
"module": "http",
"result": {
"response": {
"status_line": "HTTP/1.1 200 OK"
}
}
}
]
}
}
]
}
构建与运行¶
前置依赖¶
- Rust 工具链 ≥ 1.85(edition 2024)
- masscan 二进制
- zgrab2 二进制(仅
service_detection.enabled=true时需要) - Nuclei 二进制和本地签名模板目录(仅
nuclei.enabled=true时需要)
构建¶
cargo build --release
运行¶
# 首次:注册扫描器
./target/release/scanner-rs --id "019eb66c-..." --token "reg-code"
# 后续:直接运行
./target/release/scanner-rs
# 指定配置文件
./target/release/scanner-rs --config /etc/scanner/config.toml
# 将身份和待上传状态放在持久数据目录
./target/release/scanner-rs \
--config /etc/scanner/config.toml \
--database /var/lib/network-mapping-scanner/scanner-state.sqlite3
# 健康检查
./target/release/scanner-rs --health-check
# 系统信息
./target/release/scanner-rs --get-system-info
测试¶
cargo test
如何扩展¶
Nuclei 漏洞扫描¶
Nuclei 适配器位于 scan/engines/nuclei.rs。它只使用服务端下发的固定
vulnerability_scan 计划和节点本地配置,不接受任务中的模板、命令或任意 CLI 参数。
[nuclei] 默认关闭;启用前必须准备经过审核、固定版本且仅包含签名模板的目录。运行时固定
禁用 Interactsh、未签名模板和自动更新,并应用并发、速率、超时、结果大小和数量上限。
结果使用协议版本 3 的 vulnerability observation,上报结构化元数据而不保存 Nuclei 原始
request/response。协议版本 1、2 仍可用于滚动升级,但版本 2 不能携带漏洞记录。
完整部署、安全边界、模板更新和排障流程参见 Nuclei 漏洞扫描部署与运维。
添加新扫描器¶
扫描流程扩展位于 scan/orchestrator.rs,具体工具适配器位于 scan/engines/:
- 添加新的 engine,并返回对应的类型化 observation
- 在
run_scan()中按路径/类型分发
其他模块无需改动。
添加数据加密¶
改动仅限 upload.rs:在 upload() 的 serde_json::to_string() 与 HTTP POST 之间插入加密步骤。
添加新的上报渠道¶
替换 upload.rs 中的 Uploader,或新增 KafkaUploader 等结构体。lib.rs 只需切换实例化。
添加新的配置项¶
静态部署配置继续保持易扩展:
- 在
types.rs对应 Config 结构体中加字段(#[serde(default)]实现向后兼容) - 在
config.rs的validate()中添加验证规则 - 在
config.toml和字段说明中补充示例 - 在
lib.rs或对应模块中使用新字段
需要跨重启修改的运行状态则加入 storage.rs:提升 SCHEMA_VERSION,按旧版本顺序执行
事务迁移,并为升级和新建数据库各添加测试。不要把一次性注册令牌写入任何表。
信号处理¶
收到 SIGINT / SIGTERM 时:
- 设置全局
STOP标志 - 当前扫描完成后不再获取新目标
- 执行最终
flush()— 将缓存数据上传 - 正常退出
日志¶
| 目标 | 格式 |
|---|---|
| stderr | 人类可读,含文件+行号 |
./logs/scanner.log.YYYY-MM-DD |
JSON,按日滚动 |
级别:默认 INFO,--debug 启用 DEBUG。如果设置了 RUST_LOG,其过滤规则优先于 CLI 默认级别,例如 RUST_LOG=scanner_rs=debug scanner-rs。
DEBUG 日志覆盖配置加载、启动模式、任务 HTTP 状态、任务上下文、心跳与服务端命令、masscan 启动参数和进程状态、masscan stderr、输出解析统计、缓存及上传状态、重试延迟和退出清理。日志不会记录注册令牌、运行令牌或 HTTP Authorization 头;但会包含扫描目标、端口和扫描器输出,需按敏感运维数据保护日志文件。
依赖¶
| crate | 用途 |
|---|---|
tokio (full) |
异步运行时 + 进程管理 + 信号处理 |
clap (derive) |
命令行参数解析 |
serde + serde_json |
序列化 / 反序列化 |
toml_edit (serde) |
TOML 读写(注释保留) |
uuid (v7) |
UUIDv7 生成 |
reqwest (json) |
HTTP 客户端 |
rusqlite (bundled) |
SQLite 身份、扫描状态与配置持久化 |
chrono (serde) |
时间戳生成 |
tracing + tracing-subscriber + tracing-appender |
结构化日志 |
thiserror |
错误类型 derive |
依赖版本与功能特性以 Cargo.toml 为准。