From c31f5df85446f2998c2e8d363a4940d53fcecc7d Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 29 Mar 2024 11:36:05 +0100 Subject: [PATCH] Enforce handling of ACME-TLS/1 challenges Co-authored-by: Baptiste Mayelle Co-authored-by: Kevin Pollet --- pkg/server/router/tcp/router.go | 19 ++++++++ pkg/server/router/tcp/router_test.go | 70 +++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index dca4e6fae..0d8524398 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -8,8 +8,10 @@ import ( "io" "net" "net/http" + "slices" "time" + "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/traefik/traefik/v2/pkg/log" tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp" "github.com/traefik/traefik/v2/pkg/tcp" @@ -146,6 +148,12 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { return } + // Handling ACME-TLS/1 challenges. + if slices.Contains(hello.protos, tlsalpn01.ACMETLS1Protocol) { + r.acmeTLSALPNHandler().ServeTCP(r.GetConn(conn, hello.peeked)) + return + } + // For real, the handler eventually used for HTTPS is (almost) always the same: // it is the httpsForwarder that is used for all HTTPS connections that match // (which is also incidentally the same used in the last block below for 404s). @@ -190,6 +198,17 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { conn.Close() } +// acmeTLSALPNHandler returns a special handler to solve ACME-TLS/1 challenges. +func (r *Router) acmeTLSALPNHandler() tcp.Handler { + if r.httpsTLSConfig == nil { + return &brokenTLSRouter{} + } + + return tcp.HandlerFunc(func(conn tcp.WriteCloser) { + _ = tls.Server(conn, r.httpsTLSConfig).Handshake() + }) +} + // AddRoute defines a handler for the given rule. func (r *Router) AddRoute(rule string, priority int, target tcp.Handler) error { return r.muxerTCP.AddRoute(rule, priority, target) diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index 70f23b12c..8f6ae37c1 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/traefik/v2/pkg/config/dynamic" @@ -22,6 +23,7 @@ import ( "github.com/traefik/traefik/v2/pkg/server/service/tcp" tcp2 "github.com/traefik/traefik/v2/pkg/tcp" traefiktls "github.com/traefik/traefik/v2/pkg/tls" + "github.com/traefik/traefik/v2/pkg/tls/generate" ) type applyRouter func(conf *runtime.Configuration) @@ -164,11 +166,16 @@ func Test_Routing(t *testing.T) { serviceManager := tcp.NewManager(conf) + certPEM, keyPEM, err := generate.KeyPair("foo.bar", time.Time{}) + require.NoError(t, err) + // Creates the tlsManager and defines the TLS 1.0 and 1.2 TLSOptions. tlsManager := traefiktls.NewManager() tlsManager.UpdateConfigs( context.Background(), - map[string]traefiktls.Store{}, + map[string]traefiktls.Store{ + tlsalpn01.ACMETLS1Protocol: {}, + }, map[string]traefiktls.Options{ "default": { MinVersion: "VersionTLS10", @@ -183,7 +190,10 @@ func Test_Routing(t *testing.T) { MaxVersion: "VersionTLS12", }, }, - []*traefiktls.CertAndStores{}) + []*traefiktls.CertAndStores{{ + Certificate: traefiktls.Certificate{CertFile: traefiktls.FileOrContent(certPEM), KeyFile: traefiktls.FileOrContent(keyPEM)}, + Stores: []string{tlsalpn01.ACMETLS1Protocol}, + }}) middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares) @@ -207,6 +217,10 @@ func Test_Routing(t *testing.T) { desc: "No routers", routers: []applyRouter{}, checks: []checkCase{ + { + desc: "ACME TLS Challenge", + checkRouter: checkACMETLS, + }, { desc: "TCP with client sending first bytes should fail", checkRouter: checkTCPClientFirst, @@ -244,6 +258,16 @@ func Test_Routing(t *testing.T) { }, }, }, + { + desc: "TCP TLS passthrough does not catch ACME TLS", + routers: []applyRouter{routerTCPTLSCatchAllPassthrough}, + checks: []checkCase{ + { + desc: "ACME TLS Challenge", + checkRouter: checkACMETLS, + }, + }, + }, { desc: "Single TCP CatchAll router", routers: []applyRouter{routerTCPCatchAll}, @@ -675,6 +699,21 @@ func routerTCPTLSCatchAll(conf *runtime.Configuration) { } } +// routerTCPTLSCatchAllPassthrough a TCP TLS CatchAll Passthrough - HostSNI(`*`) router with TLS 1.0 config. +func routerTCPTLSCatchAllPassthrough(conf *runtime.Configuration) { + conf.TCPRouters["tcp-tls-catchall-passthrough"] = &runtime.TCPRouterInfo{ + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "tcp", + Rule: "HostSNI(`*`)", + TLS: &dynamic.RouterTCPTLSConfig{ + Options: "tls12", + Passthrough: true, + }, + }, + } +} + // routerTCPTLS configures a TCP TLS - HostSNI(`foo.bar`) router with TLS 1.2 config. func routerTCPTLS(conf *runtime.Configuration) { conf.TCPRouters["tcp-tls"] = &runtime.TCPRouterInfo{ @@ -717,6 +756,33 @@ func routerHTTPS(conf *runtime.Configuration) { } } +// checkACMETLS simulates a ACME TLS Challenge client connection. +// It returns an error if TLS handshake fails. +func checkACMETLS(addr string, _ time.Duration) (err error) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + ServerName: "foo.bar", + MinVersion: tls.VersionTLS10, + NextProtos: []string{tlsalpn01.ACMETLS1Protocol}, + } + conn, err := tls.Dial("tcp", addr, tlsConfig) + if err != nil { + return err + } + defer func() { + closeErr := conn.Close() + if closeErr != nil && err == nil { + err = closeErr + } + }() + + if conn.ConnectionState().Version != tls.VersionTLS10 { + return fmt.Errorf("wrong TLS version. wanted %X, got %X", tls.VersionTLS10, conn.ConnectionState().Version) + } + + return nil +} + // checkTCPClientFirst simulates a TCP client sending first bytes first. // It returns an error if it doesn't receive the expected response. func checkTCPClientFirst(addr string, timeout time.Duration) (err error) {