chick
|
20070b90f7
|
fix: enroll 写出守护进程会拒绝的配置,install 写出与自身路径不符的单元
四处"校验在错误的层"的缺陷,都属于同一类:每个组件单独看都对,接缝错了。
1. `msa-agent enroll --relay http://relay.example` / `relay.example` /
`https://relay.example/v1` 都会写出配置文件,install 报成功,守护进程在**下一次启动**才
拒绝,日志里只有一行 "msa-agent exiting"。规则原本散在三处(config 只查非空、transport
查真实规则、enroll 的注册客户端另有一份),现在收进 `internal/relayurl` 一份,三处都调它。
`config.File.Validate` 因此也补上了 https/host/无路径/无 query 的检查。
2. `enroll` 不再自己手写字段检查,改为构造真正的 `config.File` 并调用真正的 `Validate`——
一份"什么是可用配置"的定义,结构上无法再漂移。`config.Marshal` 让 enroll 与 Save 用同一个
编码器。
3. `install --config /srv/msa/config.json` 会给该文件改属主、交给服务用户,然后写一个
ExecStart 指向 `/etc/msa-agent/config.json` 的单元——服务起来找不到配置就退出,而它需要的
文件就在别处、属主正确、从未被读。`enroll --audit /srv/audit/audit.log` 同理:单元的
`ReadWritePaths` 只覆盖 `/var/log/msa-agent`,在 `ProtectSystem=strict` 下 agent 的
`MkdirAll` 报 EROFS,进程启动即退出,报错既不提那个 flag 也不提那条指令。现在单元是模板,
`RenderedUnit` 按实际布局渲染,`--print-unit` 打印的就是会被写入的那一份;默认布局渲染结果
与仓库里被 review 过的文件逐字节相同,所以 vendored 副本的漂移检查仍然有意义。
测试:新增 `internal/relayurl`,`config`/`enroll`/`install` 各补回归测试。每条都做了变异验证
(把修复回退,对应测试必须失败)。
|
2026-09-18 12:38:56 +08:00 |
|
chick
|
9949cfe5b0
|
fix(install): 刚装完的服务会失败,单元要说清楚而不是每 5 秒重启一次
我在真内核上把安装序列完整走了一遍(容器里补了 useradd/systemctl 的替身,其余全是真的),
暴露出一个只有真跑才看得见的缺陷:
装完 → `install` 启动服务 → 但那时**还没有配对的手机**,而守护进程**故意**拒绝空的设备列表
(一个什么都不能授权的 agent 不该坐在那里显得健康)。于是 `Restart=always` + `RestartSec=5`
变成每 5 秒重启一次,永远,journal 里刷同一行。实测:exit 1,"devices is empty, so no phone
could ever be authorised"。
配对发生在安装**之后**(运维在节点上跑 `--pair`,然后在手机上配对),所以这不是异常状态,
而是流程的必经一步。修法两条:
- 单元加 `StartLimitIntervalSec=300` / `StartLimitBurst=10`:十次之后 systemd 放弃,单元进入
`failed`——一个运维看得见、能处理的状态,而不是无限重启。
- `install` 的结束语改成说清这一步:服务会退出并停住,下一步是 `--pair`,用
`systemctl status` 看状态。此前它说"installed and running",而那是假的。
同一轮演练确认了此前所有修复在真内核上成立:服务用户建了、配置与日志目录交给它了、配置对
该用户可读、单元来自二进制且内容一致、卸载按序拆干净(用户/目录/单元全无残留)。
Go 全包通过(新增一项守着"单元必须有启动上限,且 install 必须说出下一步")。
|
2026-09-18 01:09:15 +08:00 |
|
chick
|
3473816167
|
feat: 独立成仓库,自己拥有契约文件
agent 从 simadmin-relay 拆出来,作为公开仓库:安装命令要由节点执行,而节点没有凭据,所以
二进制必须放在匿名可读的地方,而 relay 源码与 IPA 继续私有。
拆出来之后有一处必须改,不是搬文件:`contracts/agent-policy.json` 原先由 relay 仓库拥有、
agent 侧是 vendored 副本(Makefile 从 `../contracts/` 拷、policy_test.go 断言两份相等)。
现在反过来——**agent 拥有策略**,因为 agent 是执行它的那一方,让它去拷贝一份住在别处的文件
本来就是别扭的。relay 保留 vendored 副本(它的限额从同一组数字推导),而两侧的一致性不再
靠构建期比较(跨仓库的构建期比较谁也做不了),改由运行时保证:agent 在握手时上报策略摘要,
relay 拿自己那份比对,在 `/v1/status` 里逐节点给出 `policyMatches`。这比原来的测试更强,
因为它比的是**实际部署的那两份**,而不是两个 checkout 里的文件。
`contracts/test-vectors.json` 同理:Go 实现生成它(`make vectors`),relay 的 Node 参考实现
与 app 的 Swift 测试从 vendored 副本重算。实测重新生成的结果与 relay 那份逐字节相同。
systemd 单元的 `Documentation=` 指向本仓库。`VERSION` 提到 1.0.0。
20 个提交的历史完整保留(filter-branch 按子目录抽取)。
|
2026-09-17 23:24:57 +08:00 |
|
chick
|
7d174a0261
|
fix(agent): 卸载时指出该去哪删节点,而不是一律说 agents.json
agent 不持有运维令牌——节点不是运维,一个能删掉邻居的被控节点比它解决的问题更糟——所以
它不能自己从中继上摘掉自己。此前卸载的结束语对所有节点都写"the relay's agents.json still
lists this node",而自注册的节点根本不在那里:它在中继的状态文件里,要从手机上删。运维照着
那句话去翻一个没有这条记录的文件。
现在节点在登记时把来历写进配置(`selfEnrolled`,带 `--code` 为真),卸载结束语据此给出
正确的那一句:
- 自注册的 → "在中继的登记状态里,去 App 的「中继节点」移除"
- 手工登记的 → "在 agents.json 里,删掉那条并重启中继"
- 配置读不出来 → 两句都提,因为读不到不是让卸载失败的理由
测试两项:卸载打印的是它被告知的那一句(且不再对自注册节点说 agents.json)、登记时正确
记录来历(带码为真、不带码不写这个字段)。
Go 全包、Node 141 项、Swift 121 项通过。
|
2026-09-17 19:12:32 +08:00 |
|
chick
|
934760f8e2
|
fix(agent): 补上安装的服务那一半——建用户、改所有权、装单元
上一轮那段"一键安装"是坏的,而且只在节点上坏。实测三段里漏了三件:
1. systemd 单元写着 `User=msa-agent`,而命令从没创建这个用户;
2. `enroll` 是 sudo 跑的,配置是 root 所有 0600,服务以另一个用户运行——读不到自己的私钥;
3. 单元从 `msa-agent.service` 这个仓库里的路径拷,而节点上没有仓库。
三段都"看起来对",因为它们各自是一行合理的 shell;错的是它们与单元之间的接缝。这正
是这个项目里反复出现的那一类缺陷。
现在有 `msa-agent install`:
- 建 `--system --no-create-home --shell /usr/sbin/nologin` 的用户(已存在则跳过);
- 建日志目录,并把配置、配置目录、日志目录 chown 给该用户;
- 把 systemd 单元写进去。单元**嵌在二进制里**(`go:embed`),不是从仓库路径拷的,也不是
从 URL 下的——一个能发 unit 的服务器就能给节点塞一个后门 unit,那与"中继不可信"是同一
条理由。`--print-unit` 可以在装之前把它打出来看;
- `daemon-reload` 并 enable/start。
幂等:每步先看已经是什么状态,所以升级或上次失败后重跑都安全。`--dry-run` 打印计划且什么
都不改,并且在缺二进制或配置时**不拒绝**——"还没装"正是会去跑 dry-run 的状态,它只把缺的
东西指出来。chown 走 Runner 而不是 os.Chown,这样整条序列都能在无 root 的情况下测。
install 与 uninstall 共用同一份 `Paths`:装了什么没卸掉会在机器上留一份凭据,卸了什么没装
过会在新节点上失败。单元也一样,`TestVendoredUnitMatchesSystemd` 守着嵌入副本与
`systemd/msa-agent.service` 不漂移。
手机上的安装命令相应简化:`install` 那一步取代了原先手工的 useradd/install/systemctl 三行,
卸载那段也先给 `--dry-run`。
测试新增五项:单元不漂移、install 满足单元的全部要求(用户、所有权、单元来自二进制而非仓库
路径)、install dry run 什么都不改、缺二进制或配置时拒绝且一条命令都不跑、dry run 在全新
节点上不拒绝而是指出缺什么。变异验证:去掉 chown、去掉建用户、让 dry run 执行命令——三处都
让对应测试失败。
Go 全包、Node 134 项、Swift 116 项通过。
|
2026-09-17 17:55:57 +08:00 |
|
chick
|
62b322af39
|
feat(agent,relay): 节点自注册与一键卸载
节点侧现在有两条命令覆盖整个生命周期,中继侧有了一条让节点不必被人工登记就能加入
的通道。
agent 新增两个子命令:
- `msa-agent enroll --relay <url> [--code <code>]` 铸造密钥与标识并写配置。它和
`msa-keygen` 共用 internal/enroll,所以两者不会对"配置长什么样"产生分歧——区别只在
运行位置:keygen 需要 Go 工具链,而节点上只有交叉编译好的二进制。带 --code 时它自己
向中继登记,顺序是先登记再落盘:为"中继还不认识的节点"写下的配置会永远重试 401,而
运维无法把这种情况和网络故障区分开。
- `msa-agent uninstall [--dry-run] [--keep-logs]` 按唯一一个不会留下悬挂状态的顺序拆除:
先清设备并告诉中继,再停服务、删单元、删配置目录、删日志、删用户。顺序是重点——先删
节点会让中继继续向一台已经不存在的节点投递,而"一键卸载"此前根本不存在,尽管安装会
往宿主机放六样东西,其中配置里还有节点密码。
写这个时踩到一个值得记的坑:第一版把每个动作都写成"先 log 再执行",于是 --dry-run
会真的停掉服务——一个不是 dry run 的 dry run。现在每个副作用都 gate 在 step() 的
返回值上,并有测试断言 dry run 一条命令都不跑。
中继新增自注册,默认关闭:
- `POST /v1/enroll/code`(运维令牌)铸造一次性码,15 分钟、最多 10 个未使用。
- `POST /v1/enroll` 节点用码登记自己。码先兑换再校验登记体,所以用垃圾请求探测码是否
有效的尝试会烧掉它。
- 登记只授权"加入"这一件事。中继仍然读不到内容、伪造不了应答、下发不了命令——最坏情况
是让一个不该出现的节点出现,也就是拒绝服务,正是设计已经接受的边界。
- agents.json 的条目优先于自注册状态,所以运维永远能用一个 diff 覆盖或吊销自注册的节点;
反过来,节点不能覆盖运维条目(被拒绝,400)。
- 自注册状态要落盘,这是中继第一次有它必须记住的东西。中继此前无状态是它的一条性质,
这里明确让掉了:忘记已登记的节点会让它们全部 401,而症状看起来像 token 坏了。状态文件
原子写、损坏时拒绝启动(而不是静默变成空机群),systemd 单元用 StateDirectory 给它一块
可写目录,其余文件系统仍是只读。
测试:internal/enroll 五项(两份产物必须描述同一个节点、密码只在给出时写入、给定标识
不被重新生成、两次铸造不同、缺输入被拒),internal/install 七项(删干净、顺序、中继不可
达仍完成、dry run 不动任何东西、保留日志、缺件不中止、计划列出每条路径),
test/enrollment.test.js 十四项(流程、码单次、垃圾请求烧码、默认关闭、只有运维能铸造、
不能覆盖运维条目、可重复登记、重启后仍在、运维优先、损坏状态拒绝启动、码过期与清扫、
铸造循环不涨内存、登记体校验、状态文件不含私钥)。变异验证:去掉码的单次性、去掉运维条目
保护、让 dry run 执行命令、先删配置再通知中继——四处都让对应测试失败。
Go 全包、Node 134 项、Swift 107 项通过。
|
2026-09-17 13:28:32 +08:00 |
|