🐛 fix(proxy): 避免全局代理影响数据库连接

移除数据库和 Redis 连接对全局代理的隐式继承

保留连接自身代理与 HTTP 隧道配置并修正缓存释放链路

同步六种语言的代理作用域说明并增加回归测试

Fixes #347
This commit is contained in:
Syngnat
2026-07-18 16:43:39 +08:00
parent aeecffb517
commit 0136b87311
16 changed files with 147 additions and 75 deletions

View File

@@ -564,7 +564,7 @@ func (a *App) invalidateCachedDatabase(config connection.ConnectionConfig, reaso
if resolvedConfig, err := a.resolveConnectionSecrets(config); err == nil {
config = resolvedConfig
}
effectiveConfig := applyGlobalProxyToConnection(config)
effectiveConfig := config
key := getCacheKey(effectiveConfig)
shortKey := shortCacheKey(key)
@@ -876,7 +876,7 @@ func (a *App) resolveEffectiveConnectionConfig(config connection.ConnectionConfi
if err != nil {
return config, wrapConnectError(config, err)
}
return applyGlobalProxyToConnection(resolvedConfig), nil
return resolvedConfig, nil
}
func (a *App) getDatabaseWithPing(config connection.ConnectionConfig, forcePing bool) (db.Database, error) {
@@ -884,7 +884,7 @@ func (a *App) getDatabaseWithPing(config connection.ConnectionConfig, forcePing
if err != nil {
return nil, wrapConnectError(config, err)
}
effectiveConfig := applyGlobalProxyToConnection(resolvedConfig)
effectiveConfig := resolvedConfig
isFileDB := isFileDatabaseType(effectiveConfig.Type)
key := getCacheKey(effectiveConfig)
@@ -1164,7 +1164,7 @@ func (a *App) connectDatabaseWithStartupRetry(rawConfig connection.ConnectionCon
var lastEffectiveConfig connection.ConnectionConfig
for attempt := 1; attempt <= startupConnectRetryAttempts; attempt++ {
effectiveConfig := applyGlobalProxyToConnection(rawConfig)
effectiveConfig := rawConfig
lastEffectiveConfig = effectiveConfig
cacheKey := shortenCacheKey(getCacheKey(effectiveConfig))
@@ -1219,12 +1219,7 @@ func (a *App) startupPhaseLabel() string {
age = 0
}
if age <= startupConnectRetryWindow {
snapshot := currentGlobalProxyConfig()
state := "关闭"
if snapshot.Enabled {
state = fmt.Sprintf("启用(%s://%s:%d)", strings.ToLower(strings.TrimSpace(snapshot.Proxy.Type)), strings.TrimSpace(snapshot.Proxy.Host), snapshot.Proxy.Port)
}
return fmt.Sprintf("启动期(age=%s,全局代理=%s)", age, state)
return fmt.Sprintf("启动期(age=%s)", age)
}
return fmt.Sprintf("稳定期(age=%s)", age)
}

View File

@@ -50,7 +50,7 @@ func (f *fakeStartupRetryDB) GetTriggers(dbName, tableName string) ([]connection
return nil, nil
}
func TestConnectDatabaseWithStartupRetry_RetriesTransientFailureAndReappliesGlobalProxy(t *testing.T) {
func TestConnectDatabaseWithStartupRetry_RetriesTransientFailureWithoutApplyingGlobalProxy(t *testing.T) {
originalNewDatabaseFunc := newDatabaseFunc
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
snapshot := currentGlobalProxyConfig()
@@ -101,11 +101,11 @@ func TestConnectDatabaseWithStartupRetry_RetriesTransientFailureAndReappliesGlob
if seenConfigs[0].UseProxy {
t.Fatalf("expected first attempt without proxy, got %+v", seenConfigs[0])
}
if !seenConfigs[1].UseProxy {
t.Fatalf("expected second attempt with proxy after startup retry, got %+v", seenConfigs[1])
if seenConfigs[1].UseProxy {
t.Fatalf("expected global proxy change not to affect second database attempt, got %+v", seenConfigs[1])
}
if !effectiveConfig.UseProxy {
t.Fatalf("expected returned effective config to include proxy, got %+v", effectiveConfig)
if effectiveConfig.UseProxy {
t.Fatalf("expected returned effective config to exclude global proxy, got %+v", effectiveConfig)
}
}
@@ -300,10 +300,10 @@ func TestGetDatabaseWithPing_CoolsDownRepeatedFailures(t *testing.T) {
}
a := &App{
startedAt: time.Now().Add(-startupConnectRetryWindow - time.Second),
dbCache: make(map[string]cachedDatabase),
startedAt: time.Now().Add(-startupConnectRetryWindow - time.Second),
dbCache: make(map[string]cachedDatabase),
connectFailures: make(map[string]cachedConnectFailure),
runningQueries: make(map[string]queryContext),
runningQueries: make(map[string]queryContext),
}
config := connection.ConnectionConfig{Type: "postgres", Host: "10.1.131.86", Port: 5432, User: "postgres"}
@@ -349,10 +349,10 @@ func TestGetDatabaseWithPing_AllowsRetryAfterFailureCooldown(t *testing.T) {
}
a := &App{
startedAt: time.Now().Add(-startupConnectRetryWindow - time.Second),
dbCache: make(map[string]cachedDatabase),
startedAt: time.Now().Add(-startupConnectRetryWindow - time.Second),
dbCache: make(map[string]cachedDatabase),
connectFailures: make(map[string]cachedConnectFailure),
runningQueries: make(map[string]queryContext),
runningQueries: make(map[string]queryContext),
}
config := connection.ConnectionConfig{Type: "postgres", Host: "10.1.131.86", Port: 5432, User: "postgres"}
@@ -409,10 +409,10 @@ func TestGetDatabaseWithPing_ClearsFailureCooldownAfterSuccess(t *testing.T) {
}
a := &App{
startedAt: time.Now().Add(-startupConnectRetryWindow - time.Second),
dbCache: make(map[string]cachedDatabase),
startedAt: time.Now().Add(-startupConnectRetryWindow - time.Second),
dbCache: make(map[string]cachedDatabase),
connectFailures: make(map[string]cachedConnectFailure),
runningQueries: make(map[string]queryContext),
runningQueries: make(map[string]queryContext),
}
config := connection.ConnectionConfig{Type: "postgres", Host: "10.1.131.86", Port: 5432, User: "postgres"}

View File

@@ -10,6 +10,15 @@ import (
proxytunnel "GoNavi-Wails/internal/proxy"
)
func isFileDatabaseType(driverType string) bool {
switch strings.ToLower(strings.TrimSpace(driverType)) {
case "sqlite", "duckdb":
return true
default:
return false
}
}
func resolveDialConfigWithProxy(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
config := raw
if config.UseHTTPTunnel {

View File

@@ -160,36 +160,6 @@ func (a *App) GetGlobalProxyConfig() connection.QueryResult {
}
}
func applyGlobalProxyToConnection(config connection.ConnectionConfig) connection.ConnectionConfig {
effective := config
if effective.UseProxy || effective.UseHTTPTunnel {
return effective
}
if isFileDatabaseType(effective.Type) {
effective.Proxy = connection.ProxyConfig{}
return effective
}
snapshot := currentGlobalProxyConfig()
if !snapshot.Enabled {
effective.Proxy = connection.ProxyConfig{}
return effective
}
effective.UseProxy = true
effective.Proxy = snapshot.Proxy
return effective
}
func isFileDatabaseType(driverType string) bool {
switch strings.ToLower(strings.TrimSpace(driverType)) {
case "sqlite", "duckdb":
return true
default:
return false
}
}
func newHTTPClientWithGlobalProxy(timeout time.Duration) *http.Client {
client := &http.Client{
Timeout: timeout,

View File

@@ -0,0 +1,52 @@
package app
import (
"testing"
"GoNavi-Wails/internal/connection"
)
func TestResolveEffectiveConnectionConfigUsesOnlyConnectionProxy(t *testing.T) {
snapshot := currentGlobalProxyConfig()
if _, err := setGlobalProxyConfig(true, connection.ProxyConfig{
Type: "socks5",
Host: "127.0.0.1",
Port: 1080,
}); err != nil {
t.Fatalf("enable global proxy failed: %v", err)
}
t.Cleanup(func() {
_, _ = setGlobalProxyConfig(snapshot.Enabled, snapshot.Proxy)
})
app := NewApp()
directConfig := connection.ConnectionConfig{
Type: "mysql",
Host: "db.internal",
Port: 3306,
}
effectiveDirect, err := app.resolveEffectiveConnectionConfig(directConfig)
if err != nil {
t.Fatalf("resolve direct config failed: %v", err)
}
if effectiveDirect.UseProxy || effectiveDirect.Proxy != (connection.ProxyConfig{}) {
t.Fatalf("global proxy must not be injected into a direct database connection, got %+v", effectiveDirect)
}
connectionProxy := connection.ProxyConfig{
Type: "http",
Host: "db-proxy.internal",
Port: 8080,
User: "proxy-user",
}
proxiedConfig := directConfig
proxiedConfig.UseProxy = true
proxiedConfig.Proxy = connectionProxy
effectiveProxied, err := app.resolveEffectiveConnectionConfig(proxiedConfig)
if err != nil {
t.Fatalf("resolve proxied config failed: %v", err)
}
if !effectiveProxied.UseProxy || !proxyConfigEqual(effectiveProxied.Proxy, connectionProxy) {
t.Fatalf("connection-specific proxy must be preserved, got %+v", effectiveProxied)
}
}

View File

@@ -99,7 +99,7 @@ func (a *App) DBReleaseConnection(config connection.ConnectionConfig) connection
logger.Error(wrapped, "DBReleaseConnection 解析连接密文失败:%s", formatConnSummary(config))
return connection.QueryResult{Success: false, Message: wrapped.Error()}
}
closed := a.releaseCachedDatabaseConnectionsForConfig(applyGlobalProxyToConnection(resolvedConfig))
closed := a.releaseCachedDatabaseConnectionsForConfig(resolvedConfig)
logger.Infof("DBReleaseConnection 已释放数据库连接:%s 数量=%d", formatConnSummary(resolvedConfig), closed)
return connection.QueryResult{Success: true, Message: a.appText("db.backend.message.release_success", nil), Data: map[string]int{"closed": closed}}

View File

@@ -175,10 +175,10 @@ func TestFormatConnSummary_DefaultTimeout(t *testing.T) {
}
}
func TestDBReleaseConnectionClosesAllDatabaseCacheEntriesForSameInstance(t *testing.T) {
func TestDBReleaseConnectionClosesAllDatabaseCacheEntriesForSameInstanceDespiteGlobalProxy(t *testing.T) {
proxySnapshot := currentGlobalProxyConfig()
if _, err := setGlobalProxyConfig(false, proxySnapshot.Proxy); err != nil {
t.Fatalf("disable global proxy failed: %v", err)
if _, err := setGlobalProxyConfig(true, connection.ProxyConfig{Type: "socks5", Host: "127.0.0.1", Port: 1080}); err != nil {
t.Fatalf("enable global proxy failed: %v", err)
}
t.Cleanup(func() {
_, _ = setGlobalProxyConfig(proxySnapshot.Enabled, proxySnapshot.Proxy)

View File

@@ -682,7 +682,7 @@ func (a *App) getRedisClient(config connection.ConnectionConfig) (redis.RedisCli
return nil, wrapped
}
effectiveConfig := applyGlobalProxyToConnection(resolvedConfig)
effectiveConfig := resolvedConfig
connectConfig, proxyErr := resolveDialConfigWithProxyFunc(effectiveConfig)
if proxyErr != nil {
wrapped := wrapConnectError(effectiveConfig, proxyErr)
@@ -735,7 +735,7 @@ func (a *App) openRedisClientIsolated(config connection.ConnectionConfig) (redis
return nil, wrapped
}
effectiveConfig := applyGlobalProxyToConnection(resolvedConfig)
effectiveConfig := resolvedConfig
connectConfig, proxyErr := resolveDialConfigWithProxyFunc(effectiveConfig)
if proxyErr != nil {
wrapped := wrapConnectError(effectiveConfig, proxyErr)
@@ -824,7 +824,7 @@ func (a *App) releaseRedisClientsForConfig(config connection.ConnectionConfig) (
if err != nil {
return 0, wrapConnectError(config, err)
}
targetKey := getConnectionReleaseMatchKey(applyGlobalProxyToConnection(resolvedConfig))
targetKey := getConnectionReleaseMatchKey(resolvedConfig)
closed := 0
redisCacheMu.Lock()

View File

@@ -385,15 +385,17 @@ func TestRedisTestConnectionUsesIsolatedClientAndClosesIt(t *testing.T) {
CloseAllRedisClients()
}()
CloseAllRedisClients()
if _, err := setGlobalProxyConfig(false, proxySnapshot.Proxy); err != nil {
t.Fatalf("disable global proxy failed: %v", err)
if _, err := setGlobalProxyConfig(true, connection.ProxyConfig{Type: "socks5", Host: "127.0.0.1", Port: 1080}); err != nil {
t.Fatalf("enable global proxy failed: %v", err)
}
client := &capturingRedisClient{}
var dialConfig connection.ConnectionConfig
newRedisClientFunc = func() redislib.RedisClient {
return client
}
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
dialConfig = raw
return raw, nil
}
@@ -413,6 +415,9 @@ func TestRedisTestConnectionUsesIsolatedClientAndClosesIt(t *testing.T) {
if len(redisCache) != 0 {
t.Fatalf("redis test connection must not write global redis cache, got %d entries", len(redisCache))
}
if dialConfig.UseProxy {
t.Fatalf("global proxy must not be applied to Redis connections, got %+v", dialConfig)
}
}
func TestRedisTestConnectionReturnsLocalizedCloseFailure(t *testing.T) {