mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
fix(cache): harden cache backends, resilience, recovery, and transports
- RESP -ERR no longer closes healthy conn / aborts batch; global cap replacement-aware - global health-check pass serialized (latent data race); MemoryMonitor via ResourceManager - token-exchange retry, open-circuit rejections counted as failures - TLS-posture matching transport reuse (no silent InsecureSkipVerify handoff)
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestTransportPool_ConfigKeyDistinguishesMinTLSVersion guards the R146
|
||||
// fix in internal/pool/transport.go configKey: the key previously
|
||||
// omitted MinTLSVersion (and all timeouts), so two configs differing
|
||||
// only in the TLS minimum version collided on one key and silently
|
||||
// shared a single transport with whoever-arrived-first's settings — a
|
||||
// possible TLS-version downgrade. Distinct TLS minimum versions must
|
||||
// yield non-shared transports with the requested MinVersion.
|
||||
func TestTransportPool_ConfigKeyDistinguishesMinTLSVersion(t *testing.T) {
|
||||
p := &TransportPool{
|
||||
transports: make(map[string]*sharedTransport),
|
||||
maxConns: 20,
|
||||
clientCount: 0,
|
||||
maxClients: 100, // high so the at-cap path isn't taken
|
||||
}
|
||||
|
||||
cfg12 := TransportConfig{MinTLSVersion: tls.VersionTLS12}
|
||||
cfg13 := TransportConfig{MinTLSVersion: tls.VersionTLS13}
|
||||
|
||||
a := p.GetTransport(cfg12)
|
||||
b := p.GetTransport(cfg13)
|
||||
|
||||
if a == b {
|
||||
t.Fatal("two configs differing only in MinTLSVersion must not share a transport")
|
||||
}
|
||||
if a.TLSClientConfig == nil || a.TLSClientConfig.MinVersion != tls.VersionTLS12 {
|
||||
t.Fatalf("transport A must use TLS1.2, got %+v", a.TLSClientConfig)
|
||||
}
|
||||
if b.TLSClientConfig == nil || b.TLSClientConfig.MinVersion != tls.VersionTLS13 {
|
||||
t.Fatalf("transport B must use TLS1.3, got %+v", b.TLSClientConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransportPool_ConfigKeyDistinguishesTimeouts guards the R146 fix
|
||||
// for the timeout fields: configs differing only in a timeout must not
|
||||
// collide and share one transport.
|
||||
func TestTransportPool_ConfigKeyDistinguishesTimeouts(t *testing.T) {
|
||||
p := &TransportPool{
|
||||
transports: make(map[string]*sharedTransport),
|
||||
maxConns: 20,
|
||||
clientCount: 0,
|
||||
maxClients: 100,
|
||||
}
|
||||
|
||||
cfgFast := TransportConfig{MinTLSVersion: tls.VersionTLS12, DialTimeout: time.Second}
|
||||
cfgSlow := TransportConfig{MinTLSVersion: tls.VersionTLS12, DialTimeout: 30 * time.Second}
|
||||
|
||||
a := p.GetTransport(cfgFast)
|
||||
b := p.GetTransport(cfgSlow)
|
||||
|
||||
if a == nil || b == nil {
|
||||
t.Fatal("both transports must be created")
|
||||
}
|
||||
if a == b {
|
||||
t.Fatal("two configs differing only in DialTimeout must not share a transport")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestTransportPool_TLSMatchesNormalizesDefault covers the previously
|
||||
// 0%-covered tlsMatches security guard (R146): two transports share the
|
||||
// pool only when their TLS posture (certificate verification and
|
||||
// minimum protocol version) matches, with 0 normalized to the TLS1.2
|
||||
// default that createTransport applies.
|
||||
func TestTransportPool_TLSMatchesNormalizesDefault(t *testing.T) {
|
||||
p := &TransportPool{}
|
||||
|
||||
// 0 (createTransport's default) must be equivalent to explicit TLS1.2.
|
||||
if !p.tlsMatches(TransportConfig{}, TransportConfig{MinTLSVersion: tls.VersionTLS12}) {
|
||||
t.Fatal("zero and explicit TLS1.2 minimum must match after normalization")
|
||||
}
|
||||
|
||||
// Differing verification posture must never match.
|
||||
if p.tlsMatches(
|
||||
TransportConfig{InsecureSkipVerify: true},
|
||||
TransportConfig{InsecureSkipVerify: false},
|
||||
) {
|
||||
t.Fatal("configs differing in InsecureSkipVerify must not match")
|
||||
}
|
||||
|
||||
// Differing minimum TLS version must never match.
|
||||
if p.tlsMatches(
|
||||
TransportConfig{MinTLSVersion: tls.VersionTLS12},
|
||||
TransportConfig{MinTLSVersion: tls.VersionTLS13},
|
||||
) {
|
||||
t.Fatal("configs differing in MinTLSVersion must not match")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransportPool_NoTLSMismatchReuse drives getExistingTransport
|
||||
// directly: a strict-verifying caller must never be handed a pooled
|
||||
// InsecureSkipVerify transport (silent cert-verification bypass, R146);
|
||||
// it falls back to nil so the caller builds a safe verifying client.
|
||||
func TestTransportPool_NoTLSMismatchReuse(t *testing.T) {
|
||||
p := &TransportPool{
|
||||
transports: map[string]*sharedTransport{
|
||||
"insecure": {
|
||||
transport: &http.Transport{},
|
||||
config: TransportConfig{InsecureSkipVerify: true, MinTLSVersion: tls.VersionTLS12},
|
||||
refCount: 0,
|
||||
lastUsed: time.Now(),
|
||||
},
|
||||
"verifyTLS13": {
|
||||
transport: &http.Transport{},
|
||||
config: TransportConfig{InsecureSkipVerify: false, MinTLSVersion: tls.VersionTLS13},
|
||||
refCount: 0,
|
||||
lastUsed: time.Now(),
|
||||
},
|
||||
},
|
||||
maxConns: 20,
|
||||
maxClients: 100,
|
||||
}
|
||||
|
||||
// Strict TLS1.2 caller must NOT reuse the insecure or TLS1.3 peers.
|
||||
if tr := p.getExistingTransport(TransportConfig{MinTLSVersion: tls.VersionTLS12}); tr != nil {
|
||||
t.Fatal("strict-verifying caller received a mismatched pooled transport")
|
||||
}
|
||||
|
||||
// Insecure caller matches the insecure peer and reuses it.
|
||||
if tr := p.getExistingTransport(TransportConfig{InsecureSkipVerify: true, MinTLSVersion: tls.VersionTLS12}); tr == nil {
|
||||
t.Fatal("insecure caller should reuse the insecure pooled transport")
|
||||
}
|
||||
|
||||
// TLS1.3 verifying caller matches the TLS1.3 peer.
|
||||
if tr := p.getExistingTransport(TransportConfig{MinTLSVersion: tls.VersionTLS13}); tr == nil {
|
||||
t.Fatal("TLS1.3 caller should reuse the TLS1.3 verifying transport")
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func DefaultTransportConfig() TransportConfig {
|
||||
func (p *TransportPool) GetTransport(config TransportConfig) *http.Transport {
|
||||
// Check client limit
|
||||
if atomic.LoadInt32(&p.clientCount) >= p.maxClients {
|
||||
return p.getExistingTransport()
|
||||
return p.getExistingTransport(config)
|
||||
}
|
||||
|
||||
key := p.configKey(config)
|
||||
@@ -180,20 +180,41 @@ func (p *TransportPool) ReleaseTransport(transport *http.Transport) {
|
||||
}
|
||||
|
||||
// getExistingTransport returns any available transport when limit is reached
|
||||
func (p *TransportPool) getExistingTransport() *http.Transport {
|
||||
func (p *TransportPool) getExistingTransport(config TransportConfig) *http.Transport {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
// Prefer a transport whose TLS settings (verification, min version)
|
||||
// match the request. Reusing a mismatched transport at cap could hand a
|
||||
// strict-verifying caller an InsecureSkipVerify transport (silent cert
|
||||
// verification bypass) or an insecure caller a verifying one (R146).
|
||||
for _, shared := range p.transports {
|
||||
if shared != nil && shared.transport != nil {
|
||||
if shared != nil && shared.transport != nil && p.tlsMatches(config, shared.config) {
|
||||
atomic.AddInt32(&shared.refCount, 1)
|
||||
shared.lastUsed = time.Now()
|
||||
return shared.transport
|
||||
}
|
||||
}
|
||||
// No TLS-matching pooled transport: return nil so the caller falls back
|
||||
// to a safe basic client (http.DefaultTransport verifies certs) rather
|
||||
// than receiving a mismatched peer. Bounded by the cap (R146).
|
||||
return nil
|
||||
}
|
||||
|
||||
// tlsMatches reports whether two transports share the same TLS posture
|
||||
// (certificate verification and minimum protocol version), normalized for
|
||||
// createTransport's 0-means-TLS1.2 default.
|
||||
func (p *TransportPool) tlsMatches(a, b TransportConfig) bool {
|
||||
amin, bmin := a.MinTLSVersion, b.MinTLSVersion
|
||||
if amin == 0 {
|
||||
amin = tls.VersionTLS12
|
||||
}
|
||||
if bmin == 0 {
|
||||
bmin = tls.VersionTLS12
|
||||
}
|
||||
return a.InsecureSkipVerify == b.InsecureSkipVerify && amin == bmin
|
||||
}
|
||||
|
||||
// createTransport creates a new HTTP transport with the given config
|
||||
func (p *TransportPool) createTransport(config TransportConfig) *http.Transport {
|
||||
// Set secure defaults
|
||||
@@ -276,6 +297,28 @@ func (p *TransportPool) configKey(config TransportConfig) string {
|
||||
} else {
|
||||
sb.WriteByte(0)
|
||||
}
|
||||
// Include MinTLSVersion and every timeout so two configs differing only
|
||||
// in TLS minimum version or timeouts do NOT collide on one key and
|
||||
// silently share a transport with whoever-arrived-first's settings
|
||||
// (a possible TLS-version downgrade). Mirror http_client_pool.go's
|
||||
// all-field key (R146).
|
||||
sb.WriteByte('|')
|
||||
if config.MinTLSVersion == 0 {
|
||||
sb.WriteString("tls12")
|
||||
} else {
|
||||
sb.WriteString(strconv.Itoa(int(config.MinTLSVersion)))
|
||||
}
|
||||
for _, d := range []time.Duration{
|
||||
config.DialTimeout,
|
||||
config.TLSHandshakeTimeout,
|
||||
config.ResponseHeaderTimeout,
|
||||
config.ExpectContinueTimeout,
|
||||
config.IdleConnTimeout,
|
||||
config.KeepAlive,
|
||||
} {
|
||||
sb.WriteByte('|')
|
||||
sb.WriteString(strconv.FormatInt(int64(d), 10))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user