From e37a7371be397a18dfa1f1617b29f150d81691e8 Mon Sep 17 00:00:00 2001 From: Rogee Date: Wed, 30 Sep 2026 07:26:26 +0800 Subject: [PATCH] Validate explicit Mock Dispatcher runtime settings --- .../saas-dispatcher-implementation.md | 1 + internal/config/dispatcher_runtime.go | 65 +++++++++++++++ internal/config/dispatcher_runtime_test.go | 79 +++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 internal/config/dispatcher_runtime.go create mode 100644 internal/config/dispatcher_runtime_test.go diff --git a/docs/evidence/saas-dispatcher-implementation.md b/docs/evidence/saas-dispatcher-implementation.md index 63d144d..be8b946 100644 --- a/docs/evidence/saas-dispatcher-implementation.md +++ b/docs/evidence/saas-dispatcher-implementation.md @@ -33,6 +33,7 @@ - `PATH=/tmp/sip-go-agent-tools/bin:$PATH sh scripts/check-proto.sh` 通过(lint/build/generate/新包测试/7 文件清单 hash);`git diff --check`、`go test ./internal/rpc ./internal/agent ./internal/dispatcher ./cmd/sip-go-agent -count=1` 通过。其他旧实现/契约入口、文件名碰撞和迁移数据安全仍待 P02/P07,**不能据此称全仓已无代次或整体完成**。 - 新增无实现代次字段的 Dispatcher 环境预检:必须显式给出规范 UUID v4 身份、只读 HTTP 地址及密钥、RabbitMQ 地址和 SQLite 路径;当前 Mock 仅接受本机 HTTP/MQ 目标,mixed/real 在任何资源操作前拒绝。预检不打开数据库或网络,缺失值不继承旧默认配置,错误不打印凭据。`go test ./internal/config -run '^TestLoadDispatcherEnvironment' -count=1` 通过;此预检尚未接入主 CLI,不能当作 P02/P03 完成。 +- Dispatcher 另有纯运行环境预检:唯一 Agent 端点清单和 OSS 配置文件须提供路径,本机 gRPC 监听及双向 TLS 文件/Agent 证书指纹须显式给出;缺失、非法指纹或非本机监听明确拒绝且不回显值。该检查不读取配置文件、不连接 Agent、不打开 SQLite/MQ;`go test ./internal/config -run '^TestLoadDispatcherRuntimeEnvironment' -count=1` 通过。当前仍未由 `dispatcher` 主命令调用,不能当作 Agent 加载或 OSS 签发已验收。 - Agent 增加独立的无代次环境预检:Agent/Cell 与预授权 D 的身份、会话与私有恢复路径、隔离 Mock 场景和已加载 SIP 测试事实、D gRPC 目标及 mTLS 文件/指纹均须显式提供;仅允许本机监听和本机 D 端点,mixed/real 在访问文件或网络前拒绝。缺失项不继承旧 `FromEnv` 默认值,凭据/地址不回显;`go test ./internal/config -run '^TestLoadAgentEnvironment' -count=1` 通过。此检查已接入根命令可达的唯一 Agent Mock 入口;本机双向 TLS 监听可由受信 D 证书探测,另一张同 CA 证书被指纹门禁拒绝。本地测试用批准的 D 身份激活会话,错误 D UUID 即使携带受信证书也在写入会话日志前拒绝;尚未执行呼叫、验证 Agent→D 录音或真实 Asterisk 加载。 diff --git a/internal/config/dispatcher_runtime.go b/internal/config/dispatcher_runtime.go new file mode 100644 index 0000000..b20d905 --- /dev/null +++ b/internal/config/dispatcher_runtime.go @@ -0,0 +1,65 @@ +package config + +import ( + "errors" + "fmt" + "os" + "strings" +) + +// DispatcherRuntimeEnvironment contains deployment-owned runtime settings. A +// pure preflight must finish before any SQLite, MQ, HTTP or gRPC resource opens. +type DispatcherRuntimeEnvironment struct { + DispatcherEnvironment + Listen string + AgentEndpointsFile string + OSSConfigFile string + CAFile string + CertFile string + KeyFile string + PeerFingerprints map[string]struct{} +} + +func LoadDispatcherRuntimeEnvironment(mode string) (DispatcherRuntimeEnvironment, error) { + base, err := LoadDispatcherEnvironment(mode) + if err != nil { + return DispatcherRuntimeEnvironment{}, err + } + settings := DispatcherRuntimeEnvironment{DispatcherEnvironment: base} + get := func(name string) (string, error) { + value := os.Getenv(name) + if strings.TrimSpace(value) == "" { + return "", fmt.Errorf("%s is required", name) + } + return value, nil + } + for _, field := range []struct { + name string + value *string + }{ + {"DISPATCHER_GRPC_LISTEN", &settings.Listen}, + {"DISPATCHER_AGENT_ENDPOINTS_FILE", &settings.AgentEndpointsFile}, + {"DISPATCHER_OSS_CONFIG_FILE", &settings.OSSConfigFile}, + {"MTLS_CA_FILE", &settings.CAFile}, + {"MTLS_CERT_FILE", &settings.CertFile}, + {"MTLS_KEY_FILE", &settings.KeyFile}, + } { + value, err := get(field.name) + if err != nil { + return DispatcherRuntimeEnvironment{}, err + } + *field.value = value + } + if !localGRPCAddress(settings.Listen, true) { + return DispatcherRuntimeEnvironment{}, errors.New("DISPATCHER_GRPC_LISTEN must be an isolated local Mock address") + } + fingerprints, err := get("MTLS_PEER_CERT_FINGERPRINTS") + if err != nil { + return DispatcherRuntimeEnvironment{}, err + } + settings.PeerFingerprints, err = ParseCertificateFingerprints(fingerprints) + if err != nil || len(settings.PeerFingerprints) == 0 { + return DispatcherRuntimeEnvironment{}, errors.New("MTLS_PEER_CERT_FINGERPRINTS is invalid") + } + return settings, nil +} diff --git a/internal/config/dispatcher_runtime_test.go b/internal/config/dispatcher_runtime_test.go new file mode 100644 index 0000000..97befea --- /dev/null +++ b/internal/config/dispatcher_runtime_test.go @@ -0,0 +1,79 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func setCurrentDispatcherRuntimeEnvironment(t *testing.T) string { + t.Helper() + database := setDispatcherEnvironment(t) + root := t.TempDir() + for name, value := range map[string]string{ + "DISPATCHER_GRPC_LISTEN": "127.0.0.1:0", + "DISPATCHER_AGENT_ENDPOINTS_FILE": filepath.Join(root, "agent-endpoints.json"), + "DISPATCHER_OSS_CONFIG_FILE": filepath.Join(root, "oss.json"), + "MTLS_CA_FILE": filepath.Join(root, "ca.pem"), + "MTLS_CERT_FILE": filepath.Join(root, "dispatcher.pem"), + "MTLS_KEY_FILE": filepath.Join(root, "dispatcher.key"), + "MTLS_PEER_CERT_FINGERPRINTS": strings.Repeat("a", 64), + } { + t.Setenv(name, value) + } + return database +} + +func TestLoadDispatcherRuntimeEnvironmentRequiresExplicitMockDeployment(t *testing.T) { + database := setCurrentDispatcherRuntimeEnvironment(t) + settings, err := LoadDispatcherRuntimeEnvironment("mock") + if err != nil { + t.Fatal(err) + } + if settings.DispatcherID != dispatcherFixtureID || settings.SQLitePath != database || settings.Listen != "127.0.0.1:0" || + settings.AgentEndpointsFile == "" || settings.OSSConfigFile == "" || len(settings.PeerFingerprints) != 1 { + t.Fatal("Dispatcher deployment values were lost or replaced by a default") + } + if _, err := os.Stat(database); !os.IsNotExist(err) { + t.Fatalf("preflight opened the business database: %v", err) + } + for _, name := range []string{ + "DISPATCHER_GRPC_LISTEN", "DISPATCHER_AGENT_ENDPOINTS_FILE", "DISPATCHER_OSS_CONFIG_FILE", + "MTLS_CA_FILE", "MTLS_CERT_FILE", "MTLS_KEY_FILE", "MTLS_PEER_CERT_FINGERPRINTS", + } { + t.Run(name, func(t *testing.T) { + setCurrentDispatcherRuntimeEnvironment(t) + t.Setenv(name, "") + if _, err := LoadDispatcherRuntimeEnvironment("mock"); err == nil || !strings.Contains(err.Error(), name) { + t.Fatalf("missing Dispatcher setting silently inherited an old default: %v", err) + } + }) + } +} + +func TestLoadDispatcherRuntimeEnvironmentRejectsExternalOrUnsupportedModeWithoutIO(t *testing.T) { + for _, mode := range []string{"real", "mixed", ""} { + t.Run(mode, func(t *testing.T) { + database := setCurrentDispatcherRuntimeEnvironment(t) + if _, err := LoadDispatcherRuntimeEnvironment(mode); err == nil || !strings.Contains(err.Error(), "Mock") { + t.Fatalf("unapproved mode was admitted: %v", err) + } + if _, err := os.Stat(database); !os.IsNotExist(err) { + t.Fatalf("mode rejection opened SQLite: %v", err) + } + }) + } + setCurrentDispatcherRuntimeEnvironment(t) + const external = "0.0.0.0:19443" + t.Setenv("DISPATCHER_GRPC_LISTEN", external) + if _, err := LoadDispatcherRuntimeEnvironment("mock"); err == nil || !strings.Contains(err.Error(), "DISPATCHER_GRPC_LISTEN") || strings.Contains(err.Error(), external) { + t.Fatalf("external Mock listener was admitted or echoed: %v", err) + } + setCurrentDispatcherRuntimeEnvironment(t) + const invalid = "not-a-fingerprint" + t.Setenv("MTLS_PEER_CERT_FINGERPRINTS", invalid) + if _, err := LoadDispatcherRuntimeEnvironment("mock"); err == nil || !strings.Contains(err.Error(), "MTLS_PEER_CERT_FINGERPRINTS") || strings.Contains(err.Error(), invalid) { + t.Fatalf("unapproved peer fingerprint was accepted or echoed: %v", err) + } +}