detached 9546ebc

fix(traffic): refresh TLS mux runtime state

commit
9546ebca005efd9c301d028e3463887393c16a5f
author
brice · · 2026-06-30 05:56 UTC
parent
d60aa9f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +222 -19
M internal/adapters/in/traffic/tcp.go +51 -9
@@ -41,8 +41,33 @@ type entryPointRuntime struct {
4141 }
4242
4343 type trackedTCPConn struct {
44+ mu sync.Mutex
4445 client net.Conn
4546 backend net.Conn
47+ closing bool
48+}
49+
50+func (c *trackedTCPConn) setBackend(backend net.Conn) {
51+ c.mu.Lock()
52+ if c.closing {
53+ c.mu.Unlock()
54+ _ = backend.Close()
55+ return
56+ }
57+ c.backend = backend
58+ c.mu.Unlock()
59+}
60+
61+func (c *trackedTCPConn) close() {
62+ c.mu.Lock()
63+ c.closing = true
64+ client := c.client
65+ backend := c.backend
66+ c.mu.Unlock()
67+ _ = client.Close()
68+ if backend != nil {
69+ _ = backend.Close()
70+ }
4671 }
4772
4873 func newEntryPointRuntime(parentCtx context.Context, manager *Manager, entryPoint domain.EntryPoint, listener net.Listener, trusted []*net.IPNet) *entryPointRuntime {
@@ -83,6 +108,24 @@ func (r *entryPointRuntime) startTLSHTTPServer(entryPoint domain.EntryPoint) {
83108 if alreadyStarted {
84109 return
85110 }
111+ r.replaceTLSHTTPServer(entryPoint, config)
112+}
113+
114+func (r *entryPointRuntime) refreshTLSHTTPServer(entryPointName string) {
115+ entryPoint := r.entryPointSnapshot()
116+ if entryPoint.Name != entryPointName || entryPoint.Protocol != domain.EntryPointProtocolTLSMux {
117+ return
118+ }
119+ config, ok := r.manager.tlsHTTPServer(entryPointName)
120+ if !ok || config.Handler == nil || config.TLSConfig == nil {
121+ r.stopTLSHTTPServer(r.ctx, 0)
122+ return
123+ }
124+ r.replaceTLSHTTPServer(entryPoint, config)
125+}
126+
127+func (r *entryPointRuntime) replaceTLSHTTPServer(entryPoint domain.EntryPoint, config TLSHTTPServerConfig) {
128+ r.stopTLSHTTPServer(r.ctx, 0)
86129 listener := newTLSHTTPListener(r.listener.Addr())
87130 server := &http.Server{
88131 Handler: config.Handler,
@@ -431,8 +474,13 @@ func (r *entryPointRuntime) track(conn *trackedTCPConn) {
431474
432475 func (r *entryPointRuntime) setBackend(conn *trackedTCPConn, backend net.Conn) {
433476 r.mu.Lock()
434- conn.backend = backend
477+ _, tracked := r.activeConns[conn]
435478 r.mu.Unlock()
479+ if !tracked {
480+ _ = backend.Close()
481+ return
482+ }
483+ conn.setBackend(backend)
436484 }
437485
438486 func (r *entryPointRuntime) untrack(conn *trackedTCPConn) {
@@ -530,10 +578,7 @@ func (r *entryPointRuntime) closeActiveConns() {
530578 r.mu.Lock()
531579 defer r.mu.Unlock()
532580 for conn := range r.activeConns {
533- _ = conn.client.Close()
534- if conn.backend != nil {
535- _ = conn.backend.Close()
536- }
581+ conn.close()
537582 }
538583 }
539584
@@ -559,10 +604,7 @@ func (r *entryPointRuntime) updateEntryPoint(entryPoint domain.EntryPoint, trust
559604 }
560605 r.mu.Unlock()
561606 for _, conn := range stale {
562- _ = conn.client.Close()
563- if conn.backend != nil {
564- _ = conn.backend.Close()
565- }
607+ conn.close()
566608 }
567609 if previousProtocol == domain.EntryPointProtocolTLSMux && entryPoint.Protocol != domain.EntryPointProtocolTLSMux {
568610 r.stopTLSHTTPServer(r.ctx, 0)
@@ -41,8 +41,33 @@ type entryPointRuntime struct {
41 }41 }
42 42
43 type trackedTCPConn struct {43 type trackedTCPConn struct {
44+ mu sync.Mutex
44 client net.Conn45 client net.Conn
45 backend net.Conn46 backend net.Conn
47+ closing bool
48+}
49+
50+func (c *trackedTCPConn) setBackend(backend net.Conn) {
51+ c.mu.Lock()
52+ if c.closing {
53+ c.mu.Unlock()
54+ _ = backend.Close()
55+ return
56+ }
57+ c.backend = backend
58+ c.mu.Unlock()
59+}
60+
61+func (c *trackedTCPConn) close() {
62+ c.mu.Lock()
63+ c.closing = true
64+ client := c.client
65+ backend := c.backend
66+ c.mu.Unlock()
67+ _ = client.Close()
68+ if backend != nil {
69+ _ = backend.Close()
70+ }
46 }71 }
47 72
48 func newEntryPointRuntime(parentCtx context.Context, manager *Manager, entryPoint domain.EntryPoint, listener net.Listener, trusted []*net.IPNet) *entryPointRuntime {73 func newEntryPointRuntime(parentCtx context.Context, manager *Manager, entryPoint domain.EntryPoint, listener net.Listener, trusted []*net.IPNet) *entryPointRuntime {
@@ -83,6 +108,24 @@ func (r *entryPointRuntime) startTLSHTTPServer(entryPoint domain.EntryPoint) {
83 if alreadyStarted {108 if alreadyStarted {
84 return109 return
85 }110 }
111+ r.replaceTLSHTTPServer(entryPoint, config)
112+}
113+
114+func (r *entryPointRuntime) refreshTLSHTTPServer(entryPointName string) {
115+ entryPoint := r.entryPointSnapshot()
116+ if entryPoint.Name != entryPointName || entryPoint.Protocol != domain.EntryPointProtocolTLSMux {
117+ return
118+ }
119+ config, ok := r.manager.tlsHTTPServer(entryPointName)
120+ if !ok || config.Handler == nil || config.TLSConfig == nil {
121+ r.stopTLSHTTPServer(r.ctx, 0)
122+ return
123+ }
124+ r.replaceTLSHTTPServer(entryPoint, config)
125+}
126+
127+func (r *entryPointRuntime) replaceTLSHTTPServer(entryPoint domain.EntryPoint, config TLSHTTPServerConfig) {
128+ r.stopTLSHTTPServer(r.ctx, 0)
86 listener := newTLSHTTPListener(r.listener.Addr())129 listener := newTLSHTTPListener(r.listener.Addr())
87 server := &http.Server{130 server := &http.Server{
88 Handler: config.Handler,131 Handler: config.Handler,
@@ -431,8 +474,13 @@ func (r *entryPointRuntime) track(conn *trackedTCPConn) {
431 474
432 func (r *entryPointRuntime) setBackend(conn *trackedTCPConn, backend net.Conn) {475 func (r *entryPointRuntime) setBackend(conn *trackedTCPConn, backend net.Conn) {
433 r.mu.Lock()476 r.mu.Lock()
434- conn.backend = backend477+ _, tracked := r.activeConns[conn]
435 r.mu.Unlock()478 r.mu.Unlock()
479+ if !tracked {
480+ _ = backend.Close()
481+ return
482+ }
483+ conn.setBackend(backend)
436 }484 }
437 485
438 func (r *entryPointRuntime) untrack(conn *trackedTCPConn) {486 func (r *entryPointRuntime) untrack(conn *trackedTCPConn) {
@@ -530,10 +578,7 @@ func (r *entryPointRuntime) closeActiveConns() {
530 r.mu.Lock()578 r.mu.Lock()
531 defer r.mu.Unlock()579 defer r.mu.Unlock()
532 for conn := range r.activeConns {580 for conn := range r.activeConns {
533- _ = conn.client.Close()581+ conn.close()
534- if conn.backend != nil {
535- _ = conn.backend.Close()
536- }
537 }582 }
538 }583 }
539 584
@@ -559,10 +604,7 @@ func (r *entryPointRuntime) updateEntryPoint(entryPoint domain.EntryPoint, trust
559 }604 }
560 r.mu.Unlock()605 r.mu.Unlock()
561 for _, conn := range stale {606 for _, conn := range stale {
562- _ = conn.client.Close()607+ conn.close()
563- if conn.backend != nil {
564- _ = conn.backend.Close()
565- }
566 }608 }
567 if previousProtocol == domain.EntryPointProtocolTLSMux && entryPoint.Protocol != domain.EntryPointProtocolTLSMux {609 if previousProtocol == domain.EntryPointProtocolTLSMux && entryPoint.Protocol != domain.EntryPointProtocolTLSMux {
568 r.stopTLSHTTPServer(r.ctx, 0)610 r.stopTLSHTTPServer(r.ctx, 0)
M internal/adapters/in/traffic/tcp_test.go +18 -0
@@ -123,6 +123,24 @@ func TestTCPManagerSameAddressProtocolChangeStopsTLSHTTPServer(t *testing.T) {
123123 require.Error(t, err)
124124 }
125125
126+func TestTrackedTCPConnClosesLateBackendAfterStaleClose(t *testing.T) {
127+ clientA, clientB := net.Pipe()
128+ backendA, backendB := net.Pipe()
129+ defer clientA.Close()
130+ defer backendB.Close()
131+
132+ conn := &trackedTCPConn{client: clientB}
133+ conn.close()
134+ conn.setBackend(backendA)
135+
136+ _, err := backendB.Write([]byte("late backend"))
137+ if err == nil {
138+ _ = backendA.SetReadDeadline(time.Now().Add(50 * time.Millisecond))
139+ _, err = backendA.Read(make([]byte, 1))
140+ }
141+ require.Error(t, err)
142+}
143+
126144 func TestTCPPassthroughRejectsUntrustedCIDR(t *testing.T) {
127145 backend := startTCPEchoServer(t, 0)
128146 graph := tcpGraph(t, freeTCPAddress(t), backend.address)
@@ -123,6 +123,24 @@ func TestTCPManagerSameAddressProtocolChangeStopsTLSHTTPServer(t *testing.T) {
123 require.Error(t, err)123 require.Error(t, err)
124 }124 }
125 125
126+func TestTrackedTCPConnClosesLateBackendAfterStaleClose(t *testing.T) {
127+ clientA, clientB := net.Pipe()
128+ backendA, backendB := net.Pipe()
129+ defer clientA.Close()
130+ defer backendB.Close()
131+
132+ conn := &trackedTCPConn{client: clientB}
133+ conn.close()
134+ conn.setBackend(backendA)
135+
136+ _, err := backendB.Write([]byte("late backend"))
137+ if err == nil {
138+ _ = backendA.SetReadDeadline(time.Now().Add(50 * time.Millisecond))
139+ _, err = backendA.Read(make([]byte, 1))
140+ }
141+ require.Error(t, err)
142+}
143+
126 func TestTCPPassthroughRejectsUntrustedCIDR(t *testing.T) {144 func TestTCPPassthroughRejectsUntrustedCIDR(t *testing.T) {
127 backend := startTCPEchoServer(t, 0)145 backend := startTCPEchoServer(t, 0)
128 graph := tcpGraph(t, freeTCPAddress(t), backend.address)146 graph := tcpGraph(t, freeTCPAddress(t), backend.address)
M internal/adapters/in/traffic/tls_mux.go +29 -8
@@ -24,8 +24,6 @@ type tlsHTTPServers map[string]TLSHTTPServerConfig
2424 // SetTLSHTTPServer installs or removes the HTTPS server for a tls_mux entrypoint.
2525 func (m *Manager) SetTLSHTTPServer(entryPoint string, handler http.Handler, tlsConfig *tls.Config) {
2626 m.mu.Lock()
27- defer m.mu.Unlock()
28-
2927 current := m.loadTLSHTTPServers()
3028 next := make(tlsHTTPServers, len(current)+1)
3129 maps.Copy(next, current)
@@ -35,6 +33,17 @@ func (m *Manager) SetTLSHTTPServer(entryPoint string, handler http.Handler, tlsC
3533 next[entryPoint] = TLSHTTPServerConfig{Handler: handler, TLSConfig: tlsConfig.Clone()}
3634 }
3735 m.tlsHTTPServers.Store(next)
36+ runtimes := make([]*entryPointRuntime, 0, len(m.listeners))
37+ for _, runtime := range m.listeners {
38+ if runtime.entryPointSnapshot().Name == entryPoint {
39+ runtimes = append(runtimes, runtime)
40+ }
41+ }
42+ m.mu.Unlock()
43+
44+ for _, runtime := range runtimes {
45+ runtime.refreshTLSHTTPServer(entryPoint)
46+ }
3847 }
3948
4049 func (m *Manager) tlsHTTPServer(entryPoint string) (TLSHTTPServerConfig, bool) {
@@ -262,10 +271,12 @@ func (c replayConn) Read(p []byte) (int, error) {
262271 }
263272
264273 type tlsHTTPListener struct {
265- addr net.Addr
266- conns chan net.Conn
267- done chan struct{}
268- once sync.Once
274+ addr net.Addr
275+ conns chan net.Conn
276+ done chan struct{}
277+ once sync.Once
278+ mu sync.Mutex
279+ closed bool
269280 }
270281
271282 func newTLSHTTPListener(addr net.Addr) *tlsHTTPListener {
@@ -282,17 +293,27 @@ func (l *tlsHTTPListener) Accept() (net.Conn, error) {
282293 }
283294
284295 func (l *tlsHTTPListener) Close() error {
285- l.once.Do(func() { close(l.done) })
296+ l.once.Do(func() {
297+ l.mu.Lock()
298+ l.closed = true
299+ l.mu.Unlock()
300+ close(l.done)
301+ })
286302 return nil
287303 }
288304
289305 func (l *tlsHTTPListener) Addr() net.Addr { return l.addr }
290306
291307 func (l *tlsHTTPListener) serve(conn net.Conn) bool {
308+ l.mu.Lock()
309+ defer l.mu.Unlock()
310+ if l.closed {
311+ return false
312+ }
292313 select {
293314 case l.conns <- conn:
294315 return true
295- case <-l.done:
316+ default:
296317 return false
297318 }
298319 }
@@ -24,8 +24,6 @@ type tlsHTTPServers map[string]TLSHTTPServerConfig
24 // SetTLSHTTPServer installs or removes the HTTPS server for a tls_mux entrypoint.24 // SetTLSHTTPServer installs or removes the HTTPS server for a tls_mux entrypoint.
25 func (m *Manager) SetTLSHTTPServer(entryPoint string, handler http.Handler, tlsConfig *tls.Config) {25 func (m *Manager) SetTLSHTTPServer(entryPoint string, handler http.Handler, tlsConfig *tls.Config) {
26 m.mu.Lock()26 m.mu.Lock()
27- defer m.mu.Unlock()
28-
29 current := m.loadTLSHTTPServers()27 current := m.loadTLSHTTPServers()
30 next := make(tlsHTTPServers, len(current)+1)28 next := make(tlsHTTPServers, len(current)+1)
31 maps.Copy(next, current)29 maps.Copy(next, current)
@@ -35,6 +33,17 @@ func (m *Manager) SetTLSHTTPServer(entryPoint string, handler http.Handler, tlsC
35 next[entryPoint] = TLSHTTPServerConfig{Handler: handler, TLSConfig: tlsConfig.Clone()}33 next[entryPoint] = TLSHTTPServerConfig{Handler: handler, TLSConfig: tlsConfig.Clone()}
36 }34 }
37 m.tlsHTTPServers.Store(next)35 m.tlsHTTPServers.Store(next)
36+ runtimes := make([]*entryPointRuntime, 0, len(m.listeners))
37+ for _, runtime := range m.listeners {
38+ if runtime.entryPointSnapshot().Name == entryPoint {
39+ runtimes = append(runtimes, runtime)
40+ }
41+ }
42+ m.mu.Unlock()
43+
44+ for _, runtime := range runtimes {
45+ runtime.refreshTLSHTTPServer(entryPoint)
46+ }
38 }47 }
39 48
40 func (m *Manager) tlsHTTPServer(entryPoint string) (TLSHTTPServerConfig, bool) {49 func (m *Manager) tlsHTTPServer(entryPoint string) (TLSHTTPServerConfig, bool) {
@@ -262,10 +271,12 @@ func (c replayConn) Read(p []byte) (int, error) {
262 }271 }
263 272
264 type tlsHTTPListener struct {273 type tlsHTTPListener struct {
265- addr net.Addr274+ addr net.Addr
266- conns chan net.Conn275+ conns chan net.Conn
267- done chan struct{}276+ done chan struct{}
268- once sync.Once277+ once sync.Once
278+ mu sync.Mutex
279+ closed bool
269 }280 }
270 281
271 func newTLSHTTPListener(addr net.Addr) *tlsHTTPListener {282 func newTLSHTTPListener(addr net.Addr) *tlsHTTPListener {
@@ -282,17 +293,27 @@ func (l *tlsHTTPListener) Accept() (net.Conn, error) {
282 }293 }
283 294
284 func (l *tlsHTTPListener) Close() error {295 func (l *tlsHTTPListener) Close() error {
285- l.once.Do(func() { close(l.done) })296+ l.once.Do(func() {
297+ l.mu.Lock()
298+ l.closed = true
299+ l.mu.Unlock()
300+ close(l.done)
301+ })
286 return nil302 return nil
287 }303 }
288 304
289 func (l *tlsHTTPListener) Addr() net.Addr { return l.addr }305 func (l *tlsHTTPListener) Addr() net.Addr { return l.addr }
290 306
291 func (l *tlsHTTPListener) serve(conn net.Conn) bool {307 func (l *tlsHTTPListener) serve(conn net.Conn) bool {
308+ l.mu.Lock()
309+ defer l.mu.Unlock()
310+ if l.closed {
311+ return false
312+ }
292 select {313 select {
293 case l.conns <- conn:314 case l.conns <- conn:
294 return true315 return true
295- case <-l.done:316+ default:
296 return false317 return false
297 }318 }
298 }319 }
M internal/adapters/in/traffic/tls_mux_test.go +31 -0
@@ -169,6 +169,37 @@ func TestTLSMuxIncompleteClientHelloDoesNotHangShutdown(t *testing.T) {
169169 _ = conn.Close()
170170 }
171171
172+func TestTLSMuxHTTPServerRefreshesActiveRuntime(t *testing.T) {
173+ graph := tlsGraph(t, freeTCPAddress(t), nil)
174+ manager := NewManager()
175+ manager.SetTLSHTTPServer("websecure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
176+ _, _ = w.Write([]byte("before"))
177+ }), testTLSConfig(t, "app.example.com"))
178+ require.NoError(t, manager.Apply(context.Background(), &graph))
179+ defer shutdownManager(t, manager)
180+ assertHTTPSBody(t, graph.EntryPoints[0].Address, "app.example.com", "before")
181+
182+ manager.SetTLSHTTPServer("websecure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
183+ _, _ = w.Write([]byte("after"))
184+ }), testTLSConfig(t, "app.example.com"))
185+
186+ assertHTTPSBody(t, graph.EntryPoints[0].Address, "app.example.com", "after")
187+}
188+
189+func TestTLSHTTPListenerServeRejectsAfterClose(t *testing.T) {
190+ for range 1000 {
191+ addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:443")
192+ require.NoError(t, err)
193+ listener := newTLSHTTPListener(addr)
194+ require.NoError(t, listener.Close())
195+ client, server := net.Pipe()
196+ accepted := listener.serve(server)
197+ _ = client.Close()
198+ _ = server.Close()
199+ require.False(t, accepted)
200+ }
201+}
202+
172203 func TestTLSMuxHTTPSRoute(t *testing.T) {
173204 backend := startTLSGreetingServer(t, "raw-backend", "raw\n")
174205 graph := tlsGraph(t, freeTCPAddress(t), []tlsRoute{{name: "raw", sni: "raw.example.com", service: "raw", backend: backend.address}})
@@ -169,6 +169,37 @@ func TestTLSMuxIncompleteClientHelloDoesNotHangShutdown(t *testing.T) {
169 _ = conn.Close()169 _ = conn.Close()
170 }170 }
171 171
172+func TestTLSMuxHTTPServerRefreshesActiveRuntime(t *testing.T) {
173+ graph := tlsGraph(t, freeTCPAddress(t), nil)
174+ manager := NewManager()
175+ manager.SetTLSHTTPServer("websecure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
176+ _, _ = w.Write([]byte("before"))
177+ }), testTLSConfig(t, "app.example.com"))
178+ require.NoError(t, manager.Apply(context.Background(), &graph))
179+ defer shutdownManager(t, manager)
180+ assertHTTPSBody(t, graph.EntryPoints[0].Address, "app.example.com", "before")
181+
182+ manager.SetTLSHTTPServer("websecure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
183+ _, _ = w.Write([]byte("after"))
184+ }), testTLSConfig(t, "app.example.com"))
185+
186+ assertHTTPSBody(t, graph.EntryPoints[0].Address, "app.example.com", "after")
187+}
188+
189+func TestTLSHTTPListenerServeRejectsAfterClose(t *testing.T) {
190+ for range 1000 {
191+ addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:443")
192+ require.NoError(t, err)
193+ listener := newTLSHTTPListener(addr)
194+ require.NoError(t, listener.Close())
195+ client, server := net.Pipe()
196+ accepted := listener.serve(server)
197+ _ = client.Close()
198+ _ = server.Close()
199+ require.False(t, accepted)
200+ }
201+}
202+
172 func TestTLSMuxHTTPSRoute(t *testing.T) {203 func TestTLSMuxHTTPSRoute(t *testing.T) {
173 backend := startTLSGreetingServer(t, "raw-backend", "raw\n")204 backend := startTLSGreetingServer(t, "raw-backend", "raw\n")
174 graph := tlsGraph(t, freeTCPAddress(t), []tlsRoute{{name: "raw", sni: "raw.example.com", service: "raw", backend: backend.address}})205 graph := tlsGraph(t, freeTCPAddress(t), []tlsRoute{{name: "raw", sni: "raw.example.com", service: "raw", backend: backend.address}})
M internal/app/run.go +16 -2
@@ -3292,6 +3292,14 @@ func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSv
32923292 if cfg.Server.TLSPort == 0 {
32933293 return httpSrv, httpReady, nil, nil, nil
32943294 }
3295+ cleanupHTTP := func(err error) (*http.Server, <-chan struct{}, *http.Server, <-chan struct{}, error) {
3296+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
3297+ defer cancel()
3298+ if shutdownErr := httpSrv.Shutdown(shutdownCtx); shutdownErr != nil {
3299+ log.Error().Err(shutdownErr).Msg("failed to shut down proxy-http server during startup cleanup")
3300+ }
3301+ return httpSrv, httpReady, nil, nil, err
3302+ }
32953303
32963304 // Load static cert into a local slice (not tls.Config.Certificates) so the
32973305 // certificate selector has full control over priority ordering.
@@ -3299,7 +3307,7 @@ func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSv
32993307 if cfg.Server.TLSCertFile != "" {
33003308 staticCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCertFile, cfg.Server.TLSKeyFile)
33013309 if err != nil {
3302- return nil, nil, nil, nil, fmt.Errorf("load TLS keypair: %w", err)
3310+ return cleanupHTTP(fmt.Errorf("load TLS keypair: %w", err))
33033311 }
33043312 staticCerts = []tls.Certificate{staticCert}
33053313 log.Info().
@@ -3322,9 +3330,15 @@ func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSv
33223330 NextProtos: []string{"h2", "http/1.1"},
33233331 }
33243332 if trafficManager == nil {
3325- return nil, nil, nil, nil, fmt.Errorf("traffic manager is required when server.tls_port is enabled")
3333+ return cleanupHTTP(fmt.Errorf("traffic manager is required when server.tls_port is enabled"))
33263334 }
33273335 trafficManager.SetTLSHTTPServer(traffic.DefaultTLSEntryPointName, httpsHandler, tlsConfig)
3336+ for name, entryPoint := range cfg.EntryPoints {
3337+ domainEntryPoint := domain.EntryPoint{Name: name, Protocol: entryPoint.Protocol}
3338+ if trafficManagerOwnsEntryPoint(domainEntryPoint) && domainEntryPoint.Protocol == domain.EntryPointProtocolTLSMux {
3339+ trafficManager.SetTLSHTTPServer(name, httpsHandler, tlsConfig)
3340+ }
3341+ }
33283342
33293343 return httpSrv, httpReady, nil, nil, nil
33303344 }
@@ -3292,6 +3292,14 @@ func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSv
3292 if cfg.Server.TLSPort == 0 {3292 if cfg.Server.TLSPort == 0 {
3293 return httpSrv, httpReady, nil, nil, nil3293 return httpSrv, httpReady, nil, nil, nil
3294 }3294 }
3295+ cleanupHTTP := func(err error) (*http.Server, <-chan struct{}, *http.Server, <-chan struct{}, error) {
3296+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
3297+ defer cancel()
3298+ if shutdownErr := httpSrv.Shutdown(shutdownCtx); shutdownErr != nil {
3299+ log.Error().Err(shutdownErr).Msg("failed to shut down proxy-http server during startup cleanup")
3300+ }
3301+ return httpSrv, httpReady, nil, nil, err
3302+ }
3295 3303
3296 // Load static cert into a local slice (not tls.Config.Certificates) so the3304 // Load static cert into a local slice (not tls.Config.Certificates) so the
3297 // certificate selector has full control over priority ordering.3305 // certificate selector has full control over priority ordering.
@@ -3299,7 +3307,7 @@ func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSv
3299 if cfg.Server.TLSCertFile != "" {3307 if cfg.Server.TLSCertFile != "" {
3300 staticCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCertFile, cfg.Server.TLSKeyFile)3308 staticCert, err := tls.LoadX509KeyPair(cfg.Server.TLSCertFile, cfg.Server.TLSKeyFile)
3301 if err != nil {3309 if err != nil {
3302- return nil, nil, nil, nil, fmt.Errorf("load TLS keypair: %w", err)3310+ return cleanupHTTP(fmt.Errorf("load TLS keypair: %w", err))
3303 }3311 }
3304 staticCerts = []tls.Certificate{staticCert}3312 staticCerts = []tls.Certificate{staticCert}
3305 log.Info().3313 log.Info().
@@ -3322,9 +3330,15 @@ func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSv
3322 NextProtos: []string{"h2", "http/1.1"},3330 NextProtos: []string{"h2", "http/1.1"},
3323 }3331 }
3324 if trafficManager == nil {3332 if trafficManager == nil {
3325- return nil, nil, nil, nil, fmt.Errorf("traffic manager is required when server.tls_port is enabled")3333+ return cleanupHTTP(fmt.Errorf("traffic manager is required when server.tls_port is enabled"))
3326 }3334 }
3327 trafficManager.SetTLSHTTPServer(traffic.DefaultTLSEntryPointName, httpsHandler, tlsConfig)3335 trafficManager.SetTLSHTTPServer(traffic.DefaultTLSEntryPointName, httpsHandler, tlsConfig)
3336+ for name, entryPoint := range cfg.EntryPoints {
3337+ domainEntryPoint := domain.EntryPoint{Name: name, Protocol: entryPoint.Protocol}
3338+ if trafficManagerOwnsEntryPoint(domainEntryPoint) && domainEntryPoint.Protocol == domain.EntryPointProtocolTLSMux {
3339+ trafficManager.SetTLSHTTPServer(name, httpsHandler, tlsConfig)
3340+ }
3341+ }
3328 3342
3329 return httpSrv, httpReady, nil, nil, nil3343 return httpSrv, httpReady, nil, nil, nil
3330 }3344 }
M internal/app/start_proxy_servers_test.go +77 -0
@@ -21,6 +21,7 @@ import (
2121 outmocks "github.com/bnema/gordon/internal/boundaries/out/mocks"
2222 "github.com/bnema/gordon/internal/domain"
2323 pkiusecase "github.com/bnema/gordon/internal/usecase/pki"
24+ traffic "github.com/bnema/gordon/internal/usecase/traffic"
2425 )
2526
2627 func TestStartProxyServers_ConfiguresTrafficManagerHTTPSRoute(t *testing.T) {
@@ -58,6 +59,82 @@ func TestStartProxyServers_ConfiguresTrafficManagerHTTPSRoute(t *testing.T) {
5859 require.Contains(t, body, "secure")
5960 }
6061
62+func TestStartProxyServers_ConfiguresCustomTLSMuxHTTPSRoute(t *testing.T) {
63+ cfg := Config{}
64+ cfg.Server.Port = freeTCPPort(t)
65+ cfg.Server.TLSPort = freeTCPPort(t)
66+ cfg.EntryPoints = map[string]traffic.EntryPointConfig{
67+ "custom-secure": {Address: fmt.Sprintf("127.0.0.1:%d", freeTCPPort(t)), Protocol: domain.EntryPointProtocolTLSMux},
68+ }
69+ cfg.Traffic.TLS.Routers = []traffic.RouterConfig{{Name: "raw", EntryPoint: "custom-secure", SNI: "raw.example.com", Service: "network_service:raw:tls"}}
70+ cfg.NetworkServices = []traffic.NetworkServiceConfig{{Name: "raw", Ports: []traffic.PortConfig{{Name: "tls", Container: 443, Protocol: domain.NetworkProtocolTCP}}}}
71+
72+ manager := trafficadapter.NewManager()
73+ defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
74+ pkiSvc := newTestPKIService(t)
75+ errChan := make(chan error, 4)
76+
77+ httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
78+ cfg,
79+ http.NotFoundHandler(),
80+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("custom secure")) }),
81+ pkiSvc,
82+ nil,
83+ manager,
84+ errChan,
85+ zerowrap.Default(),
86+ )
87+ require.NoError(t, err)
88+ defer shutdownHTTPServer(t, httpSrv)
89+ require.Nil(t, tlsSrv)
90+ require.Nil(t, tlsReady)
91+ waitReadyForTest(t, httpReady, errChan)
92+
93+ configSvc := inmocks.NewMockConfigService(t)
94+ configSvc.EXPECT().GetRoutes(context.Background()).Return(nil)
95+ configSvc.EXPECT().GetExternalRoutes().Return(nil)
96+ require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc))
97+
98+ _, customPort, err := net.SplitHostPort(cfg.EntryPoints["custom-secure"].Address)
99+ require.NoError(t, err)
100+ port, err := strconv.Atoi(customPort)
101+ require.NoError(t, err)
102+ body := httpsGetForTest(t, port, "app.example.com")
103+ require.Contains(t, body, "custom secure")
104+}
105+
106+func TestStartProxyServers_TLSSetupFailureStopsHTTPServer(t *testing.T) {
107+ cfg := Config{}
108+ cfg.Server.Port = freeTCPPort(t)
109+ cfg.Server.TLSPort = freeTCPPort(t)
110+ cfg.Server.TLSCertFile = "/path/that/does/not/exist.crt"
111+ cfg.Server.TLSKeyFile = "/path/that/does/not/exist.key"
112+
113+ httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
114+ cfg,
115+ http.NotFoundHandler(),
116+ http.NotFoundHandler(),
117+ newTestPKIService(t),
118+ nil,
119+ trafficadapter.NewManager(),
120+ make(chan error, 4),
121+ zerowrap.Default(),
122+ )
123+ require.ErrorContains(t, err, "load TLS keypair")
124+ require.Nil(t, tlsSrv)
125+ require.Nil(t, tlsReady)
126+ if httpReady != nil {
127+ select {
128+ case <-httpReady:
129+ case <-time.After(time.Second):
130+ }
131+ }
132+ if httpSrv != nil {
133+ _, err = net.DialTimeout("tcp", httpSrv.Addr, 100*time.Millisecond)
134+ require.Error(t, err)
135+ }
136+}
137+
61138 func TestStartProxyServers_TLSRequiresTrafficManager(t *testing.T) {
62139 cfg := Config{}
63140 cfg.Server.Port = freeTCPPort(t)
@@ -21,6 +21,7 @@ import (
21 outmocks "github.com/bnema/gordon/internal/boundaries/out/mocks"21 outmocks "github.com/bnema/gordon/internal/boundaries/out/mocks"
22 "github.com/bnema/gordon/internal/domain"22 "github.com/bnema/gordon/internal/domain"
23 pkiusecase "github.com/bnema/gordon/internal/usecase/pki"23 pkiusecase "github.com/bnema/gordon/internal/usecase/pki"
24+ traffic "github.com/bnema/gordon/internal/usecase/traffic"
24 )25 )
25 26
26 func TestStartProxyServers_ConfiguresTrafficManagerHTTPSRoute(t *testing.T) {27 func TestStartProxyServers_ConfiguresTrafficManagerHTTPSRoute(t *testing.T) {
@@ -58,6 +59,82 @@ func TestStartProxyServers_ConfiguresTrafficManagerHTTPSRoute(t *testing.T) {
58 require.Contains(t, body, "secure")59 require.Contains(t, body, "secure")
59 }60 }
60 61
62+func TestStartProxyServers_ConfiguresCustomTLSMuxHTTPSRoute(t *testing.T) {
63+ cfg := Config{}
64+ cfg.Server.Port = freeTCPPort(t)
65+ cfg.Server.TLSPort = freeTCPPort(t)
66+ cfg.EntryPoints = map[string]traffic.EntryPointConfig{
67+ "custom-secure": {Address: fmt.Sprintf("127.0.0.1:%d", freeTCPPort(t)), Protocol: domain.EntryPointProtocolTLSMux},
68+ }
69+ cfg.Traffic.TLS.Routers = []traffic.RouterConfig{{Name: "raw", EntryPoint: "custom-secure", SNI: "raw.example.com", Service: "network_service:raw:tls"}}
70+ cfg.NetworkServices = []traffic.NetworkServiceConfig{{Name: "raw", Ports: []traffic.PortConfig{{Name: "tls", Container: 443, Protocol: domain.NetworkProtocolTCP}}}}
71+
72+ manager := trafficadapter.NewManager()
73+ defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
74+ pkiSvc := newTestPKIService(t)
75+ errChan := make(chan error, 4)
76+
77+ httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
78+ cfg,
79+ http.NotFoundHandler(),
80+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("custom secure")) }),
81+ pkiSvc,
82+ nil,
83+ manager,
84+ errChan,
85+ zerowrap.Default(),
86+ )
87+ require.NoError(t, err)
88+ defer shutdownHTTPServer(t, httpSrv)
89+ require.Nil(t, tlsSrv)
90+ require.Nil(t, tlsReady)
91+ waitReadyForTest(t, httpReady, errChan)
92+
93+ configSvc := inmocks.NewMockConfigService(t)
94+ configSvc.EXPECT().GetRoutes(context.Background()).Return(nil)
95+ configSvc.EXPECT().GetExternalRoutes().Return(nil)
96+ require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc))
97+
98+ _, customPort, err := net.SplitHostPort(cfg.EntryPoints["custom-secure"].Address)
99+ require.NoError(t, err)
100+ port, err := strconv.Atoi(customPort)
101+ require.NoError(t, err)
102+ body := httpsGetForTest(t, port, "app.example.com")
103+ require.Contains(t, body, "custom secure")
104+}
105+
106+func TestStartProxyServers_TLSSetupFailureStopsHTTPServer(t *testing.T) {
107+ cfg := Config{}
108+ cfg.Server.Port = freeTCPPort(t)
109+ cfg.Server.TLSPort = freeTCPPort(t)
110+ cfg.Server.TLSCertFile = "/path/that/does/not/exist.crt"
111+ cfg.Server.TLSKeyFile = "/path/that/does/not/exist.key"
112+
113+ httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
114+ cfg,
115+ http.NotFoundHandler(),
116+ http.NotFoundHandler(),
117+ newTestPKIService(t),
118+ nil,
119+ trafficadapter.NewManager(),
120+ make(chan error, 4),
121+ zerowrap.Default(),
122+ )
123+ require.ErrorContains(t, err, "load TLS keypair")
124+ require.Nil(t, tlsSrv)
125+ require.Nil(t, tlsReady)
126+ if httpReady != nil {
127+ select {
128+ case <-httpReady:
129+ case <-time.After(time.Second):
130+ }
131+ }
132+ if httpSrv != nil {
133+ _, err = net.DialTimeout("tcp", httpSrv.Addr, 100*time.Millisecond)
134+ require.Error(t, err)
135+ }
136+}
137+
61 func TestStartProxyServers_TLSRequiresTrafficManager(t *testing.T) {138 func TestStartProxyServers_TLSRequiresTrafficManager(t *testing.T) {
62 cfg := Config{}139 cfg := Config{}
63 cfg.Server.Port = freeTCPPort(t)140 cfg.Server.Port = freeTCPPort(t)

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