diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index f8d657265..ae110c3c3 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -3,6 +3,7 @@ package tcp import ( "bufio" "bytes" + "context" "crypto/tls" "errors" "io" @@ -206,7 +207,17 @@ func (r *Router) acmeTLSALPNHandler() tcp.Handler { } return tcp.HandlerFunc(func(conn tcp.WriteCloser) { - _ = tls.Server(conn, r.httpsTLSConfig).Handshake() + tlsConn := tls.Server(conn, r.httpsTLSConfig) + defer tlsConn.Close() + + // This avoids stale connections when validating the ACME challenge, + // as we expect a validation request to complete in a short period of time. + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + if err := tlsConn.HandshakeContext(ctx); err != nil { + log.FromContext(ctx).WithError(err).Debug("Error during ACME-TLS/1 handshake") + } }) } diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index 55b4b4f11..ecf556b23 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -679,6 +679,64 @@ func Test_Routing(t *testing.T) { } } +func Test_Router_acmeTLSALPNHandlerTimeout(t *testing.T) { + router, err := NewRouter() + require.NoError(t, err) + + router.httpsTLSConfig = &tls.Config{} + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + acceptCh := make(chan struct{}, 1) + go func() { + close(acceptCh) + + conn, err := listener.Accept() + require.NoError(t, err) + + defer listener.Close() + + router.acmeTLSALPNHandler(). + ServeTCP(conn.(*net.TCPConn)) + }() + + <-acceptCh + + conn, err := net.DialTimeout("tcp", listener.Addr().String(), 2*time.Second) + require.NoError(t, err) + + // This is a minimal truncated Client Hello message + // to simulate a hanging connection during TLS handshake. + clientHello := []byte{ + // TLS Record Header + 0x16, // Content Type: Handshake + 0x03, 0x01, // Version: TLS 1.0 (for compatibility) + 0x00, 0x50, // Length: 80 bytes + } + + _, err = conn.Write(clientHello) + require.NoError(t, err) + + errCh := make(chan error, 1) + go func() { + // This will return an EOF as the acmeTLSALPNHandler will close the connection + // after a timeout during the TLS handshake. + b := make([]byte, 256) + _, err = conn.Read(b) + + errCh <- err + }() + + select { + case err := <-errCh: + assert.ErrorIs(t, err, io.EOF) + + case <-time.After(3 * time.Second): + t.Fatal("Error: Timeout waiting for acmeTLSALPNHandler to close the connection") + } +} + // routerTCPCatchAll configures a TCP CatchAll No TLS - HostSNI(`*`) router. func routerTCPCatchAll(conf *runtime.Configuration) { conf.TCPRouters["tcp-catchall"] = &runtime.TCPRouterInfo{