Enforce handling of ACME-TLS/1 challenges

Co-authored-by: Baptiste Mayelle <baptiste.mayelle@traefik.io>
Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
Romain 2024-03-29 11:36:05 +01:00 committed by GitHub
parent 167bdb0d53
commit c31f5df854
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 87 additions and 2 deletions

View file

@ -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)

View file

@ -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) {