detached 228f200

fix(traffic): serialize TLS mux server replacement

commit
228f200f85175d6cbf7d0ba6a3d2611ed018f949
author
brice · · 2026-06-30 07:25 UTC
parent
9546ebc
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
3535 mu sync.Mutex
3636 activeConns map[*trackedTCPConn]struct{}
3737
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{}
4142 }
4243
4344 type trackedTCPConn struct {
@@ -125,7 +126,12 @@ func (r *entryPointRuntime) refreshTLSHTTPServer(entryPointName string) {
125126 }
126127
127128 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+ }
129135 listener := newTLSHTTPListener(r.listener.Addr())
130136 server := &http.Server{
131137 Handler: config.Handler,
@@ -138,6 +144,11 @@ func (r *entryPointRuntime) replaceTLSHTTPServer(entryPoint domain.EntryPoint, c
138144 done := make(chan struct{})
139145
140146 r.mu.Lock()
147+ if r.isClosed() {
148+ r.mu.Unlock()
149+ _ = listener.Close()
150+ return
151+ }
141152 r.tlsHTTPListener = listener
142153 r.tlsHTTPServer = server
143154 r.tlsHTTPDone = done
@@ -519,6 +530,12 @@ func (r *entryPointRuntime) stop(ctx context.Context, drainTimeout time.Duration
519530 }
520531
521532 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) {
522539 r.mu.Lock()
523540 listener := r.tlsHTTPListener
524541 server := r.tlsHTTPServer
@@ -593,6 +610,7 @@ func (r *entryPointRuntime) sameAddress(entryPoint domain.EntryPoint) bool {
593610
594611 func (r *entryPointRuntime) updateEntryPoint(entryPoint domain.EntryPoint, trusted []*net.IPNet) {
595612 r.mu.Lock()
613+ previousName := r.entryPoint.Name
596614 previousProtocol := r.entryPoint.Protocol
597615 r.entryPoint = entryPoint
598616 r.trusted = trusted
@@ -610,6 +628,10 @@ func (r *entryPointRuntime) updateEntryPoint(entryPoint domain.EntryPoint, trust
610628 r.stopTLSHTTPServer(r.ctx, 0)
611629 }
612630 if entryPoint.Protocol == domain.EntryPointProtocolTLSMux {
631+ if previousProtocol == domain.EntryPointProtocolTLSMux && previousName != entryPoint.Name {
632+ r.refreshTLSHTTPServer(entryPoint.Name)
633+ return
634+ }
613635 r.startTLSHTTPServer(entryPoint)
614636 }
615637 }
@@ -35,9 +35,10 @@ type entryPointRuntime struct {
35 mu sync.Mutex35 mu sync.Mutex
36 activeConns map[*trackedTCPConn]struct{}36 activeConns map[*trackedTCPConn]struct{}
37 37
38- tlsHTTPListener *tlsHTTPListener38+ tlsHTTPReplaceMu sync.Mutex
39- tlsHTTPServer *http.Server39+ 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 = listener152 r.tlsHTTPListener = listener
142 r.tlsHTTPServer = server153 r.tlsHTTPServer = server
143 r.tlsHTTPDone = done154 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.tlsHTTPListener540 listener := r.tlsHTTPListener
524 server := r.tlsHTTPServer541 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.Protocol614 previousProtocol := r.entryPoint.Protocol
597 r.entryPoint = entryPoint615 r.entryPoint = entryPoint
598 r.trusted = trusted616 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) {
102102 assertHTTPSBody(t, graph.EntryPoints[0].Address, "app.example.com", "secure")
103103 }
104104
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+
105148 func TestTCPManagerSameAddressProtocolChangeStopsTLSHTTPServer(t *testing.T) {
106149 backend := startTCPEchoServer(t, 0)
107150 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{

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close