fix(dispatcher): use real SaaS HTTP transport outside mock mode

This commit is contained in:
2026-10-09 15:27:31 +08:00
parent c144d734ee
commit 3fc61105b4
5 changed files with 129 additions and 6 deletions
+1 -2
View File
@@ -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
+25
View File
@@ -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)
}
}
+92
View File
@@ -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)
}
}
}
+1 -2
View File
@@ -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
@@ -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% 业务覆盖率检查。
## 最终部署验证
再次部署后的固定提交、制品校验、服务状态及剩余阻碍在现场验证后补充,不以本地测试替代。