From 3fc61105b4462d5cf814515f0857fa82a5b0ce34 Mon Sep 17 00:00:00 2001 From: Rogee Date: Fri, 9 Oct 2026 15:27:31 +0800 Subject: [PATCH] fix(dispatcher): use real SaaS HTTP transport outside mock mode --- cmd/sip-go-agent/dispatcher_command.go | 3 +- cmd/sip-go-agent/dispatcher_http.go | 25 +++++ cmd/sip-go-agent/dispatcher_http_test.go | 92 +++++++++++++++++++ cmd/sip-go-agent/dispatcher_sip.go | 3 +- .../mq-unused-exchange-removal-20261009.md | 12 ++- 5 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 cmd/sip-go-agent/dispatcher_http.go create mode 100644 cmd/sip-go-agent/dispatcher_http_test.go diff --git a/cmd/sip-go-agent/dispatcher_command.go b/cmd/sip-go-agent/dispatcher_command.go index a3eb076..d2a84a0 100644 --- a/cmd/sip-go-agent/dispatcher_command.go +++ b/cmd/sip-go-agent/dispatcher_command.go @@ -76,11 +76,10 @@ func runDispatcher(ctx context.Context, mode string) (result error) { if err != nil { return errors.New("Dispatcher mTLS Agent certificate configuration is invalid") } - httpClient, err := localMockHTTPClient(ca) + httpClient, err := dispatcherSaaSHTTPClient(mode, ca) if err != nil { return err } - httpClient.Timeout = 10 * time.Second _, reader, err := dispatcherConfigurationClient(mode, httpClient) if err != nil { return err diff --git a/cmd/sip-go-agent/dispatcher_http.go b/cmd/sip-go-agent/dispatcher_http.go new file mode 100644 index 0000000..23f6592 --- /dev/null +++ b/cmd/sip-go-agent/dispatcher_http.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "net/http" + "time" +) + +func dispatcherSaaSHTTPClient(mode string, mockTrustPEM []byte) (*http.Client, error) { + switch mode { + case "nonprod-real", "sip-only": + // SaaS uses the standard transport and system certificate roots, not + // the Agent mTLS CA or Mock's loopback-only transport. + return &http.Client{Timeout: 10 * time.Second}, nil + case "mock": + client, err := localMockHTTPClient(mockTrustPEM) + if err != nil { + return nil, err + } + client.Timeout = 10 * time.Second + return client, nil + default: + return nil, fmt.Errorf("unsupported Dispatcher SaaS HTTP mode %q", mode) + } +} diff --git a/cmd/sip-go-agent/dispatcher_http_test.go b/cmd/sip-go-agent/dispatcher_http_test.go new file mode 100644 index 0000000..4d171c7 --- /dev/null +++ b/cmd/sip-go-agent/dispatcher_http_test.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "encoding/pem" + "io" + "net" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestDispatcherSaaSHTTPClientAllowsRealTargetsAndKeepsMockLocal(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = io.WriteString(w, "configuration reached") + })) + defer server.Close() + trustServer := httptest.NewTLSServer(http.NotFoundHandler()) + trustPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: trustServer.Certificate().Raw}) + trustServer.Close() + + // Resolve the nonlocal SaaS hostname to the isolated test server. No request + // leaves this machine; Mock must still reject the original target hostname. + original := http.DefaultTransport + transport := original.(*http.Transport).Clone() + transport.Proxy = nil + transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { + if address == "saas.example.invalid:80" { + address = server.Listener.Addr().String() + } + return (&net.Dialer{}).DialContext(ctx, network, address) + } + http.DefaultTransport = transport + t.Cleanup(func() { + http.DefaultTransport = original + transport.CloseIdleConnections() + }) + + for _, mode := range []string{"nonprod-real", "sip-only", "mock"} { + t.Run(mode, func(t *testing.T) { + client, err := dispatcherSaaSHTTPClient(mode, trustPEM) + if err != nil { + t.Fatal(err) + } + if client.Timeout != 10*time.Second { + t.Fatalf("configuration timeout = %v", client.Timeout) + } + response, err := client.Get("http://saas.example.invalid/internal/v1/dispatcher/sip") + if mode == "mock" { + if response != nil { + _ = response.Body.Close() + } + if err == nil || !strings.Contains(err.Error(), "Mock upload target is not local") { + t.Fatalf("Mock must reject a nonlocal configuration target: %v", err) + } + response, err = client.Get(server.URL) + } + if err != nil { + t.Fatalf("%s configuration request failed: %v", mode, err) + } + defer response.Body.Close() + body, err := io.ReadAll(response.Body) + if err != nil || string(body) != "configuration reached" { + t.Fatalf("configuration response = %q, error = %v", body, err) + } + }) + } +} + +func TestDispatcherSaaSHTTPClientDoesNotUseAgentCAForRealSaaS(t *testing.T) { + for _, mode := range []string{"nonprod-real", "sip-only"} { + t.Run(mode, func(t *testing.T) { + client, err := dispatcherSaaSHTTPClient(mode, []byte("not a SaaS CA")) + if err != nil { + t.Fatalf("real SaaS must use system trust, not the Agent CA: %v", err) + } + if client.Transport != nil { + t.Fatal("real SaaS must use the standard HTTP transport") + } + }) + } +} + +func TestDispatcherSaaSHTTPClientRejectsUnsupportedMode(t *testing.T) { + for _, mode := range []string{"", "real", "mixed"} { + if _, err := dispatcherSaaSHTTPClient(mode, nil); err == nil { + t.Fatalf("unsupported mode %q was accepted", mode) + } + } +} diff --git a/cmd/sip-go-agent/dispatcher_sip.go b/cmd/sip-go-agent/dispatcher_sip.go index 8d55fcf..596ea26 100644 --- a/cmd/sip-go-agent/dispatcher_sip.go +++ b/cmd/sip-go-agent/dispatcher_sip.go @@ -46,11 +46,10 @@ func runSIPOnlyDispatcher(ctx context.Context, settings config.DispatcherRuntime if err != nil { return errors.New("SIP-only Dispatcher mTLS Agent certificate is invalid") } - httpClient, err := localMockHTTPClient(ca) + httpClient, err := dispatcherSaaSHTTPClient("sip-only", ca) if err != nil { return err } - httpClient.Timeout = 10 * time.Second _, reader, err := dispatcherConfigurationClient("sip-only", httpClient) if err != nil { return err diff --git a/docs/evidence/mq-unused-exchange-removal-20261009.md b/docs/evidence/mq-unused-exchange-removal-20261009.md index 601a828..da6f4b8 100644 --- a/docs/evidence/mq-unused-exchange-removal-20261009.md +++ b/docs/evidence/mq-unused-exchange-removal-20261009.md @@ -24,6 +24,14 @@ - `make acceptance-local` 与 `make release-check-local` 通过,业务覆盖率 **70.7%**。 - 上述验证不代表真实 SaaS、AI、OSS、通话或生产验收。 -## 部署验证 +## 首次部署与追加修复 -部署后的固定提交、制品校验、服务状态及剩余阻碍在现场验证后补充,不以本地测试替代。 +首次部署源码 `c144d734ee3625b676783b382314acf7aad1c659`,制品校验一致,未重置数据。多余交换机检查已解除;随后真实 SaaS 配置请求被 `Mock upload target is not local` 拒绝,Dispatcher 仍退出重启。该错误是程序错误,不是 SaaS 尚未就绪,不能把部署输出中的 `saas_pending` 归类当成根因或完成证明。 + +根因为普通 Dispatcher 与 SIP-only Dispatcher 都误用了 Mock 专用的本机 HTTP 客户端。使用者另行确认继续修复并重新部署:真实模式改为标准 HTTP 客户端和系统证书信任,Mock 仍保留本机连接限制和专用测试信任;不把 Agent 的双向 TLS 证书作为 SaaS 信任来源。两条入口复用明确按模式选择的客户端,保留 10 秒请求超时,未知模式明确拒绝。 + +新增行为测试将非本机 SaaS 域名在测试进程内指向隔离本机服务器,旧逻辑真实模式明确失败;修复后真实两种模式成功、Mock 仍拒绝原非本机域名且能访问本机。没有外网测试请求。追加修复后再次完成全部本地验收、race 测试、发布检查及 70.7% 业务覆盖率检查。 + +## 最终部署验证 + +再次部署后的固定提交、制品校验、服务状态及剩余阻碍在现场验证后补充,不以本地测试替代。