Files
ModelRouter/packaging/llmsproxy.service
JianFeeeee 8f73f77ec2 chore(main): 回流 —— VNC 兜底登录真正可用(放开 browser-stack 不需要的加固 + 修 3 个插件缺陷)
按纪律先在 release/v2.0.x 提交,再内容对齐回 main。
校验:git diff release/v2.0.x = 0 行。
2026-10-15 08:23:25 +08:00

86 lines
4.7 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=LLMSProxy - 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
ExecStart=/usr/local/bin/llmsproxy -config /etc/llmsproxy/config.yaml
WorkingDirectory=/etc/llmsproxy
Restart=always
RestartSec=5
# ---- 加固(2026-10-01 逐条实测后加入,不是照抄文档)----
#
# 为什么仍然以 root 运行:master.key 是 0600 root。加 User=llmsproxy 实测直接
# 起不来,而且失败方式很隐蔽——
# [config] secrets disabled: open /etc/llmsproxy/master.key: permission denied
# 只是**一行日志**,服务会带着"敏感值将以明文落盘"继续跑起来。
# 也就是说降权在当前文件权限下不是加固,而是把密钥降级。要降权必须先把
# master.key 交给服务用户并统一 /etc/llmsproxy 的属主,那是一次独立的、有回滚
# 需求的变更,不该和加固混在一起。
#
# 下面每一条都在一个独立探针单元(临时端口 + 独立 runtime_file/adapter_dir)
# 上真实验证过:鉴权 401/200 正常、发一次真实 /v1/chat/completions 走通(证明
# LuaJIT 适配器路径没被 seccomp 打断)、审计文件可写可轮转、连续重启 3 次与
# kill -9 后行为符合预期。systemd 对非法指令值不报错只"忽略",逐条实测是唯一
# 可靠做法。
NoNewPrivileges=yes
# 读路径全部落在 /etc/llmsproxy;写路径经核对只有 config.yaml / runtime.json /
# audit.jsonl / adapters/*.lua / master.key,全在该目录下(internal/{config,gateway,
# core,lua} 里的 WriteFile|Rename|Remove 调用点)。
ProtectSystem=strict
ReadWritePaths=/etc/llmsproxy
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/llmsproxy。
[Install]
WantedBy=multi-user.target