fix(traffic): keep TLS fallback reload-aware
- commit
- cd72440393c4f13e5f9cff99bd134463ec81927a
- author
- brice · · 2026-06-30 07:45 UTC
- parent
- 228f200
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +219 -61
M internal/adapters/in/traffic/tcp.go +30 -9
| @@ -162,20 +162,32 @@ func (r *entryPointRuntime) replaceTLSHTTPServer(entryPoint domain.EntryPoint, c | |||
| 162 | 162 | }() | |
| 163 | 163 | } | |
| 164 | 164 | ||
| 165 | - | func (r *entryPointRuntime) routeToHTTPS(conn net.Conn) bool { | |
| 165 | + | func (r *entryPointRuntime) routeToHTTPS(tracked *trackedTCPConn, conn net.Conn) bool { | |
| 166 | 166 | r.mu.Lock() | |
| 167 | 167 | listener := r.tlsHTTPListener | |
| 168 | 168 | r.mu.Unlock() | |
| 169 | 169 | if listener == nil { | |
| 170 | 170 | return false | |
| 171 | 171 | } | |
| 172 | - | if listener.serve(conn) { | |
| 172 | + | if listener.serve(&trackedHTTPConn{Conn: conn, onClose: func() { r.untrack(tracked) }}) { | |
| 173 | 173 | return true | |
| 174 | 174 | } | |
| 175 | 175 | _ = conn.Close() | |
| 176 | 176 | return true | |
| 177 | 177 | } | |
| 178 | 178 | ||
| 179 | + | type trackedHTTPConn struct { | |
| 180 | + | net.Conn | |
| 181 | + | once sync.Once | |
| 182 | + | onClose func() | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | func (c *trackedHTTPConn) Close() error { | |
| 186 | + | err := c.Conn.Close() | |
| 187 | + | c.once.Do(c.onClose) | |
| 188 | + | return err | |
| 189 | + | } | |
| 190 | + | ||
| 179 | 191 | func (r *entryPointRuntime) acceptLoop() { | |
| 180 | 192 | defer r.closeAcceptDone() | |
| 181 | 193 | for { | |
| @@ -220,13 +232,20 @@ func (r *entryPointRuntime) handleTCPConn(client net.Conn) { | |||
| 220 | 232 | ||
| 221 | 233 | tracked := &trackedTCPConn{client: client} | |
| 222 | 234 | r.track(tracked) | |
| 223 | - | defer r.untrack(tracked) | |
| 235 | + | untrackOnReturn := true | |
| 236 | + | defer func() { | |
| 237 | + | if untrackOnReturn { | |
| 238 | + | r.untrack(tracked) | |
| 239 | + | } | |
| 240 | + | }() | |
| 224 | 241 | ||
| 225 | 242 | switch r.entryPointSnapshot().Protocol { | |
| 226 | 243 | case domain.EntryPointProtocolTCP: | |
| 227 | 244 | r.handlePlainTCP(tracked, options) | |
| 228 | 245 | case domain.EntryPointProtocolTLSMux: | |
| 229 | - | r.handleTLSMux(tracked, options) | |
| 246 | + | if r.handleTLSMux(tracked, options) { | |
| 247 | + | untrackOnReturn = false | |
| 248 | + | } | |
| 230 | 249 | default: | |
| 231 | 250 | r.counters.totalRefused.Add(1) | |
| 232 | 251 | _ = tracked.client.Close() | |
| @@ -243,23 +262,24 @@ func (r *entryPointRuntime) handlePlainTCP(tracked *trackedTCPConn, options doma | |||
| 243 | 262 | r.proxyToBackend(tracked, tracked.client, backend, options) | |
| 244 | 263 | } | |
| 245 | 264 | ||
| 246 | - | func (r *entryPointRuntime) handleTLSMux(tracked *trackedTCPConn, options domain.TCPOptions) { | |
| 265 | + | func (r *entryPointRuntime) handleTLSMux(tracked *trackedTCPConn, options domain.TCPOptions) bool { | |
| 247 | 266 | peeked, err := r.peekTLSClientHello(tracked.client, options) | |
| 248 | 267 | if err != nil { | |
| 249 | 268 | r.counters.totalErrors.Add(1) | |
| 250 | 269 | _ = tracked.client.Close() | |
| 251 | - | return | |
| 270 | + | return false | |
| 252 | 271 | } | |
| 253 | 272 | if backend, ok := r.resolveTLSBackend(peeked.sni); ok { | |
| 254 | 273 | r.proxyToBackend(tracked, peeked.conn, backend, options) | |
| 255 | - | return | |
| 274 | + | return false | |
| 256 | 275 | } | |
| 257 | - | if r.routeToHTTPS(peeked.conn) { | |
| 276 | + | if r.routeToHTTPS(tracked, peeked.conn) { | |
| 258 | 277 | r.counters.totalAccepted.Add(1) | |
| 259 | - | return | |
| 278 | + | return true | |
| 260 | 279 | } | |
| 261 | 280 | r.counters.totalRefused.Add(1) | |
| 262 | 281 | _ = peeked.conn.Close() | |
| 282 | + | return false | |
| 263 | 283 | } | |
| 264 | 284 | ||
| 265 | 285 | func (r *entryPointRuntime) peekTLSClientHello(client net.Conn, options domain.TCPOptions) (peekedTLSConn, error) { | |
| @@ -623,6 +643,7 @@ func (r *entryPointRuntime) updateEntryPoint(entryPoint domain.EntryPoint, trust | |||
| 623 | 643 | r.mu.Unlock() | |
| 624 | 644 | for _, conn := range stale { | |
| 625 | 645 | conn.close() | |
| 646 | + | r.untrack(conn) | |
| 626 | 647 | } | |
| 627 | 648 | if previousProtocol == domain.EntryPointProtocolTLSMux && entryPoint.Protocol != domain.EntryPointProtocolTLSMux { | |
| 628 | 649 | r.stopTLSHTTPServer(r.ctx, 0) | |
| @@ -162,20 +162,32 @@ func (r *entryPointRuntime) replaceTLSHTTPServer(entryPoint domain.EntryPoint, c | |||||
| 162 | }() | 162 | }() | ||
| 163 | } | 163 | } | ||
| 164 | 164 | ||||
| 165 | - | func (r *entryPointRuntime) routeToHTTPS(conn net.Conn) bool { | 165 | + | func (r *entryPointRuntime) routeToHTTPS(tracked *trackedTCPConn, conn net.Conn) bool { |
| 166 | r.mu.Lock() | 166 | r.mu.Lock() | ||
| 167 | listener := r.tlsHTTPListener | 167 | listener := r.tlsHTTPListener | ||
| 168 | r.mu.Unlock() | 168 | r.mu.Unlock() | ||
| 169 | if listener == nil { | 169 | if listener == nil { | ||
| 170 | return false | 170 | return false | ||
| 171 | } | 171 | } | ||
| 172 | - | if listener.serve(conn) { | 172 | + | if listener.serve(&trackedHTTPConn{Conn: conn, onClose: func() { r.untrack(tracked) }}) { |
| 173 | return true | 173 | return true | ||
| 174 | } | 174 | } | ||
| 175 | _ = conn.Close() | 175 | _ = conn.Close() | ||
| 176 | return true | 176 | return true | ||
| 177 | } | 177 | } | ||
| 178 | 178 | ||||
| 179 | + | type trackedHTTPConn struct { | |||
| 180 | + | net.Conn | |||
| 181 | + | once sync.Once | |||
| 182 | + | onClose func() | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | func (c *trackedHTTPConn) Close() error { | |||
| 186 | + | err := c.Conn.Close() | |||
| 187 | + | c.once.Do(c.onClose) | |||
| 188 | + | return err | |||
| 189 | + | } | |||
| 190 | + | ||||
| 179 | func (r *entryPointRuntime) acceptLoop() { | 191 | func (r *entryPointRuntime) acceptLoop() { | ||
| 180 | defer r.closeAcceptDone() | 192 | defer r.closeAcceptDone() | ||
| 181 | for { | 193 | for { | ||
| @@ -220,13 +232,20 @@ func (r *entryPointRuntime) handleTCPConn(client net.Conn) { | |||||
| 220 | 232 | ||||
| 221 | tracked := &trackedTCPConn{client: client} | 233 | tracked := &trackedTCPConn{client: client} | ||
| 222 | r.track(tracked) | 234 | r.track(tracked) | ||
| 223 | - | defer r.untrack(tracked) | 235 | + | untrackOnReturn := true |
| 236 | + | defer func() { | |||
| 237 | + | if untrackOnReturn { | |||
| 238 | + | r.untrack(tracked) | |||
| 239 | + | } | |||
| 240 | + | }() | |||
| 224 | 241 | ||||
| 225 | switch r.entryPointSnapshot().Protocol { | 242 | switch r.entryPointSnapshot().Protocol { | ||
| 226 | case domain.EntryPointProtocolTCP: | 243 | case domain.EntryPointProtocolTCP: | ||
| 227 | r.handlePlainTCP(tracked, options) | 244 | r.handlePlainTCP(tracked, options) | ||
| 228 | case domain.EntryPointProtocolTLSMux: | 245 | case domain.EntryPointProtocolTLSMux: | ||
| 229 | - | r.handleTLSMux(tracked, options) | 246 | + | if r.handleTLSMux(tracked, options) { |
| 247 | + | untrackOnReturn = false | |||
| 248 | + | } | |||
| 230 | default: | 249 | default: | ||
| 231 | r.counters.totalRefused.Add(1) | 250 | r.counters.totalRefused.Add(1) | ||
| 232 | _ = tracked.client.Close() | 251 | _ = tracked.client.Close() | ||
| @@ -243,23 +262,24 @@ func (r *entryPointRuntime) handlePlainTCP(tracked *trackedTCPConn, options doma | |||||
| 243 | r.proxyToBackend(tracked, tracked.client, backend, options) | 262 | r.proxyToBackend(tracked, tracked.client, backend, options) | ||
| 244 | } | 263 | } | ||
| 245 | 264 | ||||
| 246 | - | func (r *entryPointRuntime) handleTLSMux(tracked *trackedTCPConn, options domain.TCPOptions) { | 265 | + | func (r *entryPointRuntime) handleTLSMux(tracked *trackedTCPConn, options domain.TCPOptions) bool { |
| 247 | peeked, err := r.peekTLSClientHello(tracked.client, options) | 266 | peeked, err := r.peekTLSClientHello(tracked.client, options) | ||
| 248 | if err != nil { | 267 | if err != nil { | ||
| 249 | r.counters.totalErrors.Add(1) | 268 | r.counters.totalErrors.Add(1) | ||
| 250 | _ = tracked.client.Close() | 269 | _ = tracked.client.Close() | ||
| 251 | - | return | 270 | + | return false |
| 252 | } | 271 | } | ||
| 253 | if backend, ok := r.resolveTLSBackend(peeked.sni); ok { | 272 | if backend, ok := r.resolveTLSBackend(peeked.sni); ok { | ||
| 254 | r.proxyToBackend(tracked, peeked.conn, backend, options) | 273 | r.proxyToBackend(tracked, peeked.conn, backend, options) | ||
| 255 | - | return | 274 | + | return false |
| 256 | } | 275 | } | ||
| 257 | - | if r.routeToHTTPS(peeked.conn) { | 276 | + | if r.routeToHTTPS(tracked, peeked.conn) { |
| 258 | r.counters.totalAccepted.Add(1) | 277 | r.counters.totalAccepted.Add(1) | ||
| 259 | - | return | 278 | + | return true |
| 260 | } | 279 | } | ||
| 261 | r.counters.totalRefused.Add(1) | 280 | r.counters.totalRefused.Add(1) | ||
| 262 | _ = peeked.conn.Close() | 281 | _ = peeked.conn.Close() | ||
| 282 | + | return false | |||
| 263 | } | 283 | } | ||
| 264 | 284 | ||||
| 265 | func (r *entryPointRuntime) peekTLSClientHello(client net.Conn, options domain.TCPOptions) (peekedTLSConn, error) { | 285 | func (r *entryPointRuntime) peekTLSClientHello(client net.Conn, options domain.TCPOptions) (peekedTLSConn, error) { | ||
| @@ -623,6 +643,7 @@ func (r *entryPointRuntime) updateEntryPoint(entryPoint domain.EntryPoint, trust | |||||
| 623 | r.mu.Unlock() | 643 | r.mu.Unlock() | ||
| 624 | for _, conn := range stale { | 644 | for _, conn := range stale { | ||
| 625 | conn.close() | 645 | conn.close() | ||
| 646 | + | r.untrack(conn) | |||
| 626 | } | 647 | } | ||
| 627 | if previousProtocol == domain.EntryPointProtocolTLSMux && entryPoint.Protocol != domain.EntryPointProtocolTLSMux { | 648 | if previousProtocol == domain.EntryPointProtocolTLSMux && entryPoint.Protocol != domain.EntryPointProtocolTLSMux { | ||
| 628 | r.stopTLSHTTPServer(r.ctx, 0) | 649 | r.stopTLSHTTPServer(r.ctx, 0) | ||
M internal/adapters/in/traffic/tcp_test.go +39 -0
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "context" | |
| 6 | 6 | "crypto/tls" | |
| 7 | 7 | "errors" | |
| 8 | + | "fmt" | |
| 8 | 9 | "io" | |
| 9 | 10 | "net" | |
| 10 | 11 | "net/http" | |
| @@ -213,6 +214,44 @@ func TestTCPPassthroughReloadAppliesTrustedCIDRChange(t *testing.T) { | |||
| 213 | 214 | assertTCPRejected(t, manager, graph.EntryPoints[0].Address, 1) | |
| 214 | 215 | } | |
| 215 | 216 | ||
| 217 | + | func TestTLSMuxReloadClosesActiveUntrustedHTTPSFallbackConnections(t *testing.T) { | |
| 218 | + | graph := domain.TrafficGraph{ | |
| 219 | + | Options: domain.TrafficOptions{TCP: domain.TCPOptions{DialTimeout: time.Second, IdleTimeout: time.Minute, DrainTimeout: time.Second}}, | |
| 220 | + | EntryPoints: []domain.EntryPoint{{Name: "websecure", Address: freeTCPAddress(t), Protocol: domain.EntryPointProtocolTLSMux, TrustedCIDRs: []string{"127.0.0.0/8"}}}, | |
| 221 | + | } | |
| 222 | + | manager := NewManager() | |
| 223 | + | manager.SetTLSHTTPServer("websecure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| 224 | + | body := "secure" | |
| 225 | + | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body))) | |
| 226 | + | _, _ = w.Write([]byte(body)) | |
| 227 | + | }), testTLSConfig(t, "app.example.com")) | |
| 228 | + | require.NoError(t, manager.Apply(context.Background(), &graph)) | |
| 229 | + | defer shutdownManager(t, manager) | |
| 230 | + | ||
| 231 | + | conn, err := tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", graph.EntryPoints[0].Address, &tls.Config{ServerName: "app.example.com", InsecureSkipVerify: true}) | |
| 232 | + | require.NoError(t, err) | |
| 233 | + | defer conn.Close() | |
| 234 | + | reader := bufio.NewReader(conn) | |
| 235 | + | _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: app.example.com\r\nConnection: keep-alive\r\n\r\n")) | |
| 236 | + | require.NoError(t, err) | |
| 237 | + | resp, err := http.ReadResponse(reader, nil) | |
| 238 | + | require.NoError(t, err) | |
| 239 | + | _, err = io.ReadAll(resp.Body) | |
| 240 | + | require.NoError(t, err) | |
| 241 | + | require.NoError(t, resp.Body.Close()) | |
| 242 | + | ||
| 243 | + | updated := graph | |
| 244 | + | updated.EntryPoints[0].TrustedCIDRs = []string{"192.0.2.0/24"} | |
| 245 | + | require.NoError(t, manager.Apply(context.Background(), &updated)) | |
| 246 | + | ||
| 247 | + | _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: app.example.com\r\nConnection: close\r\n\r\n")) | |
| 248 | + | if err == nil { | |
| 249 | + | _ = conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) | |
| 250 | + | _, err = http.ReadResponse(reader, nil) | |
| 251 | + | } | |
| 252 | + | require.Error(t, err) | |
| 253 | + | } | |
| 254 | + | ||
| 216 | 255 | func TestTCPPassthroughReloadClosesActiveUntrustedConnections(t *testing.T) { | |
| 217 | 256 | backend := startTCPEchoServer(t, 0) | |
| 218 | 257 | graph := tcpGraph(t, freeTCPAddress(t), backend.address) | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "context" | 5 | "context" | ||
| 6 | "crypto/tls" | 6 | "crypto/tls" | ||
| 7 | "errors" | 7 | "errors" | ||
| 8 | + | "fmt" | |||
| 8 | "io" | 9 | "io" | ||
| 9 | "net" | 10 | "net" | ||
| 10 | "net/http" | 11 | "net/http" | ||
| @@ -213,6 +214,44 @@ func TestTCPPassthroughReloadAppliesTrustedCIDRChange(t *testing.T) { | |||||
| 213 | assertTCPRejected(t, manager, graph.EntryPoints[0].Address, 1) | 214 | assertTCPRejected(t, manager, graph.EntryPoints[0].Address, 1) | ||
| 214 | } | 215 | } | ||
| 215 | 216 | ||||
| 217 | + | func TestTLSMuxReloadClosesActiveUntrustedHTTPSFallbackConnections(t *testing.T) { | |||
| 218 | + | graph := domain.TrafficGraph{ | |||
| 219 | + | Options: domain.TrafficOptions{TCP: domain.TCPOptions{DialTimeout: time.Second, IdleTimeout: time.Minute, DrainTimeout: time.Second}}, | |||
| 220 | + | EntryPoints: []domain.EntryPoint{{Name: "websecure", Address: freeTCPAddress(t), Protocol: domain.EntryPointProtocolTLSMux, TrustedCIDRs: []string{"127.0.0.0/8"}}}, | |||
| 221 | + | } | |||
| 222 | + | manager := NewManager() | |||
| 223 | + | manager.SetTLSHTTPServer("websecure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |||
| 224 | + | body := "secure" | |||
| 225 | + | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body))) | |||
| 226 | + | _, _ = w.Write([]byte(body)) | |||
| 227 | + | }), testTLSConfig(t, "app.example.com")) | |||
| 228 | + | require.NoError(t, manager.Apply(context.Background(), &graph)) | |||
| 229 | + | defer shutdownManager(t, manager) | |||
| 230 | + | ||||
| 231 | + | conn, err := tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", graph.EntryPoints[0].Address, &tls.Config{ServerName: "app.example.com", InsecureSkipVerify: true}) | |||
| 232 | + | require.NoError(t, err) | |||
| 233 | + | defer conn.Close() | |||
| 234 | + | reader := bufio.NewReader(conn) | |||
| 235 | + | _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: app.example.com\r\nConnection: keep-alive\r\n\r\n")) | |||
| 236 | + | require.NoError(t, err) | |||
| 237 | + | resp, err := http.ReadResponse(reader, nil) | |||
| 238 | + | require.NoError(t, err) | |||
| 239 | + | _, err = io.ReadAll(resp.Body) | |||
| 240 | + | require.NoError(t, err) | |||
| 241 | + | require.NoError(t, resp.Body.Close()) | |||
| 242 | + | ||||
| 243 | + | updated := graph | |||
| 244 | + | updated.EntryPoints[0].TrustedCIDRs = []string{"192.0.2.0/24"} | |||
| 245 | + | require.NoError(t, manager.Apply(context.Background(), &updated)) | |||
| 246 | + | ||||
| 247 | + | _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: app.example.com\r\nConnection: close\r\n\r\n")) | |||
| 248 | + | if err == nil { | |||
| 249 | + | _ = conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) | |||
| 250 | + | _, err = http.ReadResponse(reader, nil) | |||
| 251 | + | } | |||
| 252 | + | require.Error(t, err) | |||
| 253 | + | } | |||
| 254 | + | ||||
| 216 | func TestTCPPassthroughReloadClosesActiveUntrustedConnections(t *testing.T) { | 255 | func TestTCPPassthroughReloadClosesActiveUntrustedConnections(t *testing.T) { | ||
| 217 | backend := startTCPEchoServer(t, 0) | 256 | backend := startTCPEchoServer(t, 0) | ||
| 218 | graph := tcpGraph(t, freeTCPAddress(t), backend.address) | 257 | graph := tcpGraph(t, freeTCPAddress(t), backend.address) | ||
M internal/app/run.go +94 -52
| @@ -261,44 +261,46 @@ type Config struct { | |||
| 261 | 261 | ||
| 262 | 262 | // services holds all the services used by the application. | |
| 263 | 263 | type services struct { | |
| 264 | - | runtime *docker.Runtime | |
| 265 | - | eventBus *eventbus.InMemory | |
| 266 | - | blobStorage *filesystem.BlobStorage | |
| 267 | - | manifestStorage *filesystem.ManifestStorage | |
| 268 | - | backupStorage *filesystem.BackupStorage | |
| 269 | - | volumeBackupStore out.VolumeBackupStorage | |
| 270 | - | volumeBackupCfg domain.VolumeBackupConfig | |
| 271 | - | envLoader out.EnvLoader | |
| 272 | - | logWriter *logwriter.LogWriter | |
| 273 | - | tokenStore out.TokenStore | |
| 274 | - | configSvc *config.Service | |
| 275 | - | secretSvc *secretsSvc.Service | |
| 276 | - | containerSvc *container.Service | |
| 277 | - | backupSvc *backup.Service | |
| 278 | - | volumeBackupSvc *backup.VolumeService | |
| 279 | - | registrySvc *registrySvc.Service | |
| 280 | - | healthSvc *health.Service | |
| 281 | - | logSvc *logs.Service | |
| 282 | - | imageSvc *images.Service | |
| 283 | - | volumeSvc *volumesSvc.Service | |
| 284 | - | proxySvc *proxy.Service | |
| 285 | - | authSvc *auth.Service | |
| 286 | - | authHandler *authhandler.Handler | |
| 287 | - | adminHandler *admin.Handler | |
| 288 | - | internalRegUser string | |
| 289 | - | internalRegPass string | |
| 290 | - | previewStore *filesystem.PreviewStore | |
| 291 | - | previewService *preview.Service | |
| 292 | - | envDir string | |
| 293 | - | maxBlobChunkSize int64 | |
| 294 | - | maxBlobSize int64 | |
| 295 | - | caAdapter *pkiadapter.CA | |
| 296 | - | pkiSvc *pkiusecase.Service | |
| 297 | - | reloadCoordinator *reloadCoordinator | |
| 298 | - | publicTLSSvc in.PublicTLSService | |
| 299 | - | publicTLSRuntime publicTLSRuntime | |
| 300 | - | trafficManager *trafficadapter.Manager | |
| 301 | - | registryHandler interface { | |
| 264 | + | runtime *docker.Runtime | |
| 265 | + | eventBus *eventbus.InMemory | |
| 266 | + | blobStorage *filesystem.BlobStorage | |
| 267 | + | manifestStorage *filesystem.ManifestStorage | |
| 268 | + | backupStorage *filesystem.BackupStorage | |
| 269 | + | volumeBackupStore out.VolumeBackupStorage | |
| 270 | + | volumeBackupCfg domain.VolumeBackupConfig | |
| 271 | + | envLoader out.EnvLoader | |
| 272 | + | logWriter *logwriter.LogWriter | |
| 273 | + | tokenStore out.TokenStore | |
| 274 | + | configSvc *config.Service | |
| 275 | + | secretSvc *secretsSvc.Service | |
| 276 | + | containerSvc *container.Service | |
| 277 | + | backupSvc *backup.Service | |
| 278 | + | volumeBackupSvc *backup.VolumeService | |
| 279 | + | registrySvc *registrySvc.Service | |
| 280 | + | healthSvc *health.Service | |
| 281 | + | logSvc *logs.Service | |
| 282 | + | imageSvc *images.Service | |
| 283 | + | volumeSvc *volumesSvc.Service | |
| 284 | + | proxySvc *proxy.Service | |
| 285 | + | authSvc *auth.Service | |
| 286 | + | authHandler *authhandler.Handler | |
| 287 | + | adminHandler *admin.Handler | |
| 288 | + | httpsProxyHandler http.Handler | |
| 289 | + | internalRegUser string | |
| 290 | + | internalRegPass string | |
| 291 | + | previewStore *filesystem.PreviewStore | |
| 292 | + | previewService *preview.Service | |
| 293 | + | envDir string | |
| 294 | + | maxBlobChunkSize int64 | |
| 295 | + | maxBlobSize int64 | |
| 296 | + | caAdapter *pkiadapter.CA | |
| 297 | + | pkiSvc *pkiusecase.Service | |
| 298 | + | reloadCoordinator *reloadCoordinator | |
| 299 | + | publicTLSSvc in.PublicTLSService | |
| 300 | + | publicTLSRuntime publicTLSRuntime | |
| 301 | + | trafficManager *trafficadapter.Manager | |
| 302 | + | tlsHTTPEntryPoints map[string]struct{} | |
| 303 | + | registryHandler interface { | |
| 302 | 304 | UpdateBlobLimits(maxBlobChunkSize, maxBlobSize int64) | |
| 303 | 305 | } | |
| 304 | 306 | } | |
| @@ -766,6 +768,15 @@ func (si *serviceInit) registerReloadCoordinatorHooks() { | |||
| 766 | 768 | if err != nil { | |
| 767 | 769 | return err | |
| 768 | 770 | } | |
| 771 | + | var tlsConfig *tls.Config | |
| 772 | + | if reloadCfg.Server.TLSPort != 0 && si.svc.httpsProxyHandler != nil { | |
| 773 | + | var tlsErr error | |
| 774 | + | tlsConfig, tlsErr = proxyTLSConfig(reloadCfg, si.svc.pkiSvc, si.svc.publicTLSSvc, si.log) | |
| 775 | + | if tlsErr != nil { | |
| 776 | + | return tlsErr | |
| 777 | + | } | |
| 778 | + | } | |
| 779 | + | si.svc.tlsHTTPEntryPoints = registerTLSMuxHTTPServers(si.svc.trafficManager, reloadCfg, si.svc.httpsProxyHandler, tlsConfig, si.svc.tlsHTTPEntryPoints) | |
| 769 | 780 | if err := applyTrafficRuntimeConfig(reloadCtx, si.svc.trafficManager, reloadCfg, si.svc.configSvc); err != nil { | |
| 770 | 781 | return err | |
| 771 | 782 | } | |
| @@ -2455,6 +2466,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWr | |||
| 2455 | 2466 | proxyOut = middleware.AccessLogger(accessWriter, excludeHC, log, trustedNets)(proxyOut) | |
| 2456 | 2467 | httpsOut = middleware.AccessLogger(accessWriter, excludeHC, log, trustedNets)(httpsOut) | |
| 2457 | 2468 | } | |
| 2469 | + | svc.httpsProxyHandler = httpsOut | |
| 2458 | 2470 | ||
| 2459 | 2471 | return registryOut, proxyOut, httpsOut | |
| 2460 | 2472 | } | |
| @@ -2825,6 +2837,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services, | |||
| 2825 | 2837 | closeStarted(registrySrv) | |
| 2826 | 2838 | return err | |
| 2827 | 2839 | } | |
| 2840 | + | svc.tlsHTTPEntryPoints = tlsMuxHTTPServerNames(cfg) | |
| 2828 | 2841 | ||
| 2829 | 2842 | // Wait for the registry and HTTP proxy to bind before applying the traffic graph. | |
| 2830 | 2843 | // This prevents auto-start races while keeping the TLS mux under one owner. | |
| @@ -3301,13 +3314,24 @@ func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSv | |||
| 3301 | 3314 | return httpSrv, httpReady, nil, nil, err | |
| 3302 | 3315 | } | |
| 3303 | 3316 | ||
| 3304 | - | // Load static cert into a local slice (not tls.Config.Certificates) so the | |
| 3305 | - | // certificate selector has full control over priority ordering. | |
| 3317 | + | tlsConfig, err := proxyTLSConfig(cfg, pkiSvc, publicTLS, log) | |
| 3318 | + | if err != nil { | |
| 3319 | + | return cleanupHTTP(err) | |
| 3320 | + | } | |
| 3321 | + | if trafficManager == nil { | |
| 3322 | + | return cleanupHTTP(fmt.Errorf("traffic manager is required when server.tls_port is enabled")) | |
| 3323 | + | } | |
| 3324 | + | registerTLSMuxHTTPServers(trafficManager, cfg, httpsHandler, tlsConfig, nil) | |
| 3325 | + | ||
| 3326 | + | return httpSrv, httpReady, nil, nil, nil | |
| 3327 | + | } | |
| 3328 | + | ||
| 3329 | + | func proxyTLSConfig(cfg Config, pkiSvc *pkiusecase.Service, publicTLS in.PublicTLSService, log zerowrap.Logger) (*tls.Config, error) { | |
| 3306 | 3330 | var staticCerts []tls.Certificate | |
| 3307 | 3331 | if cfg.Server.TLSCertFile != "" { | |
| 3308 | 3332 | staticCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCertFile, cfg.Server.TLSKeyFile) | |
| 3309 | 3333 | if err != nil { | |
| 3310 | - | return cleanupHTTP(fmt.Errorf("load TLS keypair: %w", err)) | |
| 3334 | + | return nil, fmt.Errorf("load TLS keypair: %w", err) | |
| 3311 | 3335 | } | |
| 3312 | 3336 | staticCerts = []tls.Certificate{staticCert} | |
| 3313 | 3337 | log.Info(). | |
| @@ -3315,32 +3339,50 @@ func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSv | |||
| 3315 | 3339 | Str("key", cfg.Server.TLSKeyFile). | |
| 3316 | 3340 | Msg("loaded static TLS certificate (public ACME and internal CA handle remaining domains)") | |
| 3317 | 3341 | } | |
| 3318 | - | ||
| 3319 | 3342 | selector := &certificateSelector{ | |
| 3320 | 3343 | staticCerts: prepareStaticTLSCertificates(staticCerts), | |
| 3321 | 3344 | publicTLS: publicTLS, | |
| 3322 | 3345 | localPKI: pkiSvc, | |
| 3323 | 3346 | } | |
| 3324 | - | ||
| 3325 | - | // HTTPS listener — static cert (if provided) takes priority by SNI match via | |
| 3326 | - | // certificateSelector, then public ACME TLS, then on-demand internal CA certs. | |
| 3327 | - | tlsConfig := &tls.Config{ | |
| 3347 | + | return &tls.Config{ | |
| 3328 | 3348 | MinVersion: tls.VersionTLS12, | |
| 3329 | 3349 | GetCertificate: selector.GetCertificate, | |
| 3330 | 3350 | NextProtos: []string{"h2", "http/1.1"}, | |
| 3351 | + | }, nil | |
| 3352 | + | } | |
| 3353 | + | ||
| 3354 | + | func registerTLSMuxHTTPServers(manager *trafficadapter.Manager, cfg Config, httpsHandler http.Handler, tlsConfig *tls.Config, previous map[string]struct{}) map[string]struct{} { | |
| 3355 | + | if manager == nil { | |
| 3356 | + | return previous | |
| 3331 | 3357 | } | |
| 3332 | - | if trafficManager == nil { | |
| 3333 | - | return cleanupHTTP(fmt.Errorf("traffic manager is required when server.tls_port is enabled")) | |
| 3358 | + | next := tlsMuxHTTPServerNames(cfg) | |
| 3359 | + | for name := range previous { | |
| 3360 | + | if _, ok := next[name]; !ok { | |
| 3361 | + | manager.SetTLSHTTPServer(name, nil, nil) | |
| 3362 | + | } | |
| 3363 | + | } | |
| 3364 | + | if cfg.Server.TLSPort == 0 || httpsHandler == nil || tlsConfig == nil { | |
| 3365 | + | return next | |
| 3334 | 3366 | } | |
| 3335 | - | trafficManager.SetTLSHTTPServer(traffic.DefaultTLSEntryPointName, httpsHandler, tlsConfig) | |
| 3367 | + | for name := range next { | |
| 3368 | + | manager.SetTLSHTTPServer(name, httpsHandler, tlsConfig) | |
| 3369 | + | } | |
| 3370 | + | return next | |
| 3371 | + | } | |
| 3372 | + | ||
| 3373 | + | func tlsMuxHTTPServerNames(cfg Config) map[string]struct{} { | |
| 3374 | + | names := map[string]struct{}{} | |
| 3375 | + | if cfg.Server.TLSPort == 0 { | |
| 3376 | + | return names | |
| 3377 | + | } | |
| 3378 | + | names[traffic.DefaultTLSEntryPointName] = struct{}{} | |
| 3336 | 3379 | for name, entryPoint := range cfg.EntryPoints { | |
| 3337 | 3380 | domainEntryPoint := domain.EntryPoint{Name: name, Protocol: entryPoint.Protocol} | |
| 3338 | 3381 | if trafficManagerOwnsEntryPoint(domainEntryPoint) && domainEntryPoint.Protocol == domain.EntryPointProtocolTLSMux { | |
| 3339 | - | trafficManager.SetTLSHTTPServer(name, httpsHandler, tlsConfig) | |
| 3382 | + | names[name] = struct{}{} | |
| 3340 | 3383 | } | |
| 3341 | 3384 | } | |
| 3342 | - | ||
| 3343 | - | return httpSrv, httpReady, nil, nil, nil | |
| 3385 | + | return names | |
| 3344 | 3386 | } | |
| 3345 | 3387 | ||
| 3346 | 3388 | // startServer starts an HTTP server, returning the server instance and a channel | |
| @@ -261,44 +261,46 @@ type Config struct { | |||||
| 261 | 261 | ||||
| 262 | // services holds all the services used by the application. | 262 | // services holds all the services used by the application. | ||
| 263 | type services struct { | 263 | type services struct { | ||
| 264 | - | runtime *docker.Runtime | 264 | + | runtime *docker.Runtime |
| 265 | - | eventBus *eventbus.InMemory | 265 | + | eventBus *eventbus.InMemory |
| 266 | - | blobStorage *filesystem.BlobStorage | 266 | + | blobStorage *filesystem.BlobStorage |
| 267 | - | manifestStorage *filesystem.ManifestStorage | 267 | + | manifestStorage *filesystem.ManifestStorage |
| 268 | - | backupStorage *filesystem.BackupStorage | 268 | + | backupStorage *filesystem.BackupStorage |
| 269 | - | volumeBackupStore out.VolumeBackupStorage | 269 | + | volumeBackupStore out.VolumeBackupStorage |
| 270 | - | volumeBackupCfg domain.VolumeBackupConfig | 270 | + | volumeBackupCfg domain.VolumeBackupConfig |
| 271 | - | envLoader out.EnvLoader | 271 | + | envLoader out.EnvLoader |
| 272 | - | logWriter *logwriter.LogWriter | 272 | + | logWriter *logwriter.LogWriter |
| 273 | - | tokenStore out.TokenStore | 273 | + | tokenStore out.TokenStore |
| 274 | - | configSvc *config.Service | 274 | + | configSvc *config.Service |
| 275 | - | secretSvc *secretsSvc.Service | 275 | + | secretSvc *secretsSvc.Service |
| 276 | - | containerSvc *container.Service | 276 | + | containerSvc *container.Service |
| 277 | - | backupSvc *backup.Service | 277 | + | backupSvc *backup.Service |
| 278 | - | volumeBackupSvc *backup.VolumeService | 278 | + | volumeBackupSvc *backup.VolumeService |
| 279 | - | registrySvc *registrySvc.Service | 279 | + | registrySvc *registrySvc.Service |
| 280 | - | healthSvc *health.Service | 280 | + | healthSvc *health.Service |
| 281 | - | logSvc *logs.Service | 281 | + | logSvc *logs.Service |
| 282 | - | imageSvc *images.Service | 282 | + | imageSvc *images.Service |
| 283 | - | volumeSvc *volumesSvc.Service | 283 | + | volumeSvc *volumesSvc.Service |
| 284 | - | proxySvc *proxy.Service | 284 | + | proxySvc *proxy.Service |
| 285 | - | authSvc *auth.Service | 285 | + | authSvc *auth.Service |
| 286 | - | authHandler *authhandler.Handler | 286 | + | authHandler *authhandler.Handler |
| 287 | - | adminHandler *admin.Handler | 287 | + | adminHandler *admin.Handler |
| 288 | - | internalRegUser string | 288 | + | httpsProxyHandler http.Handler |
| 289 | - | internalRegPass string | 289 | + | internalRegUser string |
| 290 | - | previewStore *filesystem.PreviewStore | 290 | + | internalRegPass string |
| 291 | - | previewService *preview.Service | 291 | + | previewStore *filesystem.PreviewStore |
| 292 | - | envDir string | 292 | + | previewService *preview.Service |
| 293 | - | maxBlobChunkSize int64 | 293 | + | envDir string |
| 294 | - | maxBlobSize int64 | 294 | + | maxBlobChunkSize int64 |
| 295 | - | caAdapter *pkiadapter.CA | 295 | + | maxBlobSize int64 |
| 296 | - | pkiSvc *pkiusecase.Service | 296 | + | caAdapter *pkiadapter.CA |
| 297 | - | reloadCoordinator *reloadCoordinator | 297 | + | pkiSvc *pkiusecase.Service |
| 298 | - | publicTLSSvc in.PublicTLSService | 298 | + | reloadCoordinator *reloadCoordinator |
| 299 | - | publicTLSRuntime publicTLSRuntime | 299 | + | publicTLSSvc in.PublicTLSService |
| 300 | - | trafficManager *trafficadapter.Manager | 300 | + | publicTLSRuntime publicTLSRuntime |
| 301 | - | registryHandler interface { | 301 | + | trafficManager *trafficadapter.Manager |
| 302 | + | tlsHTTPEntryPoints map[string]struct{} | |||
| 303 | + | registryHandler interface { | |||
| 302 | UpdateBlobLimits(maxBlobChunkSize, maxBlobSize int64) | 304 | UpdateBlobLimits(maxBlobChunkSize, maxBlobSize int64) | ||
| 303 | } | 305 | } | ||
| 304 | } | 306 | } | ||
| @@ -766,6 +768,15 @@ func (si *serviceInit) registerReloadCoordinatorHooks() { | |||||
| 766 | if err != nil { | 768 | if err != nil { | ||
| 767 | return err | 769 | return err | ||
| 768 | } | 770 | } | ||
| 771 | + | var tlsConfig *tls.Config | |||
| 772 | + | if reloadCfg.Server.TLSPort != 0 && si.svc.httpsProxyHandler != nil { | |||
| 773 | + | var tlsErr error | |||
| 774 | + | tlsConfig, tlsErr = proxyTLSConfig(reloadCfg, si.svc.pkiSvc, si.svc.publicTLSSvc, si.log) | |||
| 775 | + | if tlsErr != nil { | |||
| 776 | + | return tlsErr | |||
| 777 | + | } | |||
| 778 | + | } | |||
| 779 | + | si.svc.tlsHTTPEntryPoints = registerTLSMuxHTTPServers(si.svc.trafficManager, reloadCfg, si.svc.httpsProxyHandler, tlsConfig, si.svc.tlsHTTPEntryPoints) | |||
| 769 | if err := applyTrafficRuntimeConfig(reloadCtx, si.svc.trafficManager, reloadCfg, si.svc.configSvc); err != nil { | 780 | if err := applyTrafficRuntimeConfig(reloadCtx, si.svc.trafficManager, reloadCfg, si.svc.configSvc); err != nil { | ||
| 770 | return err | 781 | return err | ||
| 771 | } | 782 | } | ||
| @@ -2455,6 +2466,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWr | |||||
| 2455 | proxyOut = middleware.AccessLogger(accessWriter, excludeHC, log, trustedNets)(proxyOut) | 2466 | proxyOut = middleware.AccessLogger(accessWriter, excludeHC, log, trustedNets)(proxyOut) | ||
| 2456 | httpsOut = middleware.AccessLogger(accessWriter, excludeHC, log, trustedNets)(httpsOut) | 2467 | httpsOut = middleware.AccessLogger(accessWriter, excludeHC, log, trustedNets)(httpsOut) | ||
| 2457 | } | 2468 | } | ||
| 2469 | + | svc.httpsProxyHandler = httpsOut | |||
| 2458 | 2470 | ||||
| 2459 | return registryOut, proxyOut, httpsOut | 2471 | return registryOut, proxyOut, httpsOut | ||
| 2460 | } | 2472 | } | ||
| @@ -2825,6 +2837,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services, | |||||
| 2825 | closeStarted(registrySrv) | 2837 | closeStarted(registrySrv) | ||
| 2826 | return err | 2838 | return err | ||
| 2827 | } | 2839 | } | ||
| 2840 | + | svc.tlsHTTPEntryPoints = tlsMuxHTTPServerNames(cfg) | |||
| 2828 | 2841 | ||||
| 2829 | // Wait for the registry and HTTP proxy to bind before applying the traffic graph. | 2842 | // Wait for the registry and HTTP proxy to bind before applying the traffic graph. | ||
| 2830 | // This prevents auto-start races while keeping the TLS mux under one owner. | 2843 | // This prevents auto-start races while keeping the TLS mux under one owner. | ||
| @@ -3301,13 +3314,24 @@ func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSv | |||||
| 3301 | return httpSrv, httpReady, nil, nil, err | 3314 | return httpSrv, httpReady, nil, nil, err | ||
| 3302 | } | 3315 | } | ||
| 3303 | 3316 | ||||
| 3304 | - | // Load static cert into a local slice (not tls.Config.Certificates) so the | 3317 | + | tlsConfig, err := proxyTLSConfig(cfg, pkiSvc, publicTLS, log) |
| 3305 | - | // certificate selector has full control over priority ordering. | 3318 | + | if err != nil { |
| 3319 | + | return cleanupHTTP(err) | |||
| 3320 | + | } | |||
| 3321 | + | if trafficManager == nil { | |||
| 3322 | + | return cleanupHTTP(fmt.Errorf("traffic manager is required when server.tls_port is enabled")) | |||
| 3323 | + | } | |||
| 3324 | + | registerTLSMuxHTTPServers(trafficManager, cfg, httpsHandler, tlsConfig, nil) | |||
| 3325 | + | ||||
| 3326 | + | return httpSrv, httpReady, nil, nil, nil | |||
| 3327 | + | } | |||
| 3328 | + | ||||
| 3329 | + | func proxyTLSConfig(cfg Config, pkiSvc *pkiusecase.Service, publicTLS in.PublicTLSService, log zerowrap.Logger) (*tls.Config, error) { | |||
| 3306 | var staticCerts []tls.Certificate | 3330 | var staticCerts []tls.Certificate | ||
| 3307 | if cfg.Server.TLSCertFile != "" { | 3331 | if cfg.Server.TLSCertFile != "" { | ||
| 3308 | staticCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCertFile, cfg.Server.TLSKeyFile) | 3332 | staticCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCertFile, cfg.Server.TLSKeyFile) | ||
| 3309 | if err != nil { | 3333 | if err != nil { | ||
| 3310 | - | return cleanupHTTP(fmt.Errorf("load TLS keypair: %w", err)) | 3334 | + | return nil, fmt.Errorf("load TLS keypair: %w", err) |
| 3311 | } | 3335 | } | ||
| 3312 | staticCerts = []tls.Certificate{staticCert} | 3336 | staticCerts = []tls.Certificate{staticCert} | ||
| 3313 | log.Info(). | 3337 | log.Info(). | ||
| @@ -3315,32 +3339,50 @@ func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSv | |||||
| 3315 | Str("key", cfg.Server.TLSKeyFile). | 3339 | Str("key", cfg.Server.TLSKeyFile). | ||
| 3316 | Msg("loaded static TLS certificate (public ACME and internal CA handle remaining domains)") | 3340 | Msg("loaded static TLS certificate (public ACME and internal CA handle remaining domains)") | ||
| 3317 | } | 3341 | } | ||
| 3318 | - | ||||
| 3319 | selector := &certificateSelector{ | 3342 | selector := &certificateSelector{ | ||
| 3320 | staticCerts: prepareStaticTLSCertificates(staticCerts), | 3343 | staticCerts: prepareStaticTLSCertificates(staticCerts), | ||
| 3321 | publicTLS: publicTLS, | 3344 | publicTLS: publicTLS, | ||
| 3322 | localPKI: pkiSvc, | 3345 | localPKI: pkiSvc, | ||
| 3323 | } | 3346 | } | ||
| 3324 | - | 3347 | + | return &tls.Config{ | |
| 3325 | - | // HTTPS listener — static cert (if provided) takes priority by SNI match via | |||
| 3326 | - | // certificateSelector, then public ACME TLS, then on-demand internal CA certs. | |||
| 3327 | - | tlsConfig := &tls.Config{ | |||
| 3328 | MinVersion: tls.VersionTLS12, | 3348 | MinVersion: tls.VersionTLS12, | ||
| 3329 | GetCertificate: selector.GetCertificate, | 3349 | GetCertificate: selector.GetCertificate, | ||
| 3330 | NextProtos: []string{"h2", "http/1.1"}, | 3350 | NextProtos: []string{"h2", "http/1.1"}, | ||
| 3351 | + | }, nil | |||
| 3352 | + | } | |||
| 3353 | + | ||||
| 3354 | + | func registerTLSMuxHTTPServers(manager *trafficadapter.Manager, cfg Config, httpsHandler http.Handler, tlsConfig *tls.Config, previous map[string]struct{}) map[string]struct{} { | |||
| 3355 | + | if manager == nil { | |||
| 3356 | + | return previous | |||
| 3331 | } | 3357 | } | ||
| 3332 | - | if trafficManager == nil { | 3358 | + | next := tlsMuxHTTPServerNames(cfg) |
| 3333 | - | return cleanupHTTP(fmt.Errorf("traffic manager is required when server.tls_port is enabled")) | 3359 | + | for name := range previous { |
| 3360 | + | if _, ok := next[name]; !ok { | |||
| 3361 | + | manager.SetTLSHTTPServer(name, nil, nil) | |||
| 3362 | + | } | |||
| 3363 | + | } | |||
| 3364 | + | if cfg.Server.TLSPort == 0 || httpsHandler == nil || tlsConfig == nil { | |||
| 3365 | + | return next | |||
| 3334 | } | 3366 | } | ||
| 3335 | - | trafficManager.SetTLSHTTPServer(traffic.DefaultTLSEntryPointName, httpsHandler, tlsConfig) | 3367 | + | for name := range next { |
| 3368 | + | manager.SetTLSHTTPServer(name, httpsHandler, tlsConfig) | |||
| 3369 | + | } | |||
| 3370 | + | return next | |||
| 3371 | + | } | |||
| 3372 | + | ||||
| 3373 | + | func tlsMuxHTTPServerNames(cfg Config) map[string]struct{} { | |||
| 3374 | + | names := map[string]struct{}{} | |||
| 3375 | + | if cfg.Server.TLSPort == 0 { | |||
| 3376 | + | return names | |||
| 3377 | + | } | |||
| 3378 | + | names[traffic.DefaultTLSEntryPointName] = struct{}{} | |||
| 3336 | for name, entryPoint := range cfg.EntryPoints { | 3379 | for name, entryPoint := range cfg.EntryPoints { | ||
| 3337 | domainEntryPoint := domain.EntryPoint{Name: name, Protocol: entryPoint.Protocol} | 3380 | domainEntryPoint := domain.EntryPoint{Name: name, Protocol: entryPoint.Protocol} | ||
| 3338 | if trafficManagerOwnsEntryPoint(domainEntryPoint) && domainEntryPoint.Protocol == domain.EntryPointProtocolTLSMux { | 3381 | if trafficManagerOwnsEntryPoint(domainEntryPoint) && domainEntryPoint.Protocol == domain.EntryPointProtocolTLSMux { | ||
| 3339 | - | trafficManager.SetTLSHTTPServer(name, httpsHandler, tlsConfig) | 3382 | + | names[name] = struct{}{} |
| 3340 | } | 3383 | } | ||
| 3341 | } | 3384 | } | ||
| 3342 | - | 3385 | + | return names | |
| 3343 | - | return httpSrv, httpReady, nil, nil, nil | |||
| 3344 | } | 3386 | } | ||
| 3345 | 3387 | ||||
| 3346 | // startServer starts an HTTP server, returning the server instance and a channel | 3388 | // startServer starts an HTTP server, returning the server instance and a channel | ||
M internal/app/run_reload_test.go +56 -0
| @@ -3,6 +3,9 @@ package app | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "errors" | |
| 6 | + | "net" | |
| 7 | + | "net/http" | |
| 8 | + | "strconv" | |
| 6 | 9 | "sync" | |
| 7 | 10 | "testing" | |
| 8 | 11 | "time" | |
| @@ -13,10 +16,12 @@ import ( | |||
| 13 | 16 | "github.com/stretchr/testify/require" | |
| 14 | 17 | ||
| 15 | 18 | "github.com/bnema/gordon/internal/adapters/in/http/registry" | |
| 19 | + | trafficadapter "github.com/bnema/gordon/internal/adapters/in/traffic" | |
| 16 | 20 | "github.com/bnema/gordon/internal/domain" | |
| 17 | 21 | cfgusecase "github.com/bnema/gordon/internal/usecase/config" | |
| 18 | 22 | "github.com/bnema/gordon/internal/usecase/container" | |
| 19 | 23 | "github.com/bnema/gordon/internal/usecase/proxy" | |
| 24 | + | traffic "github.com/bnema/gordon/internal/usecase/traffic" | |
| 20 | 25 | ) | |
| 21 | 26 | ||
| 22 | 27 | type watchRecorder struct { | |
| @@ -227,6 +232,57 @@ func TestReloadCoordinator_ApplyLoadedConfig_RebuildsProxyConfigAndPublishesEven | |||
| 227 | 232 | }, proxySvc.config) | |
| 228 | 233 | } | |
| 229 | 234 | ||
| 235 | + | func TestServiceInit_ReloadRegistersCustomTLSMuxHTTPSFallback(t *testing.T) { | |
| 236 | + | ctx := context.Background() | |
| 237 | + | v := viper.New() | |
| 238 | + | v.Set("server.gordon_domain", "reload.example.com") | |
| 239 | + | v.Set("server.registry_port", 5000) | |
| 240 | + | ||
| 241 | + | configSvc := cfgusecase.NewService(v, nil) | |
| 242 | + | require.NoError(t, configSvc.Load(ctx)) | |
| 243 | + | manager := trafficadapter.NewManager() | |
| 244 | + | defer func() { require.NoError(t, manager.Shutdown(ctx)) }() | |
| 245 | + | httpsHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| 246 | + | _, _ = w.Write([]byte("reload secure")) | |
| 247 | + | }) | |
| 248 | + | pkiSvc := newTestPKIService(t) | |
| 249 | + | ||
| 250 | + | customAddress := freeTCPAddress(t) | |
| 251 | + | si := &serviceInit{ | |
| 252 | + | ctx: ctx, | |
| 253 | + | v: v, | |
| 254 | + | cfg: Config{}, | |
| 255 | + | log: zerowrap.Default(), | |
| 256 | + | svc: &services{ | |
| 257 | + | configSvc: configSvc, | |
| 258 | + | containerSvc: container.NewService(nil, nil, nil, nil, container.Config{}, configSvc), | |
| 259 | + | internalRegUser: "gordon", | |
| 260 | + | internalRegPass: "secret", | |
| 261 | + | reloadCoordinator: newReloadCoordinator(v, &reloadRecorder{}, &proxyRecorder{}, nil, nil, nil, zerowrap.Default()), | |
| 262 | + | trafficManager: manager, | |
| 263 | + | httpsProxyHandler: httpsHandler, | |
| 264 | + | pkiSvc: pkiSvc, | |
| 265 | + | }, | |
| 266 | + | } | |
| 267 | + | si.registerReloadCoordinatorHooks() | |
| 268 | + | ||
| 269 | + | reloadCfg := Config{} | |
| 270 | + | reloadCfg.Server.GordonDomain = "reload.example.com" | |
| 271 | + | reloadCfg.Server.RegistryPort = 5000 | |
| 272 | + | reloadCfg.Server.TLSPort = freeTCPPort(t) | |
| 273 | + | reloadCfg.EntryPoints = map[string]traffic.EntryPointConfig{ | |
| 274 | + | "custom-secure": {Address: customAddress, Protocol: domain.EntryPointProtocolTLSMux}, | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | require.NoError(t, si.svc.reloadCoordinator.applyContainerConfig(ctx, reloadCfg)) | |
| 278 | + | _, portValue, err := net.SplitHostPort(customAddress) | |
| 279 | + | require.NoError(t, err) | |
| 280 | + | port, err := strconv.Atoi(portValue) | |
| 281 | + | require.NoError(t, err) | |
| 282 | + | body := httpsGetForTest(t, port, "app.example.com") | |
| 283 | + | require.Contains(t, body, "reload secure") | |
| 284 | + | } | |
| 285 | + | ||
| 230 | 286 | func TestServiceInit_RegisterReloadCoordinatorHooks_WiresContainerConfigApplier(t *testing.T) { | |
| 231 | 287 | ctx := context.Background() | |
| 232 | 288 | v := viper.New() | |
| @@ -3,6 +3,9 @@ package app | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "errors" | 5 | "errors" | ||
| 6 | + | "net" | |||
| 7 | + | "net/http" | |||
| 8 | + | "strconv" | |||
| 6 | "sync" | 9 | "sync" | ||
| 7 | "testing" | 10 | "testing" | ||
| 8 | "time" | 11 | "time" | ||
| @@ -13,10 +16,12 @@ import ( | |||||
| 13 | "github.com/stretchr/testify/require" | 16 | "github.com/stretchr/testify/require" | ||
| 14 | 17 | ||||
| 15 | "github.com/bnema/gordon/internal/adapters/in/http/registry" | 18 | "github.com/bnema/gordon/internal/adapters/in/http/registry" | ||
| 19 | + | trafficadapter "github.com/bnema/gordon/internal/adapters/in/traffic" | |||
| 16 | "github.com/bnema/gordon/internal/domain" | 20 | "github.com/bnema/gordon/internal/domain" | ||
| 17 | cfgusecase "github.com/bnema/gordon/internal/usecase/config" | 21 | cfgusecase "github.com/bnema/gordon/internal/usecase/config" | ||
| 18 | "github.com/bnema/gordon/internal/usecase/container" | 22 | "github.com/bnema/gordon/internal/usecase/container" | ||
| 19 | "github.com/bnema/gordon/internal/usecase/proxy" | 23 | "github.com/bnema/gordon/internal/usecase/proxy" | ||
| 24 | + | traffic "github.com/bnema/gordon/internal/usecase/traffic" | |||
| 20 | ) | 25 | ) | ||
| 21 | 26 | ||||
| 22 | type watchRecorder struct { | 27 | type watchRecorder struct { | ||
| @@ -227,6 +232,57 @@ func TestReloadCoordinator_ApplyLoadedConfig_RebuildsProxyConfigAndPublishesEven | |||||
| 227 | }, proxySvc.config) | 232 | }, proxySvc.config) | ||
| 228 | } | 233 | } | ||
| 229 | 234 | ||||
| 235 | + | func TestServiceInit_ReloadRegistersCustomTLSMuxHTTPSFallback(t *testing.T) { | |||
| 236 | + | ctx := context.Background() | |||
| 237 | + | v := viper.New() | |||
| 238 | + | v.Set("server.gordon_domain", "reload.example.com") | |||
| 239 | + | v.Set("server.registry_port", 5000) | |||
| 240 | + | ||||
| 241 | + | configSvc := cfgusecase.NewService(v, nil) | |||
| 242 | + | require.NoError(t, configSvc.Load(ctx)) | |||
| 243 | + | manager := trafficadapter.NewManager() | |||
| 244 | + | defer func() { require.NoError(t, manager.Shutdown(ctx)) }() | |||
| 245 | + | httpsHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |||
| 246 | + | _, _ = w.Write([]byte("reload secure")) | |||
| 247 | + | }) | |||
| 248 | + | pkiSvc := newTestPKIService(t) | |||
| 249 | + | ||||
| 250 | + | customAddress := freeTCPAddress(t) | |||
| 251 | + | si := &serviceInit{ | |||
| 252 | + | ctx: ctx, | |||
| 253 | + | v: v, | |||
| 254 | + | cfg: Config{}, | |||
| 255 | + | log: zerowrap.Default(), | |||
| 256 | + | svc: &services{ | |||
| 257 | + | configSvc: configSvc, | |||
| 258 | + | containerSvc: container.NewService(nil, nil, nil, nil, container.Config{}, configSvc), | |||
| 259 | + | internalRegUser: "gordon", | |||
| 260 | + | internalRegPass: "secret", | |||
| 261 | + | reloadCoordinator: newReloadCoordinator(v, &reloadRecorder{}, &proxyRecorder{}, nil, nil, nil, zerowrap.Default()), | |||
| 262 | + | trafficManager: manager, | |||
| 263 | + | httpsProxyHandler: httpsHandler, | |||
| 264 | + | pkiSvc: pkiSvc, | |||
| 265 | + | }, | |||
| 266 | + | } | |||
| 267 | + | si.registerReloadCoordinatorHooks() | |||
| 268 | + | ||||
| 269 | + | reloadCfg := Config{} | |||
| 270 | + | reloadCfg.Server.GordonDomain = "reload.example.com" | |||
| 271 | + | reloadCfg.Server.RegistryPort = 5000 | |||
| 272 | + | reloadCfg.Server.TLSPort = freeTCPPort(t) | |||
| 273 | + | reloadCfg.EntryPoints = map[string]traffic.EntryPointConfig{ | |||
| 274 | + | "custom-secure": {Address: customAddress, Protocol: domain.EntryPointProtocolTLSMux}, | |||
| 275 | + | } | |||
| 276 | + | ||||
| 277 | + | require.NoError(t, si.svc.reloadCoordinator.applyContainerConfig(ctx, reloadCfg)) | |||
| 278 | + | _, portValue, err := net.SplitHostPort(customAddress) | |||
| 279 | + | require.NoError(t, err) | |||
| 280 | + | port, err := strconv.Atoi(portValue) | |||
| 281 | + | require.NoError(t, err) | |||
| 282 | + | body := httpsGetForTest(t, port, "app.example.com") | |||
| 283 | + | require.Contains(t, body, "reload secure") | |||
| 284 | + | } | |||
| 285 | + | ||||
| 230 | func TestServiceInit_RegisterReloadCoordinatorHooks_WiresContainerConfigApplier(t *testing.T) { | 286 | func TestServiceInit_RegisterReloadCoordinatorHooks_WiresContainerConfigApplier(t *testing.T) { | ||
| 231 | ctx := context.Background() | 287 | ctx := context.Background() | ||
| 232 | v := viper.New() | 288 | v := viper.New() | ||