接收一批网络扫描原始数据(ip / port / banner),自动识别协议、软件与版本信息。
Client + Server 架构,Docker Compose 一键启动,生产级部署标准。
flowchart LR
A[scan dump JSON] --> C[client]
C -- "POST /fingerprint<br/>http://server:8080 (compose service DNS)" --> S[server :8080]
S --> E[fingerprint engine<br/>regex rules from rules.json]
S --> R[results JSON]
C --> T[table output]
H[docker HEALTHCHECK<br/>binary -healthcheck mode] -.-> S
- server:无状态 HTTP API(
POST /fingerprint批量识别、GET /health健康检查)。 - client:独立 CLI,读本地 JSON 文件 → 发给 server → 表格展示结果。
- 容器间只通过 compose 私有网络
banner-net上的服务名server访问,无 host IP 硬编码。 - 识别规则与程序代码解耦:规则全部在
internal/fingerprint/rules.json(数据驱动), 代码只负责执行规则引擎;规则文件通过go:embed内嵌进二进制,也可运行时用RULES_PATH指向外部文件热加载(免重新编译)。
docker compose up -d --build # 启动 server(client 为一次性任务,不随 up 常驻)
curl localhost:8080/health # 健康检查
docker compose run --rm client # 用 testdata/input.json 跑一次全量识别curl 直接调 API:
curl -s -X POST localhost:8080/fingerprint -d '[
{"ip":"1.2.3.4","port":22,"banner":"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3"},
{"ip":"1.2.3.5","port":80,"banner":"HTTP/1.1 200 OK\r\nServer: nginx/1.24.0"}
]'本地开发(不依赖 Docker):
go test ./... # 单元测试
go run ./cmd/server -addr :8080
go run ./cmd/client -server http://localhost:8080 -file testdata/input.json- 请求体:JSON 数组(或单个对象)
[{ "ip": string, "port": number, "banner": string }]。 - 响应:与输入一对一、保序、同数量的结果数组
[{ "ip", "port", "protocol", "product", "version", "os_hint", "confidence" }]。 - 认不出的记录返回
protocol: "unknown",永远不会因为某个 banner 无法识别而报错或崩溃。 - 限制:请求体 ≤ 10 MiB,单批 ≤ 10 000 条。
- 返回
200 {"status":"ok","service":"banner-fingerprint-server","time":...}。 - compose 的
HEALTHCHECK不是"进程活着"检查:server -healthcheck会在容器内发起 真实 HTTP GET 到/health,探针地址由实际监听地址推导(任意-addr都正确)。
| 状态码 | 含义 |
|---|---|
| 200 | 成功(含全部 unknown 的结果) |
| 400 | JSON 非法 / 空输入 / 批次超限 |
| 405 | 非 POST |
| 413 | 请求体超限 |
| 500 | 仅作为兜底(panic 恢复中间件),正常路径不会出现 |
| 退出码 | 含义 |
|---|---|
| 0 | 批次处理完成(包含 unknown 结果,不算失败) |
| 1 | 用法 / 文件读取错误 |
| 2 | 输入解析错误 |
| 3 | 网络错误(连不上 server) |
| 4 | server 返回错误响应 |
| 字段 | 说明 |
|---|---|
protocol |
协议族;未命中恒为 "unknown"(空字符串也不会出现) |
product |
具体软件(OpenSSH / Dropbear / nginx / Apache / Jetty / Microsoft IIS / lighttpd / Caddy / MySQL / MariaDB / Redis / ProFTPD / vsFTPd / Pure-FTPd;SMTP 问候语不携带产品名,product 为空) |
version |
软件版本;banner 未携带时为 ""(如 Redis、Pure-FTPd) |
os_hint |
从 banner 尾部提取的 OS 线索(如 SSH-2.0-OpenSSH_8.9p1 Ubuntu-3 → Ubuntu);没有则为 "" |
confidence |
0Server: nginx 无 /x.y)经回退规则识别产品 0.6,TLS 0.4(仅握手字节,无产品信息),unknown 恒为 0 |
两阶段、数据驱动:
- protocol 阶段:按规则 ID 排序逐个匹配 banner;首个命中确定
protocol。 自包含规则(如 SSH/MySQL/MariaDB/Redis)同时给出 product / version / confidence。 - product 阶段:泛型协议(HTTP/FTP)命中后,继续在该协议的规则子集内匹配具体产品。
有版本规则(
http-nginx)按 ID 先于无版本回退规则(http-nginx-noversion,置信 0.6)匹配: banner 带版本走精确规则,Server: nginx这类无版本头也能识别出产品。
规则字段:
扩展规则无需改代码:把 rules.json 挂到容器里并设置 RULES_PATH:
server:
environment:
RULES_PATH: /app/rules/rules.json
volumes:
- ./my-rules.json:/app/rules/rules.json:ro- 宽容 JSON 解析:banner 转储常见 C 风格
\xHH转义(非法 JSON)与 UTF-8 BOM, 客户端/服务端自动归一化(\x00→\u0000、剥离 BOM)后解析;标准 JSON 同样支持。 - banner 内任意二进制字节(NUL、非法 UTF-8、控制字符)由正则按字节匹配,不会异常。
- 服务端有 panic 恢复中间件兜底;单条记录失败不影响整批。
- 引擎是纯函数、并发安全;server 无状态,可水平扩展。
| 维度 | 实现 |
|---|---|
| 编译打包 | 多阶段构建:golang:1.23-alpine 编译 → 静态剥离二进制(CGO_ENABLED=0 -trimpath -s -w) |
| 运行镜像 | 最终阶段 FROM scratch(空用户态,无 shell/curl/包管理器,攻击面最小),镜像仅 ~6 MB |
| 权限 | 固定非 root UID 65532,cap_drop: ALL,no-new-privileges,read_only 根文件系统 |
| 健康检测 | 真实 HTTP 探针 + compose depends_on: condition: service_healthy(client 只在 server 就绪后启动) |
| 容器间访问 | 私有 bridge 网络 + 服务名 DNS 收敛,client 无对外端口 |
| 优雅停机 | SIGTERM/SIGINT → http.Server.Shutdown 排空连接 |
| 日志 | 结构化访问日志;json-file 驱动限制 10MB×3 滚动 |
├── cmd/
│ ├── server/main.go # HTTP API 服务(含 -healthcheck 探针模式)
│ └── client/main.go # 批量客户端 CLI
├── internal/
│ ├── fingerprint/ # 指纹引擎:engine.go + rules.json(规则表)
│ ├── parse/ # 宽容 JSON 解析(\xHH / BOM / \u0000)
│ └── model/ # 输入输出类型
├── testdata/input.json # 自测数据(23 条,覆盖全部识别目标与脏数据)
├── Dockerfile # 多阶段:build / server(scratch) / client(scratch)
├── docker-compose.yml # 一键启动编排
└── README.md
go test ./... 覆盖:自测语料逐条断言(协议/产品/版本/OS/置信度)、unknown 契约、
批量保序、泛化场景(HTTP/2、不同 MySQL 包长、Redis 错误类、dropbear/lighttpd/Caddy)、
脏 banner(NUL 尾随、非法 UTF-8)、宽容 JSON(BOM + \xHH)。
{ "id": "http-nginx", // 唯一 ID "stage": "product", // protocol | product "protocol": "HTTP", // 所属协议(product 阶段必填) "product": "nginx", // 产品名 "pattern": "(?i)Server:\\s*nginx/([0-9][0-9.]*)", // Go RE2;捕获组 1 = version "os_pattern": "...", // 可选:捕获组 1 = os_hint "confidence": 0.9 // 该规则的置信度 }