mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
生产实测暴露的内核缺陷:**重启后所有已批准出站主机全被拒**。
根因链:
1. `LoadKernelPermissions` 把 `.kernel-perms.json` 里的 `jp.Hosts` 装进
`ps.extraHosts`,但它明确注释「折叠由调用方负责」——**启动路径上没人负责**。
2. `SetOutboundHosts` 早已跑过(core.New 里,在 LoadDir **之前**),
于是 allowlist 只含 source 主机,插件声明的整套 host 全被拒。
3. 症状是经典的「重启丢掉运维的决定」:运维一打开插件页重新保存就恢复。
**它还静默打坏了定时任务**:`refresh_models` 在启动后 5s 补跑
(first_due_now),正好落在这个窗口里,于是每个平台的刷新都报
「host "zcode.z.ai" is not an allowed outbound destination」——看起来像
"白名单缺 host",实际是授权根本没被折叠。我按这个假象查了一圈(去核对
.kernel-perms.json 里 zcode.z.ai 明明在),才定位到折叠这一步缺失。
修法:LoadDir 末尾补 `rebuildOutboundAllowlistLocked()`。
判据:`TestPersistedHostGrantsSurviveRestart` —— 造一份持久化授权、模拟
启动顺序(SetOutboundHosts → LoadDir),断言已批准的 host 可用、
source 主机未丢、未批准的仍被拒。
**变异验证**:撤掉修复 → 判据变红(两条"被拒");加回 → 变绿。
回归:internal/{lua,core,provider,gateway} 全绿。
75 lines
2.5 KiB
Go
75 lines
2.5 KiB
Go
package lua
|
||
|
||
import (
|
||
"encoding/json"
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
)
|
||
|
||
// 持久化的出站主机授权必须在**重启后自动生效**。
|
||
//
|
||
// 实测缺陷(生产):LoadKernelPermissions 把已批准的 host 装进 ps.extraHosts,
|
||
// 但注释明说"折叠由调用方负责" —— 启动路径上没人负责。SetOutboundHosts 早已
|
||
// 跑过(core.New 里、LoadDir 之前),于是 allowlist 只有 source 主机,
|
||
// 插件声明的整套 host(zcode.z.ai / copilot.tencent.com …)在重启后全被拒:
|
||
//
|
||
// host "zcode.z.ai" is not an allowed outbound destination
|
||
//
|
||
// 症状是经典的"重启丢掉运维的决定"——运维一打开插件页重新保存就恢复。
|
||
// 它还打坏了定时任务:refresh_models 在启动后 5s 触发,正好落在那个窗口里,
|
||
// 于是每个平台的刷新都报"授权缺失"。
|
||
func TestPersistedHostGrantsSurviveRestart(t *testing.T) {
|
||
dir := t.TempDir()
|
||
plugDir := filepath.Join(dir, "plugins")
|
||
if err := os.MkdirAll(plugDir, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 造一份持久化授权(模拟运维此前在 WebUI 批准过)
|
||
perms := map[string]interface{}{
|
||
"channels": map[string]interface{}{
|
||
"actions": []string{"add_source"},
|
||
"hosts": []string{"zcode.z.ai", "copilot.tencent.com"},
|
||
},
|
||
}
|
||
b, _ := json.Marshal(perms)
|
||
if err := os.WriteFile(filepath.Join(plugDir, ".kernel-perms.json"), b, 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 模拟启动:先 SetOutboundHosts(源主机),再 LoadDir(读授权)
|
||
vm := NewVM(filepath.Join(dir, "adapters"))
|
||
if err := vm.Start(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(vm.Stop)
|
||
ps := NewPlugins(vm, plugDir)
|
||
ps.SetOutboundHosts([]string{"api.example.com"})
|
||
|
||
// LoadDir 会读授权文件;没有插件文件也没关系,关键是它必须折叠授权。
|
||
if err := ps.LoadDir(); err != nil {
|
||
t.Fatalf("LoadDir: %v", err)
|
||
}
|
||
|
||
al := ps.outboundAllowlist()
|
||
if al == nil {
|
||
t.Fatal("无 allowlist")
|
||
}
|
||
// ① 源主机仍可用
|
||
if !al.allows("api.example.com", "443") {
|
||
t.Error("source 主机被丢了")
|
||
}
|
||
// ② 已批准的 extra host 必须生效(这就是本判据的核心)
|
||
for _, h := range []string{"zcode.z.ai", "copilot.tencent.com"} {
|
||
if !al.allows(h, "443") {
|
||
t.Errorf("重启后已批准的 host %q 被拒 —— "+
|
||
"LoadKernelPermissions 装了 extraHosts 但没人折叠进 allowlist", h)
|
||
}
|
||
}
|
||
// ③ 没批准过的仍然不许
|
||
if al.allows("evil.example.com", "443") {
|
||
t.Error("未批准的 host 不该被放行")
|
||
}
|
||
}
|