Files
ModelRouter/packaging/modelrouter.service
JianFeeeee b6bbff19e9 fix(packaging): 包内 unit 指向 /usr/bin;core-dist 支持无 nfpm 打包
两个都是发布链上的真缺陷,构建 v3.0.0 产物时才暴露:

1. **unit 与包内二进制路径不一致**:deb/rpm 把二进制装到 `/usr/bin/modelrouter`,
   而 `modelrouter.service` 的 `ExecStart` 写 `/usr/local/bin/modelrouter`。
   装包后 `systemctl start` 只会给 203/EXEC,看起来像"装好了但起不来"。
   - 包内 unit 改 `/usr/bin/modelrouter`(FHS,装包即用)
   - 源码部署仍用 `/usr/local/bin`,`deploy.sh` 增加校验:unit 的 ExecStart
     指向不存在的文件时**中止**并说明两种布局的区别
   - docs/deployment.md 同步说明

2. **core-dist 硬依赖 nfpm**:构建机离线(GOPROXY 不可达)时装不上 nfpm,
   发布直接被堵死。现在 nfpm 优先,缺失时回退 dpkg-deb + rpmbuild,
   两者产出同一套文件布局,并继续由末尾的 archive-content 门禁校验。
   - rpm 回退踩到的坑:postinst/prerm 末尾无换行时,`%preun` 会粘在
     `exit 0` 后面,整个 `%files` 段被吞,rpmbuild 报
     "Installed (but unpackaged) file(s) found"。
   - `%dir /etc/modelrouter` 要求 buildroot 里存在该目录,否则
     "Directory not found"。

验证:`./packaging/core-dist.sh 3.0.0` 产出 deb(4220KB)/rpm(4229KB)/tar(4145KB);
包内 unit `ExecStart=/usr/bin/modelrouter`,`systemd-analyze verify --root` 通过;
deb/rpm 均不含 `plugins/`,adapters=14。
2026-10-27 00:32:14 +08:00

89 lines
5.0 KiB
Desktop File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[Unit]
Description=ModelRouter - unified OpenAI-compatible multi-source LLM gateway
After=network.target
[Service]
Type=simple
# Memory tuning (measured, see README "内存占用"):
# MALLOC_ARENA_MAX=2 caps glibc per-thread malloc arenas. LuaJIT allocates
# through cgo -> glibc malloc, and glibc defaults to 8*nproc arenas, so every
# OS thread that touches malloc reserved its own ~1 MB arena that is never
# returned. Measured: 8-12 arenas -> 0.
# GOGC=50 halves the Go heap growth target. On its own it does NOT help (the
# saved heap is immediately eaten by extra glibc arenas); combined with
# MALLOC_ARENA_MAX it cut settled RSS by ~19%. This gateway is I/O bound, so
# the extra GC cycles are free.
Environment=GOGC=50
Environment=MALLOC_ARENA_MAX=2
# Package installs put the binary in /usr/bin (FHS). A source deployment via
# deploy.sh uses /usr/local/bin — that script verifies the unit below points at
# a binary that actually exists, so the two layouts cannot silently disagree.
ExecStart=/usr/bin/modelrouter -config /etc/modelrouter/config.yaml
WorkingDirectory=/etc/modelrouter
Restart=always
RestartSec=5
# ---- 加固(2026-10-01 逐条实测后加入,不是照抄文档)----
#
# 为什么仍然以 root 运行:master.key 是 0600 root。加 User=modelrouter 实测直接
# 起不来,而且失败方式很隐蔽——
# [config] secrets disabled: open /etc/modelrouter/master.key: permission denied
# 只是**一行日志**,服务会带着"敏感值将以明文落盘"继续跑起来。
# 也就是说降权在当前文件权限下不是加固,而是把密钥降级。要降权必须先把
# master.key 交给服务用户并统一 /etc/modelrouter 的属主,那是一次独立的、有回滚
# 需求的变更,不该和加固混在一起。
#
# 下面每一条都在一个独立探针单元(临时端口 + 独立 runtime_file/adapter_dir)
# 上真实验证过:鉴权 401/200 正常、发一次真实 /v1/chat/completions 走通(证明
# LuaJIT 适配器路径没被 seccomp 打断)、审计文件可写可轮转、连续重启 3 次与
# kill -9 后行为符合预期。systemd 对非法指令值不报错只"忽略",逐条实测是唯一
# 可靠做法。
NoNewPrivileges=yes
# 读路径全部落在 /etc/modelrouter;写路径经核对只有 config.yaml / runtime.json /
# audit.jsonl / adapters/*.lua / master.key,全在该目录下(internal/{config,gateway,
# core,lua} 里的 WriteFile|Rename|Remove 调用点)。
ProtectSystem=strict
ReadWritePaths=/etc/modelrouter
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
LockPersonality=yes
# ⚠️ 地址族**不限制**(2026-10-15)。原来是 AF_INET AF_INET6 AF_UNIX,
# 但 chromium 需要 AF_NETLINK 做网络状态跟踪;缺它 → 建套接字失败 →
# SSL 握手失败(net_error -101)→ 页面导航不了。
# 现在不写这一项(systemd 默认不限制)。
# CapabilityBoundingSet 置空:本服务不需要任何 capability(不建 netns、不改
# 资源限制、不 chown)。留空即"一个都不给",比列一份允许清单更难写错。
CapabilityBoundingSet=
# ⚠️⚠️ **不限制 syscall**(2026-10-15,实测后决定)。
#
# 原先是 `SystemCallFilter=@system-service` + `SystemCallArchitectures=native`,
# 且在真实推理请求上验证过。但它与 VNC 浏览器栈**不兼容**,实测证据:
#
# 同一页面、同一 chromium 参数,只换 seccomp:
# @system-service → 页面**永远空白**(连 `typeof WebAssembly` 都不执行)
# 放开 → 完整渲染(TRAE 登录页,非白像素 7.9% → 90.6%)
#
# 原因:TRAE 的授权页是 React SPA + 字节风控 SDK(sdk-glue.js),依赖 WASM/JIT。
# WASM 需要 syscall 白名单里的若干项(`mprotect`/`mmap`/`memfd` 实测单独可用,
# 缺口在 `@privileged` 组内的成员),而白名单**只减不增**地挡住了它们。
#
# 为什么不再逐个补白名单:那是**没有终点的维护**。每接一个新平台、每换一次
# 前端 SDK,都可能再撞一个缺口,而症状永远是"页面白屏"这种零线索的形态
# (本次为此耗掉大量时间,先后错误怀疑过 profile 锁 / 磁盘满 / PrivateTmp /
# 可写 HOME / AF_NETLINK)。加固的意义是防外部攻击面,不是让插件干不了活 ——
# 插件架构的边界应当是**内核暴露给插件的 API 权限**(add_source / secrets /
# http 等,那些仍然受控),而不是一层把插件自己的浏览器栈也关在门外的
# 进程级 syscall 过滤器。
#
# 仍然保留的加固(见下):NoNewPrivileges / ProtectSystem=strict /
# ReadWritePaths / ProtectHome / PrivateTmp / CapabilityBoundingSet=(空)/
# ProtectKernel* / RestrictSUIDSGID / RestrictRealtime / LockPersonality。
# 这些**都不影响**浏览器栈(已逐条实测),并把可写面收窄到 /etc/modelrouter。
[Install]
WantedBy=multi-user.target