feat(overlay): 覆盖网络线 序㊾ —— 探针观测面改「两台中继并集」(附 序㊽ 源码/文档补提交)
序㊾(本棒):
- scripts/overlay-probe.cjs:OBS-01 / OBS-08 / OBS-09 的数据源由「只读 47 中继」
改为「按两台中继取并集」,消除 worker 归属漂移时的假红 / 假 SKIP
· endpoints 以 network:hostId:port 为键合并,online 取「或」、localPort 取在线那一侧
· used 按 network/hostId 去重计数(不求和,避免凭空放大在册数)
· localPort 属中继机回环落点 ⇒ 按归属分机探活(106 侧落点由 106 机上探)
· derived(OBS-11)保持 47 视角;阈值与判据一律未放宽
· OBS-16 计数约束:对 47 /status 的读取仍为三次、Δ 只取 47 的 counters;
对端 106 的采样为独立一次,落在第三次采样之后,不进 (status2, status3] 门窗口
· 新增 --peer-status-fixture(并集的对端那一半)与「并集不可取证」强制留痕
- 交接单《覆盖网络-序45-低熵块治理-测熵与实现》§16 全节(§8 前前缀逐字未变)
- 参数表 §11.16 补记(§10 现算指纹未变,值格未动)
附(前几棒已完成并已部署、但尚未入仓的源码 / 文档):
- src/net/relay/content/*.ts、src/net/relay/index.ts、main.ts:块级寻址 C 域分离
- src/supervisor/orchestrator.ts、src/worker/agent.ts:日志采集与巡检(方案 C)
- test/overlay-content.test.mjs:随附用例(npm test = 200 pass / 0 fail / 1 skipped,Node 22)
- scripts/dshlog.mjs(跨机日志取证)、scripts/overlay-entropy.cjs(熵探针)
- dsh-server-docs/04-调整方案/129、133;INDEX.md / docs-manifest.json / 交接单 README 登记
This commit is contained in:
1 parent
45b4999d24
commit
d2ef362a98
20 files changed
+2998
-183
No files matched your search
@@ -0,0 +1,227 @@
|
||||
# 日志采集 · 实时巡检 · 事后溯源(方案 C 实现)
|
||||
|
||||
- 版本:**v1 实现稿**(2026-09-18)
|
||||
- 状态:✅ **已实现 · 本机实测通过**(工具 = 代码仓 `scripts/dshlog.mjs`)
|
||||
- 上游决策:**方案 C** —— 不装新日志服务(不装 Amber / 不装 Loki+Alloy),靠现有 journald + 自研观测面
|
||||
- 取消的候选:Amber(无预编译产物 / 单维护者 / 落盘异步不去重 ⇒ 与"日志原文行数"类判据冲突)· Loki+Alloy(组件数与内存代价,规模未到)
|
||||
- 复跑入口:`"E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe" scripts/dshlog.mjs help`
|
||||
|
||||
---
|
||||
|
||||
## 0. 一句话
|
||||
|
||||
**一条 CLI 把 47 / 106 的 journald 拉回本机按天归档;跨机时间线一键重建;巡检规则命中即出判据表与非零退出码。** 服务器侧**零安装、零新增端口、零常驻进程** —— 远端只用系统自带的 `journalctl`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状取证(为什么只能走这条路)
|
||||
|
||||
| 事实 | 实测值(2026-09-18) | 含义 |
|
||||
|---|---|---|
|
||||
| 47 日志分布(当日) | `dshs` 29274 · `dshs-worker` 7312 · `sshd` 3830 · `init.scope` 3155 · `dshs-relay` 830 | 主角是**四层单元**:Manager / Worker / relay / PG |
|
||||
| 106 日志分布 | `user@0` 2604 · `init` 1575 · `crond` 728 · `sshd` 339 | 同一套单元命名,靠 `host` 字段区分 |
|
||||
| journald 形态 | 两机均 **persistent**(`/var/log/journal/<machine-id>/`);47 = 769 MB · 106 = 130 MB | 可直接增量拉取;**但两机都没设上限**(默认吃磁盘 10%) |
|
||||
| 时钟 | 两机 **NTP 均已同步**(`timedatectl NTPSynchronized=yes`) | 跨机时间线**不需要自造校时** |
|
||||
| 用户实例(`dsh --profile`) | `_SYSTEMD_UNIT=dsh-<uid>-<hash>.scope` **无任何条目**;`journalctl _PID=<实例pid>` **无条目**;实例 `fd/1` 指向 `socket:[…]` | 🔴 **实例 stdout 不进 journald** —— 它被 Worker 用 socket 接管,只有 Worker 愿意转发的那部分才落在 `dshs-worker` 里 |
|
||||
| journalctl 版本 | 47 = systemd **239** · 106 = 255 | `--output-fields` 两机都支持(用 `awk NR==1` 取版本号,别用正则猜) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构(三层,无新增常驻组件)
|
||||
|
||||
```text
|
||||
[采集] 47 / 106 的 journalctl -o json ← 远端只读,不写任何文件
|
||||
│ ssh -C(压缩,见 §6.1)
|
||||
▼
|
||||
[归档] E:/dsh-logs/<host>/<YYYY-MM-DD>.ndjson.gz ← gzip 多成员追加,按天分片
|
||||
E:/dsh-logs/state.json ← 每节点 cursor / lastTs / NTP 状态
|
||||
E:/dsh-logs/hosts.json ←(可选)节点清单,缺省用内置
|
||||
▼
|
||||
[使用] q(查询)· timeline(溯源)· watch(巡检)· stats / ls / prune(运维)
|
||||
```
|
||||
|
||||
**三条不可让步的设计约束**
|
||||
|
||||
1. **零新增常驻服务 / 零新增监听口** —— 不装 agent、不开端口、远端不落任何脚本(`bash -s` 走 stdin);符合 R5 的"权限只准收窄"。
|
||||
2. **日志原文保真** —— `msg` 字段逐字落盘(本项目大量判据依赖日志原文的行数/字节数);非 UTF-8 字节转义为 `\xNN` 保留。
|
||||
3. **"拉取失败" 与 "确无日志" 永不混淆** —— 数据走 stdout、元信息走 stderr 哨兵 `__DSHLOG_EOF__ rc= errbytes=` + `__DSHLOG_LINES__ n`;两条通道物理分离。
|
||||
|
||||
---
|
||||
|
||||
## 3. 三个目标 → 怎么满足
|
||||
|
||||
| 目标 | 命令 | 判据 |
|
||||
|---|---|---|
|
||||
| **不同节点/设备的日志存储** | `collect`(增量 cursor 续拉 / `--since` 回填) | 四态:`OK` / `EMPTY` / `MISMATCH`(远端报数与本地解析数不等) / `FAIL`;非 OK 即退出码 2 |
|
||||
| **快速排查线上运行问题** | `q <词\|/正则/>` · `timeline` | `q` 支持跨机跨单元、时间窗、正则;`timeline` 把多机日志按时间戳合并成单条时间线 |
|
||||
| **实时 bug 监控** | `watch`(默认先自动增量续拉再巡检) | 8 条规则 → 判据表 `PASS/FAIL`;有 FAIL ⇒ 退出码 2(可作自动化判据) |
|
||||
| **事后溯源分析** | `timeline --from --to --grep --out` | 带毫秒排序 + `host/unit` 归属 + `pri`;输出可落文件归档 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 用法
|
||||
|
||||
```bash
|
||||
N="E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe"
|
||||
|
||||
# ① 拉取:首次回填 / 之后增量
|
||||
$N scripts/dshlog.mjs collect --since 6h --chunk 2h # 回填(自动去重)
|
||||
$N scripts/dshlog.mjs collect # 增量:只搬 cursor 之后的新行
|
||||
|
||||
# ② 查询
|
||||
$N scripts/dshlog.mjs q "EADDRINUSE" --since 24h
|
||||
$N scripts/dshlog.mjs q "/no-trusted-keys|信任链/" --host 106 --limit 20
|
||||
|
||||
# ③ 事后溯源(把一次故障从两机日志里拼出来)
|
||||
$N scripts/dshlog.mjs timeline --from 2026-09-18T16:00 --to 2026-09-18T17:00 \
|
||||
--grep "overlay|relay" --out E:/dsh-logs/tl-1617.log
|
||||
|
||||
# ④ 巡检(实时监控入口)
|
||||
$N scripts/dshlog.mjs watch --since 30m --report # 有 FAIL ⇒ rc=2
|
||||
$N scripts/dshlog.mjs watch --since 30m --json # 机器可读
|
||||
|
||||
# ⑤ 运维
|
||||
$N scripts/dshlog.mjs stats --detail # 归档分布(按节点/日期/单元 top4)
|
||||
$N scripts/dshlog.mjs prune --keep 14 # 保留策略(默认干跑,--apply 才删)
|
||||
```
|
||||
|
||||
**扩展新节点**(用户说的"不同设备"):在 `E:/dsh-logs/hosts.json` 加一条即可,命令无需改动。
|
||||
|
||||
```json
|
||||
{ "47": { "ssh": ["-p", "22", "[email protected]"], "label": "Manager + w-47" },
|
||||
"106": { "ssh": ["-p", "22", "test106"], "label": "w-106" } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 实测基线(2026-09-18 21:0x–21:2x)
|
||||
|
||||
| 项 | 实测 |
|
||||
|---|---|
|
||||
| 当日归档量 | 47 = **35,603 行** · 106 = **5,988 行**(gzip 后 47 ≈ 0.3 MB) |
|
||||
| 回填 6 h(3 块 × 2 h) | **2 分 28 秒**(含首次去重重读);单块上限受链路带宽支配 |
|
||||
| 增量续拉(1 块,含去重) | **19.5 秒** |
|
||||
| 单块原始体积 | 47 ≈ 1.6 MB / 小时(未压缩,全字段) |
|
||||
| 巡检扫描 41,226 行(6 h 窗口) | **< 3 秒**(纯本地 gzip 顺序扫) |
|
||||
| 跨机时间线 | 6 h 窗口内 41k 行,毫秒级排序,秒级完成 |
|
||||
|
||||
**压缩是最大杠杆**(见 §6.1):47 出方向未压缩实测 **~20 KB/s**(1.6 MB 要 84 秒),开 `ssh -C` 后同样数据 **11 秒**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键设计(每条都有实测出处,别改回去)
|
||||
|
||||
### 6.1 🔴 `ssh -C` 不可省 —— 7.6× 提速
|
||||
|
||||
实测同一块数据(1 h,1.6 MB)真传到本机:**不带 `-C` = 84 s** vs **带 `-C` = 11 s**。
|
||||
JSON 日志压缩率极高;`-C` 的开销是两端 CPU(1.6 MB 约几十毫秒),完全值得。
|
||||
⚠️ 不加 `-C` 时表现为"像是卡死了"(第一版跑 3 h 回填超过 5 分钟被外层超时杀掉,误判为代码 hang)。
|
||||
|
||||
### 6.2 数据/元信息**双通道**
|
||||
|
||||
远端脚本把 `journalctl` 的 stdout 逐行 `awk` 转发(数据),同时把**行数**写 stderr(`__DSHLOG_LINES__`),末尾再补 `__DSHLOG_EOF__ rc=… errbytes=…`(元信息)。
|
||||
⇒ `"确无日志"(0 行)` 与 `"拉取失败"(无哨兵 / rc≠0)` 在同一份输出里**天然可分** —— 这是本项目反复踩过的坑,此处按机制而非纪律解决。
|
||||
|
||||
### 6.3 归档是 **gzip 多成员**追加,且必须**同步写**
|
||||
|
||||
- 每次 `appendFileSync` 追加一个完整 gzip 成员 ⇒ `gunzip` / node `createGunzip` 都能顺序读回(已实测:596 + 248 两个成员读出 844 行)。
|
||||
- ⛔ 不能用 `createWriteStream` 异步管道 —— 进程收尾时可能未 flush,**静默丢最后一批**。
|
||||
|
||||
### 6.4 回填必须去重,续拉必须用 cursor
|
||||
|
||||
- **续拉**(无 `--since`):`journalctl --after-cursor=<cursor>` —— 精确、不重不漏。
|
||||
- **回填**(有 `--since`):先把已有分片的 `__CURSOR` 读进 Set,落盘时跳过 ⇒ 实测一次回填跳过 8100 行重复。
|
||||
⛔ 不去重会让"行数类判据"直接失真。
|
||||
|
||||
### 6.5 校时用 **NTP 状态**,不要自造往返估算
|
||||
|
||||
第一版用 `(远端date + 本地往返/2)` 估偏移,实测给出 **+1337 ms** 的假偏移(47 的 ssh RTT 达 3.7 s 且往返不对称)⇒ 拿它做校正会**制造**错序。
|
||||
改为读 `timedatectl show -p NTPSynchronized`:两机都是 `yes` ⇒ **不校正**,`timeline` 默认输出原始系统时间戳,并把 NTP 状态打在末尾。
|
||||
|
||||
### 6.6 版本号解析必须 `awk 'NR==1{…}'`
|
||||
|
||||
`journalctl --version` 是**多行**输出;不限定行会让变量变成 `"239\n0"` ⇒ `[: integer expression expected` ⇒ 静默退回全字段(体积翻倍)。
|
||||
症状是"结果没错但慢一倍",只在 `err:` 里留一行噪声。
|
||||
|
||||
### 6.7 巡检规则要**收紧到指向本项目故障**(误报比漏报更贵)
|
||||
|
||||
实测踩过的两条反例:
|
||||
|
||||
- `fatal` 裸写 ⇒ sshd 的 `ssh_dispatch_run_fatal`(客户端网络断)天天命中 ⇒ 收紧为 `PANIC|FATAL ERROR|unhandled…` + 排除 `sshd/crond/systemd-logind` 噪声单元。
|
||||
- `Stopped .*` 裸写 ⇒ 实例**正常退出**(用户关会话)会打 `Stopped /usr/bin/bwrap …` ⇒ 收紧为 `Failed with result|start request repeated|Main process exited, code=…`。
|
||||
|
||||
当前 8 条规则:进程级致命 / 内存被杀 / 端口连接失败 / 权限属主 / 磁盘写入 / **覆盖网络信任链被拒** / HTTP 5xx / 服务异常终止。
|
||||
> 实测有效:WATCH-06 在 106 上命中 `⚠ 取目录全部失败 ⇒ 回落到内置种子地址本身` —— 正是**序 ㉗ 记录的 E3 缺口**(106 候选链退化为单点),属真报。
|
||||
|
||||
---
|
||||
|
||||
## 7. 边界(红线遵守情况)
|
||||
|
||||
| 项 | 状态 |
|
||||
|---|---|
|
||||
| 新增公网监听口 | ⛔ **0 个**(只读 ssh,未改 nft / nginx / 任何监听) |
|
||||
| 服务器侧新增常驻进程 / 安装 | ⛔ **0 个**(只用系统自带 `journalctl`;脚本走 `bash -s` stdin,不在远端落文件) |
|
||||
| 生产值改动 | ⛔ **0 处** |
|
||||
| 权限 | 只读:`journalctl` 读取无需提权以外的任何放行;ssh 用既有连接 |
|
||||
| 归档位置 | `E:/dsh-logs/`(E 盘;**不在** git 仓库内) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 已知限制与未做项(如实登记)
|
||||
|
||||
1. 🔴 **实例层(`dsh --profile`)日志目前抓不到** —— 实例 stdout 被 Worker 用 socket 接管,**不进 journald**(`_SYSTEMD_UNIT=…scope` 与 `_PID=` 均为空,已双重取证)。当前只能拿到 Worker 转发的部分。
|
||||
⇒ **这是本方案最大的剩余缺口**;要补需改 Worker 的 stdout 接管方式(属改代码,另立一单),或让实例自己写日志文件(需确认官方 dsh 是否支持日志落文件,受 R2 约束)。
|
||||
2. **本机(WorkBuddy 开发机)未纳入** —— 本机是 Windows,无 journald;其日志在 `~/.workbuddy/logs/`,与"线上运行"关联弱,暂不入归档。
|
||||
3. **全文检索是线性扫描** —— 当前量级(每日 4 万行)毫秒级;若到 GB 级需换索引(届时正是上 Loki 的信号,见上游评估的触发条件)。
|
||||
4. **未设置 journald 上限** —— 两机 `/etc/systemd/journald.conf` 均为空(默认 10% 磁盘)。47 已 769 MB。建议后续加 `SystemMaxUse=`,避免日志本身成为磁盘事故源。
|
||||
5. `--threshold` 目前是**全规则统一阈值**,未做逐规则独立阈值。
|
||||
|
||||
### 8.1 日志保留策略 = 3 天(2026-09-18 落地)
|
||||
|
||||
**服务器侧**(两机均写 drop-in,⛔ 不覆盖主配置 `/etc/systemd/journald.conf`):
|
||||
|
||||
```ini
|
||||
# /etc/systemd/journald.conf.d/10-retention.conf
|
||||
[Journal]
|
||||
MaxRetentionSec=3d # 时间上限(用户口径"只保留3天的日志")
|
||||
SystemMaxUse=512M # 47 / 106 用 192M —— 容量兜底,防单日风暴吃满磁盘
|
||||
SystemMaxFileSize=32M # 47 / 106 用 8M —— 压小分片,提高"3 天"判定精度
|
||||
```
|
||||
|
||||
**本机归档**:`dshlog prune --keep 3 --apply`(默认干跑;默认保留 3 天,与服务器同口径)。
|
||||
|
||||
**🔴 关键实测:`--vacuum-time` 的判据是「分片起始记录时间」,且按**整片**删除**
|
||||
|
||||
⇒ 实际保留期 = **3 天 −(0 ~ 一片跨度)**。分片越大,"3 天"缩水越严重。
|
||||
这解释了为什么必须同时压小 `SystemMaxFileSize`:
|
||||
|
||||
| 机 | 分片跨度(默认 128M 时) | 清理后实际保留 | 压小分片后预期 |
|
||||
|---|---|---|---|
|
||||
| 47 | ~128M ≈ **1.5 天/片** | **1.35 天** ❌ | 32M ≈ 6h/片 ⇒ 2.75~3 天 |
|
||||
| 106 | ~47M ≈ **3 天/片** | **2.44 天** ❌ | 8M ≈ 9h/片 ⇒ 2.6~3 天 |
|
||||
|
||||
⚠️ **一次执行的偏差记录(如实留档)**:首次清理**未预判该判据**,按"末记录时间"估算,实测多删了一片 ——
|
||||
47 释放 608 MB(768→160 MB)、106 释放 59.5 MB(130→70 MB),但**保留期分别只有 1.35 天 / 2.44 天**,未达 3 天。
|
||||
受影响而**永久丢失**的区间:47 = 09-15 13:33 ~ 09-17 13:05;106 = 09-13 12:54 ~ 09-16 11:29(本机归档当时也尚未覆盖该区间 ⇒ 无副本)。
|
||||
⇒ **47 的保留跨度会随新数据每日增长约 1 天,约 1.7 天后自然恢复到 3 天并稳定**(无需人工干预)。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 9. 待你拍板
|
||||
|
||||
**是否挂周期自动化做"实时"巡检**:
|
||||
|
||||
**A. 挂 automation 每 30 分钟跑一次 `watch`,仅 FAIL 时出报告(推荐)**
|
||||
优点:真"实时",异常半小时内可见,且只在有 FAIL 时才有可读产出。
|
||||
缺点:每天 48 次新会话,按本项目实测的自动化成本(每轮 5–9 积分)估算约 **250–430 积分/天**,成本可观。
|
||||
|
||||
**B. 挂 automation 每天 1 次汇总(如 08:00)**
|
||||
优点:成本低(约 5–9 积分/天),能发现"过夜积累"的问题。
|
||||
缺点:不是实时;白天的突发故障要等次日,或靠人工跑 `watch`。
|
||||
|
||||
**C. 不挂,保持按需手动执行**
|
||||
优点:**零成本**,需要时一条命令 10–20 秒出结果;当前平台规模小(47 仅 0–2 实例),"现拉现看"足够。
|
||||
缺点:无人自动发现问题,依赖你或我主动去查。
|
||||
|
||||
我的倾向:**先 C,等出现一次"事后才发现"的线上问题再上 A** —— 理由是本项目已有 `overlay-probe.cjs` 这类主动探针承担"判据式巡检",`watch` 的价值主要在**事后取证**;而实时性的成本(积分)与当前规模不匹配。若你认为线上稳定性优先于积分,直接上 A(我按"每 30 分钟 + 仅 FAIL 出报告"落地)。
|
||||
@@ -0,0 +1,198 @@
|
||||
# 覆盖网络 · 低熵块治理方案(**C 域分离 + D 非确定性**)
|
||||
|
||||
- 版本:**v1 规划稿**(2026-09-18 · 规划棒 · ⛔ 零代码 / 零服务器改动)
|
||||
- 状态:📐 **规划完成 · 待执行**(第一个动作 = **补测「低熵块种类数 / 体积 / 占首屏包比例」**,⛔ 从未测过)
|
||||
- 上游决策:**用户 2026-09-18 21:28 拍板 —— 采纳 C + D**;**B(OPRF / SA-MLE)降级为可选加强、⛔ 本轮不立项**
|
||||
- 权威来源(⛔ 不另起炉灶、⛔ 不重查文献、⛔ 不重新评估方案优劣):
|
||||
- 工作区根 `调研_MLE加密去重最优方案_20260918.md`(**§3** 六方案族 / **§5** 结构性发现 / **§5.5** 复杂度与收益)
|
||||
- `.workbuddy/memory/2026-09-18.md` **21:1x / 21:5x / 21:2x** 三节(C / D / B 定义、量化读数、"**收敛加密下持组密钥 ≠ 能解密**"这一前提)
|
||||
- 阈值与判据面单一来源:工作区根 `参数表_覆盖网络_20260917.md`(⚠️ 现版 §10 指纹 = `d408d640246a980f702fe7b0a2895219`,**以现算为准**)
|
||||
- 执行载体:`交接单/覆盖网络-序45-低熵块治理-测熵与实现.md`
|
||||
|
||||
---
|
||||
|
||||
## 0. 一句话
|
||||
|
||||
**分两件事治**:**C** 把块 id 的收敛哈希 `HA` 从"裸 `sha256(字节)`"改成"**每个 network 一把密钥**的 keyed hash"(切断跨 network 的相关性推断,代价≈0);**D** 让**低熵块**不再确定性(首选 **D-1「稀释域」** —— 低熵小字段与高熵内容拼在一起再加密;备选 **D-2** 每块随机密钥 + 密钥封装)。**方案能否定量,取决于一个从未测过的读数** ⇒ 因此**第一个动作不是改代码,是测熵**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状取证(源码事实 · 本轮新读)
|
||||
|
||||
| # | 事实 | 出处(文件:行) | 含义 |
|
||||
|---|---|---|---|
|
||||
| 1 | 块 id = **`sha256(字节)` 前 32 hex**(**只由字节决定**,⛔ 无密钥) | `src/net/relay/content/chunker.ts:108-110`(`blockIdOf`),`BLOCK_ID_HEX_LEN = 32`(同文件 `:64`) | 这就是 **C** 要改的那一行;也是"相同明文 ⇒ 相同块 id ⇒ 跨节点共享"的**唯一**来源 |
|
||||
| 2 | 整份内容的 id 同样 = `sha256(全部落库字节)` | 同文件 `:113-115`(`contentIdOf`) | **C 必须同时改它**,⛔ 只改 `blockIdOf` 会留下"内容指纹仍裸哈希"的缺口 |
|
||||
| 3 | 块密钥**含明文成分**:`iv = HMAC(key,"iv"‖plain)` 前 12 B ⇒ `k = HMAC(key,"k"‖iv)` | `src/net/relay/content/crypto.ts:405-413`(`ivOf` / `keyOf`) | ⇒ 🔑 **持组密钥 ≠ 能解密**(本题的前提)。⇒ 低熵块之所以可枚举,是"**猜明文 + 复算 iv/k + 比对**",⛔ 不是"直接解密" |
|
||||
| 4 | 组密钥文件缺省 `/etc/dshs/content-group-key.json` | 同文件 `:47`(`DEFAULT_GROUP_KEY_FILE`) | **C 的 per-network 密钥**应从这条既有链路派生,⛔ 不新引入密钥来源 |
|
||||
| 5 | 🔴 **切分是「定长 1 MiB」**(`offset += blockSize` 死循环) | `chunker.ts:137-158`;`DEFAULT_BLOCK_SIZE = 1024*1024`(`:60`,⚠️ **常量而非配置**) | ⇒ 一份首屏包 10.8 MB ≈ **11 块**,每块是 **1 MiB 混合内容**。⇒ 🔑 **首屏包里的"纯低熵块"在结构上几乎不可能存在**(除非整整 1 MiB 都是低熵) |
|
||||
| 6 | 块存储 = **纯内存**,调用方**没给** `dir` | `src/web/server.ts:909-911`(`new ContentStore({ maxBytes })`,**无 dir**);`runtime.ts:347-350` 同 | ⇒ 块**不落盘** ⇒ **"低熵块"无法从生产盘上"捞"出来** ⇒ 测熵**只能对"真实内容"离线做** |
|
||||
| 7 | 快照上限 = `CONTENT_STORE_MAX_BYTES` 64 MiB | `参数表_覆盖网络_20260917.md:147` | 内存预算独立(⛔ 不挤 relay 的 `MEM_PER_HOST_MB`)⇒ D 若引入"合并加密",**是否增加常驻字节**要在这笔账里算 |
|
||||
|
||||
### 1.1 🔴 本轮最重要的结构发现(⛔ 尚无任何文档记录 · ⚠️ 待实测确认)
|
||||
|
||||
由事实 5 直接推出两条,**方向相反**,共同决定 D 的边界:
|
||||
|
||||
1. **对"首屏包"而言,D-1 几乎是免费的** —— 定长 1 MiB 意味着低熵小字段**已经被同块的高熵内容稀释**了。⇒ D-1 在首屏包上**代价≈0、收益≈0**(本来就不暴露)。
|
||||
2. 🔴 **D 的真实战场不在首屏包,而在「小于 1 MiB 的独立内容」** —— 任何**整体小于 1 MiB 的一份内容**只会切成 **1 个块**;若该内容本身低熵(配置 / 状态 / 清单 / 密钥交换载荷类),它就是一块**纯低熵块** ⇒ 中继可数相等性、持钥者可枚举。
|
||||
|
||||
⇒ **推论(决定待测项的形状)**:测熵**必须同时覆盖两个集合**,只测首屏包会得出"没有低熵块"的**假绿**结论。
|
||||
⚠️ 本条为**源码推导**,⛔ 不是实测 ⇒ 待测项 `M1` 的第一条就是要**证实或推翻它**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 第一个动作:补测「低熵块的种类数 / 体积 / 占首屏包比例」(**M1**)
|
||||
|
||||
### 2.1 为什么必须放第一个(⛔ 不是"顺手做的取证")
|
||||
|
||||
- **D 的边界由它决定** —— "什么算低熵、稀释单元取多大、要不要合并"全是它的函数(`调研…§6` 阻碍 4 已点明「**什么算低熵本身无可靠判据**」)。
|
||||
- **它从未被测过,而且历史上所有"低熵"读数都是合成字节** —— 三条硬证据:
|
||||
|
||||
| # | 证据 | 出处 | 说明 |
|
||||
|---|---|---|---|
|
||||
| a | perf 用的是 `makeBytes(PACK_BYTES, SEED)` | `_tmp_seq32/p01-perf.mjs:42` | **合成** 11,363,655 B 缓冲,⛔ 不是真首屏包 —— 对 perf(尺寸驱动)无害,对**熵统计致命** |
|
||||
| b | 低熵定性读数用 `blk-0000…` 8 B 合成块、域 1000 | `_tmp_seq32/p03-lowentropy.txt`;引用见 `参数表_覆盖网络_20260917.md:617-622` | 结论是**定性**的("可枚举"),⛔ 从未给出"真实内容里低熵占多少" |
|
||||
| c | 块存储纯内存、不落盘 | §1 事实 6 | ⇒ 生产侧**没有任何**可回捞的低熵块样本 |
|
||||
|
||||
### 2.2 待测项定义(可判定 · 落 `参数表` §11.3 补记区)
|
||||
|
||||
**度量对象(两个集合,⛔ 都用真实内容)**
|
||||
|
||||
- **S1 = 真实首屏包**:门户 `GET <portal>/plugins/`(= 既有口径的 `11,363,655 B`)。⚠️ **只读 GET**,⛔ 不重启、⛔ 不写盘。
|
||||
- **S2 = 真实"独立小内容"集**:覆盖网络**实际分发**的、**非首屏**的内容(⛔ 由执行棒先枚举来源并留痕;若确实不存在 ⇒ **报 SKIP + 说清"不存在"**,⛔ 不许用合成字节填充)。
|
||||
|
||||
**方法(本机离线 · 纯函数 · 零生产触碰)**
|
||||
|
||||
用**代码常量** `DEFAULT_BLOCK_SIZE`(= 1 MiB)复刻 `chunkify` 的**定长切分**,再对每块统计。⚠️ **必须调用仓库里那份 `chunkify`**(⛔ 不复刻一份算法 —— 复刻=双源,本线明令禁止)。
|
||||
|
||||
**指标(每条都可机器断言)**
|
||||
|
||||
| 编号 | 指标 | 定义 | 期望落点 |
|
||||
|---|---|---|---|
|
||||
| **M1-a** | **种类数 / 重复率** | 唯一块 id 数 ÷ 总块数;并给出**完全重复块**(同 id 出现 ≥ 2 次)的清单 | 数字 |
|
||||
| **M1-b** | **低熵块数 / 体积** | 逐块经验 Shannon 熵(字节分布,bit/byte),阈值初值 **H ≤ 4.0**(⚠️ **阈值本身是待定项**,须在报告里给出 H 的**直方图**,⛔ 不给单一阈值当结论) | 块数 + 字节数 + 直方图 |
|
||||
| **M1-c** | **占首屏包比例** | 低熵块字节 ÷ 首屏包字节 | 百分数(**这就是"能不能定量"的那个数**) |
|
||||
| **M1-d** | 🔑 **反向腿 · 子窗口熵** | 整块高熵 ≠ 块内无低熵字段:按**滑窗**(如 4 KiB)扫整份内容,给出"低熵窗口"的**尺寸分布** | 尺寸分布(**决定 D-1 的稀释单元粒度**) |
|
||||
|
||||
🔴 **M1-d 是防假绿的关键腿**:只做整块统计,会因为 §1.1 事实 5 得出"全是高熵块"的**假绿** —— 而那**恰恰漏掉**了低熵字段真实存在的形态(它们是块内的一小段)。本条同时**同时交叉验证** §1.1 的两条推论。
|
||||
|
||||
**落点与边界**
|
||||
|
||||
- 先可落 `_tmp_seq*/` 一次性脚本取证,**证据齐后固化为只读探针** `scripts/overlay-entropy.cjs`(⛔ **零第三方依赖**,只 `node:crypto` + `node:fs`)。
|
||||
- 读数进 `参数表_覆盖网络_20260917.md` **§11.3 补记区**(⚠️ 属**验收读数**,⛔ 不进 §10 指纹口径 —— 沿用序㉛ 的既定做法,见该表 `:506`)。
|
||||
- ⛔ **不动 §10 指纹**(除非同时新增判据键,那时按 §3.3 一并处理)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 方案 C —— 域分离(per-network keyed hash)
|
||||
|
||||
### 3.1 改什么
|
||||
|
||||
把**内容寻址的两个哈希**从"裸哈希"改成"**带 per-network 密钥的 keyed hash**":
|
||||
|
||||
| 现状 | 目标 | 落点 |
|
||||
|---|---|---|
|
||||
| `blockIdOf(bytes) = sha256(bytes)[0:32]` | `blockIdOf(bytes, netKey) = HMAC-SHA256(netKey, bytes)[0:32]` | `chunker.ts:108-110` |
|
||||
| `contentIdOf(bytes) = sha256(bytes)[0:32]` | `contentIdOf(bytes, netKey) = HMAC-SHA256(netKey, bytes)[0:32]` | `chunker.ts:113-115` |
|
||||
|
||||
🔑 **密钥来源**:复用既有组密钥链路从 **network 维度**派生(`crypto.ts:47` 的组密钥文件 + 既有的 network 标识),⛔ **不新增密钥文件、不新增 env**。⚠️ per-network 密钥的**派生写法**(`HMAC(groupKey, "overlay-block-id"‖network)` 之类)属**技术实现** ⇒ 由执行棒自决,⛔ 不上抛。
|
||||
|
||||
### 3.2 治什么 / 不治什么(口径必须写死)
|
||||
|
||||
- ✅ **治**:跨 network 的 **COF / LRI / 离线枚举**;中继从"同一块 id 同时出现在 A / B 两个 network"推出的**跨租户相关性**。
|
||||
- 🔴 **不治**:**同一 network 内部**持钥者枚举(`85,878 条/秒` 原样存在)⇒ **C 是低成本加分项,⛔ 不是 §10-3 的解**。
|
||||
- ✅ **代价≈0 的依据**:**跨 network 本来就不该去重**(不同租户 / 不同覆盖网络)⇒ 去重域的收窄**踩在不值得保留的地方**。
|
||||
|
||||
### 3.3 🔴 C 的真实代价:**块 id 口径换代(这是本方案最重的一笔)**
|
||||
|
||||
- 块 id 一变 ⇒ **全部既有块 id 失效**(缓存全放弃、去重率归零重算)⇒ **直接触发组密钥单 §9-5 的回头条件**。
|
||||
- ⇒ ⚠️ **C 不能"悄悄上"**,必须与 **`E1` 判据的基线重置**同时做(见 §5)。
|
||||
- ⚠️ **换接口形状也要改**:`blockIdOf` 从 1 参变 2 参 ⇒ **所有调用点**(`store.ts` 校验路径、`chunker.ts` 内部、装配层)都要一起改。⇒ **执行前必须先出"调用点清单"**(⛔ 漏一处 = 校验必红且极难定位)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 方案 D —— 低熵块非确定性
|
||||
|
||||
### 4.1 首选 **D-1「稀释域」合并加密**
|
||||
|
||||
**做法**:把低熵小字段与**高熵内容**拼在一起再加密 ⇒ 明文域从 `100` 扩到 `100 × 2³²` ⇒ 不可枚举。
|
||||
**代价**:这批块的**去重失效**(低熵块去重本无价值 ⇒ 代价踩在不值得保留的地方);**块 id 口径不变**(⚠️ 与 C 的方向**相反** —— C 换 id,D-1 只换"这批块加不加密、怎么加密")。
|
||||
|
||||
🔴 **D-1 的定义式设计约束(本方案必须写死,否则实现会做错)**:**稀释源必须是"攻击者猜不到"的那个量**。
|
||||
- 因为本题的攻击面是"**持组密钥者 · 猜明文 + 复算**"(§1 事实 3)⇒ 若稀释源是**攻击者能自己算出来的量**(如"由组密钥确定性派生的每块盐"、或块序号),**稀释对持钥者完全无效**(他能复算出同一份密文)。
|
||||
- ⇒ **稀释源必须包含真随机且不出现在明文可见面的字节**;⚠️ 该随机量的**存放位置**(密文内 / 密钥封装体里 / 块外层)是**技术实现**,执行棒自决 —— 但**判据必须能区分它是不是真随机**(见 §5 判据面)。
|
||||
|
||||
⚠️ **与 §1.1 的联动**:若 `M1` 证实"首屏包内不存在纯低熵块",**D-1 在首屏包上的作用面 ≈ 0** ⇒ D 的实际工作范围将收窄到 **S2(独立小内容)**,⛔ 那时**不许**为了"让 D 有用"而把块切小(切小 = 块数暴涨 = 控制面开销与回源字节都恶化 ⇒ **净变差,触 R11**)。
|
||||
|
||||
### 4.2 备选 **D-2 每块随机密钥 + 密钥封装**
|
||||
|
||||
**做法**:彻底非确定性 —— 每块一把随机密钥,密钥随内容封装(用组密钥包裹)。
|
||||
**代价**:该块**不去重**;**新增密钥封装配送**这一层(=新的出错面)。
|
||||
**定位**:**仅在 D-1 的稀释源无法满足 §4.1 约束时启用** —— 即"低熵内容确实是**独立成块**且**没有任何高熵内容可与它同批**"时。
|
||||
|
||||
---
|
||||
|
||||
## 5. 判据面 / 影响面 / 验收
|
||||
|
||||
### 5.1 判据面(⚠️ 这是"改完凭什么算改对")
|
||||
|
||||
| 项 | 要求 |
|
||||
|---|---|
|
||||
| **新增 OBS 行** | 现有 OBS 编号已到 **`OBS-28`**(`参数表` 第 264-270 行一带)⇒ 本单新增自 **`OBS-29`** 起,⛔ **不得跳号、不得复用** |
|
||||
| **新增阈值键** | 若 D 需断言"稀释源是真随机"⇒ 需新增键(形如 `CONTENT_ENTROPY_*` / `CONTENT_DILUTE_*`)。⚠️ **键名与取值由执行棒定**,⛔ 不上抛 |
|
||||
| 🔴 **必须有的负腿** | 「**稀释源被换成确定性派生量 ⇒ 判据必红**」—— ⛔ 否则"上了个无效的稀释"会**全绿**(本线老病根:装了但没生效 = 静默放行) |
|
||||
|
||||
> 🆕 **序㊻ 修订(2026-09-18 22:4x · 执行棒回填 · ⛔ 不改本方案的技术口径,只改判据面)**
|
||||
>
|
||||
> **① D 本轮不实现**(依据 = 序㊺ 实测:真实首屏包 23 块**0 个低熵块**、60 份真实独立小内容**0 份低熵**
|
||||
> ⇒ `D-1` **无对象可稀释**、`D-2` **前提不成立**)。⇒ 上表那条**负腿失去对象**(无稀释源可换)。
|
||||
> **② `OBS-29` 已重裁成 C 的判据**(编号仍在 **`OBS-29`**,⛔ 不跳号不复用):
|
||||
> **正腿** = 同字节 + **不同 network** ⇒ 块 id **不同**(**且两侧都 ≠ 裸哈希**);**正腿** = 同 network + 同字节 ⇒ 块 id **相同**(⛔ 只测前者会漏"去了重");
|
||||
> **装配面腿** = `store` 两处复算 + 重组位全过(⇒ 调用点无漏改);
|
||||
> 🔴 **负腿(真跑、具名)** = **去掉 per-network 维度**(域密钥取同值 / 取空)⇒ 谓词**必红**(`flat-key-collapses` / `empty-key-falls-back-to-bare-hash`)。
|
||||
> **③ `E1` 基线**按 §5 的要求**重取**(C 换了 id 口径 ⇒ 旧读数作废);⚠️ `E1` 的**定义不重估**。
|
||||
> **④ §3.3 的口径不变**:块 id 换代 ⇒ 缓存全清、去重率归零重算 —— ⛔ 回滚也**不是无损**。
|
||||
| **`E1` 基线重置** | C 换 id 口径 ⇒ `CONTENT_TIER_HITS_MIN` 等**命中类阈值的前提变了**;⚠️ `E1` 本身(回源字节 ≈ 1 份 × 组数)**不重估**,但**基线读数必须重取**(⛔ 不许拿旧读数当对照组) |
|
||||
| **`OBS-17` 口径一致腿** | 该行断言 `content.blockSize == CONTENT_BLOCK_SIZE` 且 `content.storeMaxBytes == CONTENT_STORE_MAX_BYTES`(`参数表:418`)⇒ 若 D 引入额外常驻字节,**需在此处补列,⛔ 不许绕过** |
|
||||
|
||||
### 5.2 影响面(**执行前必须先出清单**)
|
||||
|
||||
| 面 | 改动量级 | 是否触"超过 10 文件先出清单" |
|
||||
|---|---|---|
|
||||
| `src/net/relay/content/chunker.ts` | 小(两个函数 + 调用点) | 否 |
|
||||
| `chunker.ts` / `store.ts` / 装配层的 **`blockIdOf` 调用点** | **待清点**(⚠️ 数量未知 ⇒ 清点后若 > 10 ⇒ **先出清单再动**) | ⚠️ **可能触** |
|
||||
| `src/net/relay/content/crypto.ts` | 中(D-1/D-2 的加密路径) | 否 |
|
||||
| `scripts/overlay-probe.cjs` | 小(新增 OBS) | 否 |
|
||||
| `参数表_覆盖网络_20260917.md` | 小(§11.3 + §6 判据行)|⚠️ **§10 指纹会变**(新增键才变) | 否 |
|
||||
| 生产 env | 🔴 **预期为 0 个新增** | — |
|
||||
|
||||
### 5.3 验收(三段式,沿用本线既有做法)
|
||||
|
||||
1. **离线**:`M1` 读数落盘 + 熵直方图 + 探针自检(夹具模式封闭 ⇒ **⛔ 不 ssh**)。
|
||||
2. **本机**:`npm test`(Node 22)全绿 + `OBS-29` 正腿绿、**负腿红**。
|
||||
3. **真机**:`/status` 的 `content` 块 —— ⚠️ **只读**,⛔ 不重启在线服务;⚠️ `http2 on` 环境 curl 必须 `--http1.1`。
|
||||
|
||||
### 5.4 回滚
|
||||
|
||||
- **代码**:C / D 必须是**可独立回退**的两处开关(C 的密钥可"传空 ⇒ 回落裸哈希";D 可"不启用稀释 ⇒ 回落确定性")⇒ ⛔ 不许做成"上了就下不来"。
|
||||
- **口径**:C 回退后**块 id 会再变一次**(⇒ 缓存再清一次)—— ⚠️ 这条要写进回滚说明,⛔ 不许默认"回滚 = 无损"。
|
||||
- **判据**:`参数表` 回退到本文件所记的指纹 `d408d640246a980f702fe7b0a2895219`(⚠️ 以现算为准)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 边界(本方案**不做**的事)
|
||||
|
||||
- ⛔ **不立项 B**(OPRF / SA-MLE)—— 用户已拍板降级为可选加强;其接口位(Manager 做 KS)保留为**将来可插**,⛔ 本轮不写任何代码。
|
||||
- ⛔ **不做"按熵分层加盐"那条老路**(它必须同时动块 id 与去重率 ⇒ 比 C+D 更重,`调研…§5.5` 已量化)。
|
||||
- ⛔ **不改 `DEFAULT_BLOCK_SIZE`**(切小 = 净变差 ⇒ 触 R11)。
|
||||
- ⛔ **不引第三方依赖**、⛔ **不动 `package.json`**、⛔ **不改生产实例 / ⛔ 不重启在线服务**(本轮规划棒另加:⛔ 零代码、⛔ 零服务器触碰)。
|
||||
- ⛔ 合规 / 数据主权**不进本方案**(口径:方案只做技术实现)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 参考
|
||||
|
||||
- `调研_MLE加密去重最优方案_20260918.md` —— §3 方案族全景表 / §5.3 域分离 / §5.4 三选项 / §5.5 复杂度与收益 / §6 六条阻碍
|
||||
- `.workbuddy/memory/2026-09-18.md` —— 21:1x(三不兼得)/21:5x(文献印证 + 域分离发现)/21:2x(C/D/B 对比与"持钥者 ≠ 能解密")
|
||||
- `参数表_覆盖网络_20260917.md` —— `:146`(`CONTENT_BLOCK_SIZE`)/`:147`(`CONTENT_STORE_MAX_BYTES`)/`:418`(`OBS-17`)/`:506`(§11.3 补记区用法)/`:617-622`(§10-3 低熵现状定性)
|
||||
- 源码 —— `chunker.ts:60/64/108-115/137-158`|`crypto.ts:47/405-413`|`store.ts:1-40`(模块定位与"不做加解密"边界)|`server.ts:909-911`|`runtime.ts:347-350`
|
||||
Reference in new issue
Block a user