fix(traffic): serialize TLS mux server replacement
- commit
- 228f200f85175d6cbf7d0ba6a3d2611ed018f949
- author
- brice · · 2026-06-30 07:25 UTC
- parent
- 9546ebc
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +69 -4
M internal/adapters/in/traffic/tcp.go +26 -4
| @@ -35,9 +35,10 @@ type entryPointRuntime struct { | |||
| 35 | 35 | mu sync.Mutex | |
| 36 | 36 | activeConns map[*trackedTCPConn]struct{} | |
| 37 | 37 | ||
| 38 | - | tlsHTTPListener *tlsHTTPListener | |
| 39 | - | tlsHTTPServer *http.Server | |
| 40 | - | tlsHTTPDone chan struct{} | |
| 38 | + | tlsHTTPReplaceMu sync.Mutex | |
| 39 | + | tlsHTTPListener *tlsHTTPListener | |
| 40 | + | tlsHTTPServer *http.Server | |
| 41 | + | tlsHTTPDone chan struct{} | |
| 41 | 42 | } | |
| 42 | 43 | ||
| 43 | 44 | type trackedTCPConn struct { | |
| @@ -125,7 +126,12 @@ func (r *entryPointRuntime) refreshTLSHTTPServer(entryPointName string) { | |||
| 125 | 126 | } | |
| 126 | 127 | ||
| 127 | 128 | func (r *entryPointRuntime) replaceTLSHTTPServer(entryPoint domain.EntryPoint, config TLSHTTPServerConfig) { | |
| 128 | - | r.stopTLSHTTPServer(r.ctx, 0) | |
| 129 | + | r.tlsHTTPReplaceMu.Lock() | |
| 130 | + | defer r.tlsHTTPReplaceMu.Unlock() | |
| 131 | + | r.stopTLSHTTPServerLocked(r.ctx, 0) | |
| 132 | + | if r.isClosed() { | |
| 133 | + | return | |
| 134 | + | } | |
| 129 | 135 | listener := newTLSHTTPListener(r.listener.Addr()) | |
| 130 | 136 | server := &http.Server{ | |
| 131 | 137 | Handler: config.Handler, | |
| @@ -138,6 +144,11 @@ func (r *entryPointRuntime) replaceTLSHTTPServer(entryPoint domain.EntryPoint, c | |||
| 138 | 144 | done := make(chan struct{}) | |
| 139 | 145 | ||
| 140 | 146 | r.mu.Lock() | |
| 147 | + | if r.isClosed() { | |
| 148 | + | r.mu.Unlock() | |
| 149 | + | _ = listener.Close() | |
| 150 | + | return | |
| 151 | + | } | |
| 141 | 152 | r.tlsHTTPListener = listener | |
| 142 | 153 | r.tlsHTTPServer = server | |
| 143 | 154 | r.tlsHTTPDone = done | |
| @@ -519,6 +530,12 @@ func (r *entryPointRuntime) stop(ctx context.Context, drainTimeout time.Duration | |||
| 519 | 530 | } | |
| 520 | 531 | ||
| 521 | 532 | func (r *entryPointRuntime) stopTLSHTTPServer(ctx context.Context, drainTimeout time.Duration) { | |
| 533 | + | r.tlsHTTPReplaceMu.Lock() | |
| 534 | + | defer r.tlsHTTPReplaceMu.Unlock() | |
| 535 | + | r.stopTLSHTTPServerLocked(ctx, drainTimeout) | |
| 536 | + | } | |
| 537 | + | ||
| 538 | + | func (r *entryPointRuntime) stopTLSHTTPServerLocked(ctx context.Context, drainTimeout time.Duration) { | |
| 522 | 539 | r.mu.Lock() | |
| 523 | 540 | listener := r.tlsHTTPListener | |
| 524 | 541 | server := r.tlsHTTPServer | |
| @@ -593,6 +610,7 @@ func (r *entryPointRuntime) sameAddress(entryPoint domain.EntryPoint) bool { | |||
| 593 | 610 | ||
| 594 | 611 | func (r *entryPointRuntime) updateEntryPoint(entryPoint domain.EntryPoint, trusted []*net.IPNet) { | |
| 595 | 612 | r.mu.Lock() | |
| 613 | + | previousName := r.entryPoint.Name | |
| 596 | 614 | previousProtocol := r.entryPoint.Protocol | |
| 597 | 615 | r.entryPoint = entryPoint | |
| 598 | 616 | r.trusted = trusted | |
| @@ -610,6 +628,10 @@ func (r *entryPointRuntime) updateEntryPoint(entryPoint domain.EntryPoint, trust | |||
| 610 | 628 | r.stopTLSHTTPServer(r.ctx, 0) | |
| 611 | 629 | } | |
| 612 | 630 | if entryPoint.Protocol == domain.EntryPointProtocolTLSMux { | |
| 631 | + | if previousProtocol == domain.EntryPointProtocolTLSMux && previousName != entryPoint.Name { | |
| 632 | + | r.refreshTLSHTTPServer(entryPoint.Name) | |
| 633 | + | return | |
| 634 | + | } | |
| 613 | 635 | r.startTLSHTTPServer(entryPoint) | |
| 614 | 636 | } | |
| 615 | 637 | } | |
| @@ -35,9 +35,10 @@ type entryPointRuntime struct { | |||||
| 35 | mu sync.Mutex | 35 | mu sync.Mutex | ||
| 36 | activeConns map[*trackedTCPConn]struct{} | 36 | activeConns map[*trackedTCPConn]struct{} | ||
| 37 | 37 | ||||
| 38 | - | tlsHTTPListener *tlsHTTPListener | 38 | + | tlsHTTPReplaceMu sync.Mutex |
| 39 | - | tlsHTTPServer *http.Server | 39 | + | tlsHTTPListener *tlsHTTPListener |
| 40 | - | tlsHTTPDone chan struct{} | 40 | + | tlsHTTPServer *http.Server |
| 41 | + | tlsHTTPDone chan struct{} | |||
| 41 | } | 42 | } | ||
| 42 | 43 | ||||
| 43 | type trackedTCPConn struct { | 44 | type trackedTCPConn struct { | ||
| @@ -125,7 +126,12 @@ func (r *entryPointRuntime) refreshTLSHTTPServer(entryPointName string) { | |||||
| 125 | } | 126 | } | ||
| 126 | 127 | ||||
| 127 | func (r *entryPointRuntime) replaceTLSHTTPServer(entryPoint domain.EntryPoint, config TLSHTTPServerConfig) { | 128 | func (r *entryPointRuntime) replaceTLSHTTPServer(entryPoint domain.EntryPoint, config TLSHTTPServerConfig) { | ||
| 128 | - | r.stopTLSHTTPServer(r.ctx, 0) | 129 | + | r.tlsHTTPReplaceMu.Lock() |
| 130 | + | defer r.tlsHTTPReplaceMu.Unlock() | |||
| 131 | + | r.stopTLSHTTPServerLocked(r.ctx, 0) | |||
| 132 | + | if r.isClosed() { | |||
| 133 | + | return | |||
| 134 | + | } | |||
| 129 | listener := newTLSHTTPListener(r.listener.Addr()) | 135 | listener := newTLSHTTPListener(r.listener.Addr()) | ||
| 130 | server := &http.Server{ | 136 | server := &http.Server{ | ||
| 131 | Handler: config.Handler, | 137 | Handler: config.Handler, | ||
| @@ -138,6 +144,11 @@ func (r *entryPointRuntime) replaceTLSHTTPServer(entryPoint domain.EntryPoint, c | |||||
| 138 | done := make(chan struct{}) | 144 | done := make(chan struct{}) | ||
| 139 | 145 | ||||
| 140 | r.mu.Lock() | 146 | r.mu.Lock() | ||
| 147 | + | if r.isClosed() { | |||
| 148 | + | r.mu.Unlock() | |||
| 149 | + | _ = listener.Close() | |||
| 150 | + | return | |||
| 151 | + | } | |||
| 141 | r.tlsHTTPListener = listener | 152 | r.tlsHTTPListener = listener | ||
| 142 | r.tlsHTTPServer = server | 153 | r.tlsHTTPServer = server | ||
| 143 | r.tlsHTTPDone = done | 154 | r.tlsHTTPDone = done | ||
| @@ -519,6 +530,12 @@ func (r *entryPointRuntime) stop(ctx context.Context, drainTimeout time.Duration | |||||
| 519 | } | 530 | } | ||
| 520 | 531 | ||||
| 521 | func (r *entryPointRuntime) stopTLSHTTPServer(ctx context.Context, drainTimeout time.Duration) { | 532 | func (r *entryPointRuntime) stopTLSHTTPServer(ctx context.Context, drainTimeout time.Duration) { | ||
| 533 | + | r.tlsHTTPReplaceMu.Lock() | |||
| 534 | + | defer r.tlsHTTPReplaceMu.Unlock() | |||
| 535 | + | r.stopTLSHTTPServerLocked(ctx, drainTimeout) | |||
| 536 | + | } | |||
| 537 | + | ||||
| 538 | + | func (r *entryPointRuntime) stopTLSHTTPServerLocked(ctx context.Context, drainTimeout time.Duration) { | |||
| 522 | r.mu.Lock() | 539 | r.mu.Lock() | ||
| 523 | listener := r.tlsHTTPListener | 540 | listener := r.tlsHTTPListener | ||
| 524 | server := r.tlsHTTPServer | 541 | server := r.tlsHTTPServer | ||
| @@ -593,6 +610,7 @@ func (r *entryPointRuntime) sameAddress(entryPoint domain.EntryPoint) bool { | |||||
| 593 | 610 | ||||
| 594 | func (r *entryPointRuntime) updateEntryPoint(entryPoint domain.EntryPoint, trusted []*net.IPNet) { | 611 | func (r *entryPointRuntime) updateEntryPoint(entryPoint domain.EntryPoint, trusted []*net.IPNet) { | ||
| 595 | r.mu.Lock() | 612 | r.mu.Lock() | ||
| 613 | + | previousName := r.entryPoint.Name | |||
| 596 | previousProtocol := r.entryPoint.Protocol | 614 | previousProtocol := r.entryPoint.Protocol | ||
| 597 | r.entryPoint = entryPoint | 615 | r.entryPoint = entryPoint | ||
| 598 | r.trusted = trusted | 616 | r.trusted = trusted | ||
| @@ -610,6 +628,10 @@ func (r *entryPointRuntime) updateEntryPoint(entryPoint domain.EntryPoint, trust | |||||
| 610 | r.stopTLSHTTPServer(r.ctx, 0) | 628 | r.stopTLSHTTPServer(r.ctx, 0) | ||
| 611 | } | 629 | } | ||
| 612 | if entryPoint.Protocol == domain.EntryPointProtocolTLSMux { | 630 | if entryPoint.Protocol == domain.EntryPointProtocolTLSMux { | ||
| 631 | + | if previousProtocol == domain.EntryPointProtocolTLSMux && previousName != entryPoint.Name { | |||
| 632 | + | r.refreshTLSHTTPServer(entryPoint.Name) | |||
| 633 | + | return | |||
| 634 | + | } | |||
| 613 | r.startTLSHTTPServer(entryPoint) | 635 | r.startTLSHTTPServer(entryPoint) | ||
| 614 | } | 636 | } | ||
| 615 | } | 637 | } | ||
M internal/adapters/in/traffic/tcp_test.go +43 -0
| @@ -102,6 +102,49 @@ func TestTCPManagerSameAddressProtocolChangeStartsTLSHTTPServer(t *testing.T) { | |||
| 102 | 102 | assertHTTPSBody(t, graph.EntryPoints[0].Address, "app.example.com", "secure") | |
| 103 | 103 | } | |
| 104 | 104 | ||
| 105 | + | func TestTCPManagerSameAddressTLSMuxRenameRefreshesHTTPSRoute(t *testing.T) { | |
| 106 | + | address := freeTCPAddress(t) | |
| 107 | + | manager := NewManager() | |
| 108 | + | manager.SetTLSHTTPServer("old-secure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| 109 | + | _, _ = w.Write([]byte("old")) | |
| 110 | + | }), testTLSConfig(t, "app.example.com")) | |
| 111 | + | manager.SetTLSHTTPServer("new-secure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| 112 | + | _, _ = w.Write([]byte("new")) | |
| 113 | + | }), testTLSConfig(t, "app.example.com")) | |
| 114 | + | graph := domain.TrafficGraph{ | |
| 115 | + | Options: domain.TrafficOptions{TCP: domain.TCPOptions{DialTimeout: time.Second, IdleTimeout: time.Minute, DrainTimeout: time.Second}}, | |
| 116 | + | EntryPoints: []domain.EntryPoint{{Name: "old-secure", Address: address, Protocol: domain.EntryPointProtocolTLSMux}}, | |
| 117 | + | } | |
| 118 | + | require.NoError(t, manager.Apply(context.Background(), &graph)) | |
| 119 | + | defer shutdownManager(t, manager) | |
| 120 | + | assertHTTPSBody(t, address, "app.example.com", "old") | |
| 121 | + | ||
| 122 | + | renamed := graph | |
| 123 | + | renamed.EntryPoints[0].Name = "new-secure" | |
| 124 | + | require.NoError(t, manager.Apply(context.Background(), &renamed)) | |
| 125 | + | ||
| 126 | + | assertHTTPSBody(t, address, "app.example.com", "new") | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | func TestTCPRuntimeDoesNotPublishTLSHTTPServerAfterClose(t *testing.T) { | |
| 130 | + | listener, err := net.Listen("tcp", "127.0.0.1:0") | |
| 131 | + | require.NoError(t, err) | |
| 132 | + | defer listener.Close() | |
| 133 | + | manager := NewManager() | |
| 134 | + | runtime := newEntryPointRuntime(context.Background(), manager, domain.EntryPoint{Name: "websecure", Address: listener.Addr().String(), Protocol: domain.EntryPointProtocolTLSMux}, listener, nil) | |
| 135 | + | runtime.closed.Store(true) | |
| 136 | + | ||
| 137 | + | runtime.replaceTLSHTTPServer(runtime.entryPointSnapshot(), TLSHTTPServerConfig{ | |
| 138 | + | Handler: http.NotFoundHandler(), | |
| 139 | + | TLSConfig: testTLSConfig(t, "app.example.com"), | |
| 140 | + | }) | |
| 141 | + | ||
| 142 | + | runtime.mu.Lock() | |
| 143 | + | server := runtime.tlsHTTPServer | |
| 144 | + | runtime.mu.Unlock() | |
| 145 | + | require.Nil(t, server) | |
| 146 | + | } | |
| 147 | + | ||
| 105 | 148 | func TestTCPManagerSameAddressProtocolChangeStopsTLSHTTPServer(t *testing.T) { | |
| 106 | 149 | backend := startTCPEchoServer(t, 0) | |
| 107 | 150 | graph := domain.TrafficGraph{ | |
| @@ -102,6 +102,49 @@ func TestTCPManagerSameAddressProtocolChangeStartsTLSHTTPServer(t *testing.T) { | |||||
| 102 | assertHTTPSBody(t, graph.EntryPoints[0].Address, "app.example.com", "secure") | 102 | assertHTTPSBody(t, graph.EntryPoints[0].Address, "app.example.com", "secure") | ||
| 103 | } | 103 | } | ||
| 104 | 104 | ||||
| 105 | + | func TestTCPManagerSameAddressTLSMuxRenameRefreshesHTTPSRoute(t *testing.T) { | |||
| 106 | + | address := freeTCPAddress(t) | |||
| 107 | + | manager := NewManager() | |||
| 108 | + | manager.SetTLSHTTPServer("old-secure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |||
| 109 | + | _, _ = w.Write([]byte("old")) | |||
| 110 | + | }), testTLSConfig(t, "app.example.com")) | |||
| 111 | + | manager.SetTLSHTTPServer("new-secure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |||
| 112 | + | _, _ = w.Write([]byte("new")) | |||
| 113 | + | }), testTLSConfig(t, "app.example.com")) | |||
| 114 | + | graph := domain.TrafficGraph{ | |||
| 115 | + | Options: domain.TrafficOptions{TCP: domain.TCPOptions{DialTimeout: time.Second, IdleTimeout: time.Minute, DrainTimeout: time.Second}}, | |||
| 116 | + | EntryPoints: []domain.EntryPoint{{Name: "old-secure", Address: address, Protocol: domain.EntryPointProtocolTLSMux}}, | |||
| 117 | + | } | |||
| 118 | + | require.NoError(t, manager.Apply(context.Background(), &graph)) | |||
| 119 | + | defer shutdownManager(t, manager) | |||
| 120 | + | assertHTTPSBody(t, address, "app.example.com", "old") | |||
| 121 | + | ||||
| 122 | + | renamed := graph | |||
| 123 | + | renamed.EntryPoints[0].Name = "new-secure" | |||
| 124 | + | require.NoError(t, manager.Apply(context.Background(), &renamed)) | |||
| 125 | + | ||||
| 126 | + | assertHTTPSBody(t, address, "app.example.com", "new") | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | func TestTCPRuntimeDoesNotPublishTLSHTTPServerAfterClose(t *testing.T) { | |||
| 130 | + | listener, err := net.Listen("tcp", "127.0.0.1:0") | |||
| 131 | + | require.NoError(t, err) | |||
| 132 | + | defer listener.Close() | |||
| 133 | + | manager := NewManager() | |||
| 134 | + | runtime := newEntryPointRuntime(context.Background(), manager, domain.EntryPoint{Name: "websecure", Address: listener.Addr().String(), Protocol: domain.EntryPointProtocolTLSMux}, listener, nil) | |||
| 135 | + | runtime.closed.Store(true) | |||
| 136 | + | ||||
| 137 | + | runtime.replaceTLSHTTPServer(runtime.entryPointSnapshot(), TLSHTTPServerConfig{ | |||
| 138 | + | Handler: http.NotFoundHandler(), | |||
| 139 | + | TLSConfig: testTLSConfig(t, "app.example.com"), | |||
| 140 | + | }) | |||
| 141 | + | ||||
| 142 | + | runtime.mu.Lock() | |||
| 143 | + | server := runtime.tlsHTTPServer | |||
| 144 | + | runtime.mu.Unlock() | |||
| 145 | + | require.Nil(t, server) | |||
| 146 | + | } | |||
| 147 | + | ||||
| 105 | func TestTCPManagerSameAddressProtocolChangeStopsTLSHTTPServer(t *testing.T) { | 148 | func TestTCPManagerSameAddressProtocolChangeStopsTLSHTTPServer(t *testing.T) { | ||
| 106 | backend := startTCPEchoServer(t, 0) | 149 | backend := startTCPEchoServer(t, 0) | ||
| 107 | graph := domain.TrafficGraph{ | 150 | graph := domain.TrafficGraph{ | ||