chore: v0.6.0 baseline — 主题美化前存档

已实现:HTTP/TCP/Banner 探测、90 天可用率、表格/卡片视图、
数据源过滤、UptimeRobot 双向兼容、服务端驱动的刷新节奏、
刷新接口滥用防护与信息泄露控制。
This commit is contained in:
tamakyi
2026-10-13 18:31:58 +08:00
commit 3db3ce570a
31 changed files with 5136 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
# 部署方案详解
四种方案按「成本从低到要求、数据从准到轻」排列。所有方案共用同一份 `public/` 目录与 `public/monitors.json` 配置。
---
## 方案一:本地运行(Node)
最适合内网自用与开发调试,数据最实时。
```bash
node prober/node/index.js # 默认端口 8787
PORT=9000 node prober/node/index.js # 自定义端口
```
- 面板:`http://localhost:8787/`
- 状态 API:`http://localhost:8787/api/status`
- 改 `public/monitors.json` 后自动热重载,无需重启
- 历史心率存于 `public/status.json`,重启后自动恢复
- 90 天可用率按天累积在 `public/status.json`,重启不丢(每次探测先恢复分桶再追加)
- 面板刷新即重新检测(`/api/refresh` 等待本轮完成再返回);多标签页同时刷新只触发一轮探测
- 后台常驻可用 pm2 / systemd / Windows 服务,按需自行封装
**适用**:监控内网服务(面板浏览器与目标同内网时,浏览器直连模式也能用)。
---
## 方案二:GitHub Pages + Actions(零成本公网)
架构:仓库里的 `public/` 整体作为 Pages 站点;Actions 每 5 分钟跑一轮 Node 探针,把 `public/status.json` 提交回仓库,面板以 `static` 模式读取。
步骤:
1. 推送代码到 GitHub 仓库(`main` 分支)
2. 仓库 **Settings → Pages → Build and deployment → Source** 选 **GitHub Actions**
3. 首次提交时建议带一份占位数据,避免页面空白:
```bash
node prober/node/index.js --once --demo # 生成演示 status.json
git add public/status.json && git commit -m "chore: demo status" && git push
```
4. 之后 `pages.yml` 负责每次推送自动部署;`monitor.yml` 每 5 分钟更新一次 `status.json`(提交信息为 `chore: update status snapshot`)
5. 访问 `https://<用户名>.github.io/<仓库名>/`
注意事项:
- Actions 计划任务最小粒度 5 分钟,数据最多 5 分钟延迟;心跳图按「检测次数」记录,5 分钟粒度下 48 格 ≈ 4 小时
- 90 天可用率同样按「检测次数」统计,5 分钟粒度约 288 次/天;每轮 Actions 会把当日分桶续写到 `status.json` 并提交,因此可用率数据跨运行累积(若某天 Actions 未运行,该日色条会显示「无数据」而不是 0%)
- **静态快照模式没有服务端,面板无法触发重新检测**:此方案下刷新只是重新读取仓库里的 `status.json`,数据新鲜度完全取决于 Actions 的运行频率(面板页脚会显示「数据源:静态快照」以作区分)
- 若仓库开启了分支保护,需允许 `github-actions[bot]` 推送(workflow 已声明 `contents: write` 权限)
- 免费额度对公开仓库无限量,私有仓库每月 2000 分钟,足够
---
## 方案三:Cloudflare Workers(边缘实时)
架构:Worker 每分钟由 cron 触发,用 `fetch` 探测 HTTP、用 `cloudflare:sockets` 探测 TCP,状态写入 KV;同 Worker 伺服 `public/` 静态资源与 `/api/*`。面板以 `api` 模式实时读取。
```bash
# 1. 安装 wrangler(任选其一,不影响本项目运行时)
npm install -g wrangler
# 2. 登录并创建 KV 命名空间
wrangler login
wrangler kv namespace create MONITOR_KV
# 把输出的 id 填到 edge/cloudflare/wrangler.toml 的 [[kv_namespaces]].id
# 3. 部署(wrangler.toml 已指向 ../../public 作为静态资产,monitors.json 随之部署,无需手动导入 KV)
cd edge/cloudflare
wrangler deploy
```
- 面板与 API:`https://<worker 域名>/` 与 `/api/status`
- 手动触发一轮:`curl https://<worker 域名>/api/refresh`(等待本轮完成后返回最新快照,与本地探针同语义)
- cron 表达式在 `wrangler.toml` 的 `[triggers].crons` 中调整(`* * * * *` = 每分钟)
注意事项:
- **TCP 出站连接(`cloudflare:sockets`)需要 Workers 付费计划($5/月)**;免费计划下 `http` 类型探测与面板完全正常,`tcp` 类型会报错并记为离线
- KV 为最终一致存储,刚写入后立即读取可能有秒级延迟,对分钟级巡检无影响
- 90 天可用率分桶随 state 一起存于 KV,按 **UTC 日期** 分桶(边缘环境无本地时区);每分钟 cron 约 1440 次/天
- 免费版 Worker 每日 10 万次请求,面板静态资源走 Assets 不计入 Worker 请求数
- 中国大陆访问 Worker 默认域名不稳定,可绑定自有域名(需 Cloudflare 账号管理 DNS)
---
## 方案四:EdgeOne Pages(国内加速)
EdgeOne Pages 当前以静态托管为主,采用与方案二相同的「静态 + 定时数据」路径,国内访问体验更好。
步骤:
1. 登录腾讯云 EdgeOne 控制台,进入 **Pages → 新建项目 → 导入 Git 仓库**(或直接上传 `public/` 目录)
2. 构建配置:无构建步骤,输出目录填 `public`
3. 数据更新二选一:
- **A. GitHub Actions 驱动**:保持方案二的 `monitor.yml` 不变,EdgeOne 侧开启「构建时拉取最新提交」或配置 Webhook 触发重新部署
- **B. CI 推送触发**:在 `monitor.yml` 末尾追加一步,调用 EdgeOne 的部署 API / Webhook(按控制台提示填写)
4. 绑定自定义域名(需要备案的域名走国内加速线路)
说明:EdgeOne Pages 若上线边缘函数(类 Pages Functions),可把 `edge/cloudflare/worker.js` 的 `/api/*` 部分平移过去,实现分钟级实时数据;当前初稿以静态路径交付,接口契约完全一致,迁移时面板无需改动。
---
## 90 天可用率在各方案下的表现
可用率按「探测次数」加权统计,因此**探测频率直接决定精度**,也决定色条填充速度:
| 方案 | 每次探测间隔 | 每天样本数 | 90 天色条填满所需时间 |
| --- | --- | --- | --- |
| 本地 Node | 按 `monitors.json` 的 `interval`(示例 60s) | ≈1440 | 90 天 |
| Cloudflare Workers | cron 每分钟 | ≈1440(UTC 日期分桶) | 90 天 |
| GitHub Pages + Actions | ≥5 分钟(Actions 下限) | ≈288 | 90 天 |
| EdgeOne Pages | 同 GitHub Pages | ≈288 | 90 天 |
注意:分桶数据随 `status.json`(或 KV state)持久化并滚动保留 90 天,**必须由探针持续写入才会累积**——静态快照方案下依赖 Actions 按计划运行;Actions 计划任务在仓库长期无活动时可能被 GitHub 自动暂停(后台重新启用即可)。
需要临时提升精度时,可把 `interval` 调到 30s 或更低(本地/Worker 均支持,注意目标站点的压力与自身限流)。
---
## 方案对比与选型建议
| 维度 | 本地 Node | GH Pages | CF Workers | EdgeOne Pages |
| --- | --- | --- | --- | --- |
| 数据延迟 | 秒级(interval) | ≤5 分钟 | ≤1 分钟 | ≤5 分钟 |
| 90 天可用率精度 | 高(≈1440 次/天) | 中(≈288 次/天) | 高(≈1440 次/天) | 中(≈288 次/天) |
| TCP 探测 | ✅ | ✅(Actions 内) | 付费版 ✅ / 免费版 ❌ | 同 GH Pages |
| 真实状态码 | ✅ | ✅ | ✅ | ✅ |
| 内网目标 | ✅(浏览器同网即可) | ❌ | ❌ | ❌ |
| 国内访问速度 | 本机最快 | 一般 | 一般 | 快 |
| 运维成本 | 需保活进程 | 零 | 零 | 零 |
建议:个人/演示用 **GitHub Pages**;生产公网监控用 **Cloudflare Workers**;服务对象在国内且以 HTTP 监控为主用 **EdgeOne Pages**;内网资产用 **本地 Node**。
+154
View File
@@ -0,0 +1,154 @@
# 安全说明:刷新滥用防护、SSH 探测与信息泄露控制
面向「公网可访问的状态页」这个前提编写。核心结论先给:
| 场景 | 建议 |
| --- | --- |
| 本地 / 内网自用 | 默认配置即可,无需额外改动 |
| 公网发布 | 静态快照(GitHub Pages / EdgeOne)或 Cloudflare Worker + `REFRESH_TOKEN`;**不要把 Node 探针直接暴露到公网** |
| 目标含内网资产 | 用 `local.monitors.json` 分层,公开配置只留 `id/name/group`,并开 `hideTargets` + `redactDetails` |
---
## 一、`/api/refresh` 的滥用风险与分层防护
「刷新即重新检测」意味着**一个未鉴权的 GET 请求会触发一次对所有目标的出站连接**。滥用的三种形态:
1. **探测放大器**:任何人反复调用,把本服务当作对目标站点施压的跳板(借刀杀人)
2. **资源耗尽**:每次强制刷新都会对所有目标发起连接,慢目标(如超时 10s)会长时间占用并发
3. **目标配额消耗**:被诱导高频探测的第三方目标可能限流甚至封禁我们的出口 IP(真实案例:GitHub 未认证 API 上限 60 次/小时,被高频探测打满后返回 403,面板如实记为离线)
### 防护措施(已实现)
| 位置 | 措施 | 默认 |
| --- | --- | --- |
| Node 探针 | 仅允许内网/回环来源触发;**只信任 socket 对端地址,不解析 `X-Forwarded-For`**(该头部可被伪造) | 开启 |
| Node 探针 | 每来源冷却 `settings.refreshCooldownSec`,超限返回 `429` + `Retry-After` | 5s |
| Node 探针 | 轮次去重:多标签页/多客户端共享同一轮探测;2s 内的重复请求直接复用快照 | 开启 |
| Node 探针 | 单轮上限 22s 兜底(`FORCED_MAX_WAIT_MS`),防止卡死连接长期占用请求 | 开启 |
| Node 探针 | 总开关 `settings.allowRefresh: false`,完全关闭强制探测(只读模式) | 可配 |
| Node 探针 | 拒绝响应只回错误码(`{"error":"refresh-throttled"}`),不回显目标与内部细节 | 固定 |
| Worker | **必须配置 `REFRESH_TOKEN` 才开放** `/api/refresh`,否则一律 403;token 比较用等长逐字节异或,避免按字符提前返回的时序差异 | 默认关闭 |
| 面板 | 收到 401/403 自动退回只读轮询(页脚标注「只读」);429 仅本轮降级并在按钮提示 | 自动 |
### 配置参考
```jsonc
{
"settings": {
"refreshInterval": 30, // 面板刷新节奏
"probeOnRefresh": true, // 刷新是否重新探测
"allowRefresh": true, // false = 完全关闭强制探测(公网部署建议)
"refreshLocalOnly": true, // 仅内网/回环来源可触发
"refreshCooldownSec": 5 // 每来源冷却
}
}
```
单个目标也可退出「刷新即检测」,避免被高频探测压垮配额:
```jsonc
{ "id": "github-api", "interval": 300, "probeOnRefresh": false }
```
### 已知限制(请按需叠加外部措施)
- **限流在进程内**:多个探针实例不共享计数;需要强约束时请在网关(Nginx / Cloudflare Rate Limiting)再加一层
- **反向代理场景**:因为不信任 `X-Forwarded-For`,前置代理时所有请求都来自代理的内网地址,`refreshLocalOnly` 会失效——此时应设 `allowRefresh: false`,或自行在代理层做认证与限流
- Worker 的同隔离体去重是 best-effort(不同边缘节点各自计数)
---
## 二、SSH 这类服务怎么探测
支持三种探测类型,按「侵入性从低到高」:
| 类型 | 做什么 | 适合 |
| --- | --- | --- |
| `tcp` | 仅建立 TCP 连接,测连通性与连接耗时 | 任意端口存活监控 |
| `banner` | 连接后**被动读取服务端主动发送的标识串**,可与规则比对 | SSH / SMTP / FTP / POP3 / IMAP / HTTP 等主动问候的协议 |
| `http` | 完整 HTTP 请求,读状态码 | Web 服务 |
### SSH 配置示例
```jsonc
{
"id": "ssh-gateway",
"name": "跳板机 SSH",
"group": "基础设施",
"type": "banner",
"host": "10.0.0.5", // 真实地址建议放 local.monitors.json
"port": 22,
"timeout": 5,
"interval": 300,
"expectBanner": "^SSH-2\\.0-", // 策略:必须是 SSH 2.0
"revealBanner": false // 默认不回显标识串(含软件版本)
}
```
`expectBanner` 让探测同时具备**合规检查**能力:例如要求 `^SSH-2\.0-OpenSSH_[89]`,若目标降级到旧版本,探测即判为离线并触发面板告警。
### 明确不做的三件事
1. **不做 SSH 登录/认证尝试**:会触发目标侧 fail2ban/sshguard 封禁,需要凭据,还会在目标留下审计噪音
2. **不发送任何应用层数据**:因此需要客户端先行发包的协议(Redis 的 `PING`、MySQL 握手)暂不适用——这是刻意保留的安全边界,不是能力缺失
3. **不在标识串不匹配时回显实际收到的内容**:避免把「目标是什么版本」写进公开的 `status.json`
banner 探测的副作用仅有一次 TCP 连接与读取,与端口扫描器不同:不产生登录失败记录,也不改动目标任何状态。
---
## 三、信息泄露控制
状态页天生是公开的,泄露面有三处,逐个封堵:
| 泄露面 | 风险 | 措施 |
| --- | --- | --- |
| `public/monitors.json` | 面板必需文件,静态部署必然公开,含真实 URL / 内网主机端口 | **私有配置分层**:公开文件只写 `id/name/group`,真实地址放仓库根的 `local.monitors.json`(已 gitignore),探针按 `id` 合并覆盖 |
| `status.json` / `/api/status` | 探针输出,Pages 部署会公开,`detail` 可能含地址、版本、错误码 | `settings.redactDetails: true` → detail 只保留「正常/异常」;TCP 详情已默认不含地址;UptimeRobot 镜像目标的地址在 `hideTargets` 开启时不下发 |
| 面板渲染 | 直接把地址、错误详情展示给访客 | `settings.hideTargets: true` → 不渲染任何地址(含 tooltip),显示「已隐藏」 |
| UptimeRobot API Key | 泄露后可读取(只读 Key 不能改配置,但会暴露监控清单) | 只放 `local.monitors.json`(gitignore)或环境变量 `UPTIMEROBOT_API_KEY`;`public/monitors.json` 里 `apiKey` 必须留空 |
| `/api/v2/getMonitors`(兼容接口) | 无鉴权,暴露全部目标与可用率 | 内容与公开面板一致,故默认开放;如需收敛请在其前方加带认证的代理,或设 `allowRefresh: false` 并只暴露静态快照 |
### 对外发布状态页的推荐组合
```jsonc
// local.monitors.json(不入库,仅本机/CI 环境存在)
{
"monitors": [
{ "id": "blog", "url": "https://blog.example.com" },
{ "id": "nas", "host": "192.168.1.10", "port": 5000 }
]
}
```
```jsonc
// public/monitors.json(公开)
{
"settings": { "hideTargets": true, "redactDetails": true },
"monitors": [
{ "id": "blog", "name": "博客站点", "group": "网站", "type": "http", "interval": 60, "timeout": 10 },
{ "id": "nas", "name": "家庭存储", "group": "基础设施", "type": "tcp", "interval": 300, "timeout": 5 }
]
}
```
这样即使 `status.json` 与面板完全公开,访客也只能看到「有哪些服务、是否在线、延迟、可用率」。
补充:`dataFilter`(`both` / `config` / `uptimerobot`)只决定**展示与快照输出**哪些目标,不影响探测行为;如果希望某个来源的数据完全不进入公开快照,应使用 `config` 过滤或把对应目标从配置中移除。
### 仍然会被公开的信息(请自行评估)
- **服务数量与名称**:命名不要包含敏感语义(不要叫「内网财务系统」)
- **延迟特征**:多次采样可反推大致网络位置
- **可用率与故障时间**:这本身就是状态页要公开的信息
- **GitHub Actions 场景**:CI 读不到未提交的 `local.monitors.json`,需用 repository secrets 在 workflow 中生成,或只监控公开目标
---
## 四、其他基线
- `.gitignore` 已包含 `local.monitors.json` 与 `.dev.vars`(Cloudflare 本地密钥)
- Worker 的 `REFRESH_TOKEN` 用 `wrangler secret put REFRESH_TOKEN` 管理,不要写进 `wrangler.toml`
- 面板为只读前端:无 cookie、无写入接口、无服务端会话;渲染配置字段时统一做 HTML 转义(`escapeHtml`),XSS 面仅剩配置自身
- Node 探针同时充当静态服务器,**不要直接暴露公网**;确实需要时在其前方加带认证的反向代理,并设 `allowRefresh: false`
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB