diff --git a/pkg/muxer/tcp/mux.go b/pkg/muxer/tcp/mux.go index b82675614..a70e5fd34 100644 --- a/pkg/muxer/tcp/mux.go +++ b/pkg/muxer/tcp/mux.go @@ -95,32 +95,21 @@ func NewMuxer() (*Muxer, error) { return &Muxer{parser: parser}, nil } -// Match returns the handler of the first route matching the connection metadata. -func (m Muxer) Match(meta ConnData) tcp.Handler { +// Match returns the handler of the first route matching the connection metadata, +// and whether the match is exactly from the rule HostSNI(*). +func (m Muxer) Match(meta ConnData) (tcp.Handler, bool) { for _, route := range m.routes { if route.matchers.match(meta) { - return route.handler + return route.handler, route.catchAll } } - return nil + return nil, false } // AddRoute adds a new route, associated to the given handler, at the given // priority, to the muxer. func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error { - // Special case for when the catchAll fallback is present. - // When no user-defined priority is found, the lowest computable priority minus one is used, - // in order to make the fallback the last to be evaluated. - if priority == 0 && rule == "HostSNI(`*`)" { - priority = -1 - } - - // Default value, which means the user has not set it, so we'll compute it. - if priority == 0 { - priority = len(rule) - } - parse, err := m.parser.Parse(rule) if err != nil { return fmt.Errorf("error while parsing rule %s: %w", rule, err) @@ -131,16 +120,36 @@ func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error { return fmt.Errorf("error while parsing rule %s", rule) } + ruleTree := buildTree() + var matchers matchersTree - err = addRule(&matchers, buildTree()) + err = addRule(&matchers, ruleTree) if err != nil { return err } + var catchAll bool + if ruleTree.RuleLeft == nil && ruleTree.RuleRight == nil && len(ruleTree.Value) == 1 { + catchAll = ruleTree.Value[0] == "*" && strings.EqualFold(ruleTree.Matcher, "HostSNI") + } + + // Special case for when the catchAll fallback is present. + // When no user-defined priority is found, the lowest computable priority minus one is used, + // in order to make the fallback the last to be evaluated. + if priority == 0 && catchAll { + priority = -1 + } + + // Default value, which means the user has not set it, so we'll compute it. + if priority == 0 { + priority = len(rule) + } + newRoute := &route{ handler: handler, - priority: priority, matchers: matchers, + catchAll: catchAll, + priority: priority, } m.routes = append(m.routes, newRoute) @@ -207,9 +216,10 @@ type route struct { matchers matchersTree // handler responsible for handling the route. handler tcp.Handler - - // Used to disambiguate between two (or more) rules that would both match for a - // given request. + // catchAll indicates whether the route rule has exactly the catchAll value (HostSNI(`*`)). + catchAll bool + // priority is used to disambiguate between two (or more) rules that would + // all match for a given request. // Computed from the matching rule length, if not user-set. priority int } diff --git a/pkg/muxer/tcp/mux_test.go b/pkg/muxer/tcp/mux_test.go index 65f3021b7..82119319e 100644 --- a/pkg/muxer/tcp/mux_test.go +++ b/pkg/muxer/tcp/mux_test.go @@ -474,7 +474,7 @@ func Test_addTCPRoute(t *testing.T) { connData, err := NewConnData(test.serverName, conn) require.NoError(t, err) - matchingHandler := router.Match(connData) + matchingHandler, _ := router.Match(connData) if test.matchErr { require.Nil(t, matchingHandler) return @@ -568,6 +568,54 @@ func TestParseHostSNI(t *testing.T) { } } +func Test_HostSNICatchAll(t *testing.T) { + testCases := []struct { + desc string + rule string + isCatchAll bool + }{ + { + desc: "HostSNI(`foobar`) is not catchAll", + rule: "HostSNI(`foobar`)", + }, + { + desc: "HostSNI(`*`) is catchAll", + rule: "HostSNI(`*`)", + isCatchAll: true, + }, + { + desc: "HOSTSNI(`*`) is catchAll", + rule: "HOSTSNI(`*`)", + isCatchAll: true, + }, + { + desc: `HostSNI("*") is catchAll`, + rule: `HostSNI("*")`, + isCatchAll: true, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {})) + require.NoError(t, err) + + handler, catchAll := muxer.Match(ConnData{ + serverName: "foobar", + }) + require.NotNil(t, handler) + assert.Equal(t, test.isCatchAll, catchAll) + }) + } +} + func Test_HostSNI(t *testing.T) { testCases := []struct { desc string @@ -934,7 +982,7 @@ func Test_Priority(t *testing.T) { require.NoError(t, err) } - handler := muxer.Match(ConnData{ + handler, _ := muxer.Match(ConnData{ serverName: test.serverName, remoteIP: test.remoteIP, }) diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index d9b5327c0..9b42b7ebe 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -93,7 +93,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { return } - handler := r.muxerTCP.Match(connData) + handler, _ := r.muxerTCP.Match(connData) // If there is a handler matching the connection metadata, // we let it handle the connection. if handler != nil { @@ -133,7 +133,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { } if !tls { - handler := r.muxerTCP.Match(connData) + handler, _ := r.muxerTCP.Match(connData) switch { case handler != nil: handler.ServeTCP(r.GetConn(conn, peeked)) @@ -145,20 +145,38 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { return } - handler := r.muxerTCPTLS.Match(connData) - if handler != nil { - handler.ServeTCP(r.GetConn(conn, peeked)) + // 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). + // The added value from doing Match is to find and use the specific TLS config + // (wrapped inside the returned handler) requested for the given HostSNI. + handlerHTTPS, catchAllHTTPS := r.muxerHTTPS.Match(connData) + if handlerHTTPS != nil && !catchAllHTTPS { + // In order not to depart from the behavior in 2.6, we only allow an HTTPS router + // to take precedence over a TCP-TLS router if it is _not_ an HostSNI(*) router (so + // basically any router that has a specific HostSNI based rule). + handlerHTTPS.ServeTCP(r.GetConn(conn, peeked)) return } - // for real, the handler returned here 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). - // The added value from doing Match, is to find and use the specific TLS config - // requested for the given HostSNI. - handler = r.muxerHTTPS.Match(connData) - if handler != nil { - handler.ServeTCP(r.GetConn(conn, peeked)) + // Contains also TCP TLS passthrough routes. + handlerTCPTLS, catchAllTCPTLS := r.muxerTCPTLS.Match(connData) + if handlerTCPTLS != nil && !catchAllTCPTLS { + handlerTCPTLS.ServeTCP(r.GetConn(conn, peeked)) + return + } + + // Fallback on HTTPS catchAll. + // We end up here for e.g. an HTTPS router that only has a PathPrefix rule, + // which under the scenes is counted as an HostSNI(*) rule. + if handlerHTTPS != nil { + handlerHTTPS.ServeTCP(r.GetConn(conn, peeked)) + return + } + + // Fallback on TCP TLS catchAll. + if handlerTCPTLS != nil { + handlerTCPTLS.ServeTCP(r.GetConn(conn, peeked)) return } diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go new file mode 100644 index 000000000..f9e9978cf --- /dev/null +++ b/pkg/server/router/tcp/router_test.go @@ -0,0 +1,919 @@ +package tcp + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traefik/traefik/v2/pkg/config/dynamic" + "github.com/traefik/traefik/v2/pkg/config/runtime" + tcpmiddleware "github.com/traefik/traefik/v2/pkg/server/middleware/tcp" + "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" +) + +type applyRouter func(conf *runtime.Configuration) + +type checkRouter func(addr string, timeout time.Duration) error + +type httpForwarder struct { + net.Listener + connChan chan net.Conn + errChan chan error +} + +func newHTTPForwarder(ln net.Listener) *httpForwarder { + return &httpForwarder{ + Listener: ln, + connChan: make(chan net.Conn), + errChan: make(chan error), + } +} + +// Close closes the Listener. +func (h *httpForwarder) Close() error { + h.errChan <- http.ErrServerClosed + + return nil +} + +// ServeTCP uses the connection to serve it later in "Accept". +func (h *httpForwarder) ServeTCP(conn tcp2.WriteCloser) { + h.connChan <- conn +} + +// Accept retrieves a served connection in ServeTCP. +func (h *httpForwarder) Accept() (net.Conn, error) { + select { + case conn := <-h.connChan: + return conn, nil + case err := <-h.errChan: + return nil, err + } +} + +// Test_Routing aims to settle the behavior between routers of different types on the same TCP entryPoint. +// It has been introduced as a regression test following a fix on the v2.7 TCP Muxer. +// +// The routing precedence is roughly as follows: +// - TCP over HTTP +// - HTTPS over TCP-TLS +// +// Discrepancies for server sending first bytes support: +// - On v2.6, it is possible as long as you have one and only one TCP Non-TLS HostSNI(`*`) router (so called CatchAllNoTLS) defined. +// - On v2.7, it is possible as long as you have zero TLS/HTTPS router defined. +// +// Discrepancies in routing precedence between TCP and HTTP routers: +// - TCP HostSNI(`*`) and HTTP Host(`foobar`) +// - On v2.6 and v2.7, the TCP one takes precedence. +// +// - TCP ClientIP(`[::]`) and HTTP Host(`foobar`) +// - On v2.6, ClientIP matcher doesn't exist. +// - On v2.7, the TCP one takes precedence. +// +// Routing precedence between TCP-TLS and HTTPS routers (considering a request/connection with the servername "foobar"): +// - TCP-TLS HostSNI(`*`) and HTTPS Host(`foobar`) +// - On v2.6 and v2.7, the HTTPS one takes precedence. +// +// - TCP-TLS HostSNI(`foobar`) and HTTPS Host(`foobar`) +// - On v2.6 and v2.7, the HTTPS one takes precedence (overriding the TCP-TLS one in v2.6). +// +// - TCP-TLS HostSNI(`*`) and HTTPS PathPrefix(`/`) +// - On v2.6 and v2.7, the HTTPS one takes precedence (overriding the TCP-TLS one in v2.6). +// +// - TCP-TLS HostSNI(`foobar`) and HTTPS PathPrefix(`/`) +// - On v2.6 and v2.7, the TCP-TLS one takes precedence. +// +func Test_Routing(t *testing.T) { + // This listener simulates the backend service. + // It is capable of switching into server first communication mode, + // if the client takes to long to send the first bytes. + tcpBackendListener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + // This allows the closing of the TCP backend listener to happen last. + t.Cleanup(func() { + tcpBackendListener.Close() + }) + + go func() { + for { + conn, err := tcpBackendListener.Accept() + if err != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Temporary() { + continue + } + + return + } + + err = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)) + if err != nil { + return + } + + buf := make([]byte, 100) + _, err = conn.Read(buf) + + var opErr *net.OpError + if err == nil { + _, err = fmt.Fprint(conn, "TCP-CLIENT-FIRST") + require.NoError(t, err) + } else if errors.As(err, &opErr) && opErr.Timeout() { + _, err = fmt.Fprint(conn, "TCP-SERVER-FIRST") + require.NoError(t, err) + } + + err = conn.Close() + require.NoError(t, err) + } + }() + + // Configuration defining the TCP backend service, used by TCP routers later. + conf := &runtime.Configuration{ + TCPServices: map[string]*runtime.TCPServiceInfo{ + "tcp": { + TCPService: &dynamic.TCPService{ + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: tcpBackendListener.Addr().String(), + }, + }, + }, + }, + }, + }, + } + + serviceManager := tcp.NewManager(conf) + + // 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.Options{ + "default": { + MaxVersion: "VersionTLS10", + }, + "tls10": { + MaxVersion: "VersionTLS10", + }, + "tls12": { + MinVersion: "VersionTLS12", + MaxVersion: "VersionTLS12", + }, + }, + []*traefiktls.CertAndStores{}) + + middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares) + + manager := NewManager(conf, serviceManager, middlewaresBuilder, + nil, nil, tlsManager) + + type checkCase struct { + checkRouter + + desc string + expectedError string + timeout time.Duration + } + + testCases := []struct { + desc string + routers []applyRouter + checks []checkCase + }{ + { + desc: "No routers", + routers: []applyRouter{}, + checks: []checkCase{ + { + desc: "TCP with client sending first bytes should fail", + checkRouter: checkTCPClientFirst, + expectedError: "i/o timeout", + }, + { + desc: "TCP with server sending first bytes should fail", + checkRouter: checkTCPServerFirst, + expectedError: "i/o timeout", + }, + { + desc: "HTTP request should be handled by HTTP service (404)", + checkRouter: checkHTTP, + }, + { + desc: "TCP TLS 1.0 connection should fail", + checkRouter: checkTCPTLS10, + expectedError: "i/o timeout", + }, + { + desc: "TCP TLS 1.2 connection should fail", + checkRouter: checkTCPTLS12, + // The HTTPS forwarder catches the connection with the TLS 1.0 config, + // because no matching routes are defined with the custom TLS Config. + expectedError: "wrong TLS version", + }, + { + desc: "HTTPS TLS 1.0 request should be handled by HTTPS (HTTPS forwarder with tls 1.0 config) (404)", + checkRouter: checkHTTPSTLS10, + }, + { + desc: "HTTPS TLS 1.2 request should fail", + checkRouter: checkHTTPSTLS12, + expectedError: "wrong TLS version", + }, + }, + }, + { + desc: "Single TCP CatchAll router", + routers: []applyRouter{routerTCPCatchAll}, + checks: []checkCase{ + { + desc: "TCP with client sending first bytes should be handled by TCP service", + checkRouter: checkTCPClientFirst, + }, + { + desc: "TCP with server sending first bytes should be handled by TCP service", + checkRouter: checkTCPServerFirst, + }, + }, + }, + { + desc: "Single HTTP router", + routers: []applyRouter{routerHTTP}, + checks: []checkCase{ + { + desc: "HTTP request should be handled by HTTP service", + checkRouter: checkHTTP, + }, + }, + }, + { + desc: "Single TCP TLS router", + routers: []applyRouter{routerTCPTLS}, + checks: []checkCase{ + { + desc: "TCP TLS 1.0 connection should fail", + checkRouter: checkTCPTLS10, + expectedError: "wrong TLS version", + }, + { + desc: "TCP TLS 1.2 connection should be handled by TCP service", + checkRouter: checkTCPTLS12, + }, + }, + }, + { + desc: "Single TCP TLS CatchAll router", + routers: []applyRouter{routerTCPTLSCatchAll}, + checks: []checkCase{ + { + desc: "TCP TLS 1.0 connection should be handled by TCP service", + checkRouter: checkTCPTLS10, + }, + { + desc: "TCP TLS 1.2 connection should fail", + checkRouter: checkTCPTLS12, + expectedError: "wrong TLS version", + }, + }, + }, + { + desc: "Single HTTPS router", + routers: []applyRouter{routerHTTPS}, + checks: []checkCase{ + { + desc: "HTTPS TLS 1.0 request should fail", + checkRouter: checkHTTPSTLS10, + expectedError: "wrong TLS version", + }, + { + desc: "HTTPS TLS 1.2 request should be handled by HTTPS service", + checkRouter: checkHTTPSTLS12, + }, + }, + }, + { + desc: "Single HTTPS PathPrefix router", + routers: []applyRouter{routerHTTPSPathPrefix}, + checks: []checkCase{ + { + desc: "HTTPS TLS 1.0 request should be handled by HTTPS service", + checkRouter: checkHTTPSTLS10, + }, + { + desc: "HTTPS TLS 1.2 request should fail", + checkRouter: checkHTTPSTLS12, + expectedError: "wrong TLS version", + }, + }, + }, + { + desc: "TCP CatchAll router && HTTP router", + routers: []applyRouter{routerTCPCatchAll, routerHTTP}, + checks: []checkCase{ + { + desc: "TCP client sending first bytes should be handled by TCP service", + checkRouter: checkTCPClientFirst, + }, + { + desc: "TCP server sending first bytes should be handled by TCP service", + checkRouter: checkTCPServerFirst, + }, + { + desc: "HTTP request should fail, because handled by TCP service", + checkRouter: checkHTTP, + expectedError: "malformed HTTP response", + }, + }, + }, + { + desc: "TCP TLS CatchAll router && HTTP router", + routers: []applyRouter{routerTCPTLS, routerHTTP}, + checks: []checkCase{ + { + desc: "TCP TLS 1.0 connection should fail", + checkRouter: checkTCPTLS10, + expectedError: "wrong TLS version", + }, + { + desc: "TCP TLS 1.2 connection should be handled by TCP service", + checkRouter: checkTCPTLS12, + }, + { + desc: "HTTP request should be handled by HTTP service", + checkRouter: checkHTTP, + }, + }, + }, + { + desc: "TCP CatchAll router && HTTPS router", + routers: []applyRouter{routerTCPCatchAll, routerHTTPS}, + checks: []checkCase{ + { + desc: "TCP client sending first bytes should be handled by TCP service", + checkRouter: checkTCPClientFirst, + }, + { + desc: "TCP server sending first bytes should timeout on clientHello", + checkRouter: checkTCPServerFirst, + expectedError: "i/o timeout", + }, + { + desc: "HTTP request should fail, because handled by TCP service", + checkRouter: checkHTTP, + expectedError: "malformed HTTP response", + }, + { + desc: "HTTPS TLS 1.0 request should be handled by HTTPS service", + checkRouter: checkHTTPSTLS10, + expectedError: "wrong TLS version", + }, + { + desc: "HTTPS TLS 1.2 request should be handled by HTTPS service", + checkRouter: checkHTTPSTLS12, + }, + }, + }, + { + // We show that a not CatchAll HTTPS router takes priority over a TCP-TLS router. + desc: "TCP TLS router && HTTPS router", + routers: []applyRouter{routerTCPTLS, routerHTTPS}, + checks: []checkCase{ + { + desc: "TCP TLS 1.0 connection should fail", + checkRouter: checkTCPTLS10, + expectedError: "wrong TLS version", + }, + { + desc: "TCP TLS 1.2 connection should fail", + checkRouter: checkTCPTLS12, + // The connection is handled by the HTTPS router, + // which has the correct TLS config, + // but the HTTP server is detecting a malformed request which ends with a timeout. + expectedError: "i/o timeout", + }, + { + desc: "HTTPS TLS 1.0 request should fail", + checkRouter: checkHTTPSTLS10, + expectedError: "wrong TLS version", + }, + { + desc: "HTTPS TLS 1.2 request should be handled by HTTPS service", + checkRouter: checkHTTPSTLS12, + }, + }, + }, + { + // We show that a not CatchAll HTTPS router takes priority over a CatchAll TCP-TLS router. + desc: "TCP TLS CatchAll router && HTTPS router", + routers: []applyRouter{routerTCPCatchAll, routerHTTPS}, + checks: []checkCase{ + { + desc: "TCP TLS 1.0 connection should fail", + checkRouter: checkTCPTLS10, + expectedError: "wrong TLS version", + }, + { + desc: "TCP TLS 1.2 connection should fail", + checkRouter: checkTCPTLS12, + // The connection is handled by the HTTPS router, + // which has the correct TLS config, + // but the HTTP server is detecting a malformed request which ends with a timeout. + expectedError: "i/o timeout", + }, + { + desc: "HTTPS TLS 1.0 request should fail", + checkRouter: checkHTTPSTLS10, + expectedError: "wrong TLS version", + }, + { + desc: "HTTPS TLS 1.2 request should be handled by HTTPS service", + checkRouter: checkHTTPSTLS12, + }, + }, + }, + { + // We show that TCP-TLS router (not CatchAll) takes priority over non-Host rule HTTPS router (CatchAll). + desc: "TCP TLS router && HTTPS Path prefix router", + routers: []applyRouter{routerTCPTLS, routerHTTPSPathPrefix}, + checks: []checkCase{ + { + desc: "TCP TLS 1.0 connection should fail", + checkRouter: checkTCPTLS10, + expectedError: "wrong TLS version", + }, + { + desc: "TCP TLS 1.2 connection should be handled by TCP service", + checkRouter: checkTCPTLS12, + }, + { + desc: "HTTPS TLS 1.0 request should fail", + checkRouter: checkHTTPSTLS10, + expectedError: "malformed HTTP response", + }, + { + desc: "HTTPS TLS 1.2 should fail", + checkRouter: checkHTTPSTLS12, + expectedError: "malformed HTTP response", + }, + }, + }, + { + desc: "TCP TLS router && TCP TLS CatchAll router", + routers: []applyRouter{routerTCPTLS, routerTCPTLSCatchAll}, + checks: []checkCase{ + { + desc: "TCP TLS 1.0 connection should fail", + checkRouter: checkTCPTLS10, + expectedError: "wrong TLS version", + }, + { + desc: "TCP TLS 1.2 connection should be handled by TCP service", + checkRouter: checkTCPTLS12, + }, + }, + }, + { + desc: "All routers, all checks", + routers: []applyRouter{routerTCPCatchAll, routerHTTP, routerHTTPS, routerTCPTLS, routerTCPTLSCatchAll}, + checks: []checkCase{ + { + desc: "TCP client sending first bytes should be handled by TCP service", + checkRouter: checkTCPClientFirst, + }, + { + desc: "TCP server sending first bytes should timeout on clientHello", + checkRouter: checkTCPServerFirst, + expectedError: "i/o timeout", + }, + { + desc: "HTTP request should fail, because handled by TCP service", + checkRouter: checkHTTP, + expectedError: "malformed HTTP response", + }, + { + desc: "HTTPS TLS 1.0 request should fail", + checkRouter: checkHTTPSTLS10, + expectedError: "wrong TLS version", + }, + { + desc: "HTTPS TLS 1.2 request should be handled by HTTPS service", + checkRouter: checkHTTPSTLS12, + }, + { + desc: "TCP TLS 1.0 connection should fail", + checkRouter: checkTCPTLS10, + expectedError: "wrong TLS version", + }, + { + desc: "TCP TLS 1.2 connection should fail", + checkRouter: checkTCPTLS12, + // The connection is handled by the HTTPS router, + // witch have the correct TLS config, + // but the HTTP server is detecting a malformed request which ends with a timeout. + expectedError: "i/o timeout", + }, + }, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dynConf := &runtime.Configuration{ + Routers: map[string]*runtime.RouterInfo{}, + TCPRouters: map[string]*runtime.TCPRouterInfo{}, + } + + for _, router := range test.routers { + router(dynConf) + } + + router, err := manager.buildEntryPointHandler(context.Background(), dynConf.TCPRouters, dynConf.Routers, nil, nil) + require.NoError(t, err) + + epListener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + // serverHTTP handler returns only the "HTTP" value as body for further checks. + serverHTTP := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err = fmt.Fprint(w, "HTTP") + require.NoError(t, err) + }), + } + + stoppedHTTP := make(chan struct{}) + forwarder := newHTTPForwarder(epListener) + go func() { + defer close(stoppedHTTP) + _ = serverHTTP.Serve(forwarder) + }() + + router.SetHTTPForwarder(forwarder) + + // serverHTTPS handler returns only the "HTTPS" value as body for further checks. + serverHTTPS := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err = fmt.Fprint(w, "HTTPS") + require.NoError(t, err) + }), + } + + stoppedHTTPS := make(chan struct{}) + httpsForwarder := newHTTPForwarder(epListener) + go func() { + defer close(stoppedHTTPS) + _ = serverHTTPS.Serve(httpsForwarder) + }() + + router.SetHTTPSForwarder(httpsForwarder) + + stoppedTCP := make(chan struct{}) + go func() { + defer close(stoppedTCP) + for { + conn, err := epListener.Accept() + if err != nil { + return + } + + tcpConn, ok := conn.(*net.TCPConn) + if !ok { + t.Error("not a write closer") + } + + router.ServeTCP(tcpConn) + } + }() + + for _, check := range test.checks { + timeout := 2 * time.Second + if check.timeout > 0 { + timeout = check.timeout + } + + err := check.checkRouter(epListener.Addr().String(), timeout) + + if check.expectedError != "" { + require.NotNil(t, err, check.desc) + assert.Contains(t, err.Error(), check.expectedError, check.desc) + continue + } + + assert.Nil(t, err, check.desc) + } + + epListener.Close() + + <-stoppedTCP + + serverHTTP.Close() + serverHTTPS.Close() + + <-stoppedHTTP + <-stoppedHTTPS + }) + } +} + +// routerTCPCatchAll configures a TCP CatchAll No TLS - HostSNI(`*`) router. +func routerTCPCatchAll(conf *runtime.Configuration) { + conf.TCPRouters["tcp-catchall"] = &runtime.TCPRouterInfo{ + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "tcp", + Rule: "HostSNI(`*`)", + }, + } +} + +// routerHTTP configures an HTTP - Host(`foo.bar`) router. +func routerHTTP(conf *runtime.Configuration) { + conf.Routers["http"] = &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "http", + Rule: "Host(`foo.bar`)", + }, + } +} + +// routerTCPTLSCatchAll a TCP TLS CatchAll - HostSNI(`*`) router with TLS 1.0 config. +func routerTCPTLSCatchAll(conf *runtime.Configuration) { + conf.TCPRouters["tcp-tls-catchall"] = &runtime.TCPRouterInfo{ + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "tcp", + Rule: "HostSNI(`*`)", + TLS: &dynamic.RouterTCPTLSConfig{ + Options: "tls10", + }, + }, + } +} + +// 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{ + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "tcp", + Rule: "HostSNI(`foo.bar`)", + TLS: &dynamic.RouterTCPTLSConfig{ + Options: "tls12", + }, + }, + } +} + +// routerHTTPSPathPrefix configures an HTTPS - PathPrefix(`/`) router with TLS 1.0 config. +func routerHTTPSPathPrefix(conf *runtime.Configuration) { + conf.Routers["https"] = &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "http", + Rule: "PathPrefix(`/`)", + TLS: &dynamic.RouterTLSConfig{ + Options: "tls10", + }, + }, + } +} + +// routerHTTPS configures an HTTPS - Host(`foo.bar`) router with TLS 1.2 config. +func routerHTTPS(conf *runtime.Configuration) { + conf.Routers["https-custom-tls"] = &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "http", + Rule: "Host(`foo.bar`)", + TLS: &dynamic.RouterTLSConfig{ + Options: "tls12", + }, + }, + } +} + +// 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) { + conn, err := net.Dial("tcp", addr) + if err != nil { + return err + } + defer func() { + closeErr := conn.Close() + if closeErr != nil && err == nil { + err = closeErr + } + }() + + fmt.Fprint(conn, "HELLO") + + err = conn.SetReadDeadline(time.Now().Add(timeout)) + if err != nil { + return + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, conn) + if err != nil { + return err + } + + if !strings.HasPrefix(buf.String(), "TCP-CLIENT-FIRST") { + return fmt.Errorf("unexpected response: %s", buf.String()) + } + + return nil +} + +// checkTCPServerFirst simulates a TCP client waiting for the server first bytes. +// It returns an error if it doesn't receive the expected response. +func checkTCPServerFirst(addr string, timeout time.Duration) (err error) { + conn, err := net.Dial("tcp", addr) + if err != nil { + return err + } + defer func() { + closeErr := conn.Close() + if closeErr != nil && err == nil { + err = closeErr + } + }() + + err = conn.SetReadDeadline(time.Now().Add(timeout)) + if err != nil { + return + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, conn) + if err != nil { + return err + } + + if !strings.HasPrefix(buf.String(), "TCP-SERVER-FIRST") { + return fmt.Errorf("unexpected response: %s", buf.String()) + } + + return nil +} + +// checkHTTP simulates an HTTP client. +// It returns an error if it doesn't receive the expected response. +func checkHTTP(addr string, timeout time.Duration) error { + httpClient := &http.Client{Timeout: timeout} + + req, err := http.NewRequest(http.MethodGet, "http://"+addr, nil) + if err != nil { + return err + } + req.Header.Set("Host", "foo.bar") + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if !strings.Contains(string(body), "HTTP") { + return fmt.Errorf("unexpected response: %s", string(body)) + } + + return nil +} + +// checkTCPTLS simulates a TCP client connection. +// It returns an error if it doesn't receive the expected response. +func checkTCPTLS(addr string, timeout time.Duration, tlsVersion uint16) (err error) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + ServerName: "foo.bar", + MinVersion: tls.VersionTLS10, + MaxVersion: tls.VersionTLS12, + } + 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 != tlsVersion { + return fmt.Errorf("wrong TLS version. wanted %X, got %X", tlsVersion, conn.ConnectionState().Version) + } + + fmt.Fprint(conn, "HELLO") + + err = conn.SetReadDeadline(time.Now().Add(timeout)) + if err != nil { + return + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, conn) + if err != nil { + return err + } + + if !strings.HasPrefix(buf.String(), "TCP-CLIENT-FIRST") { + return fmt.Errorf("unexpected response: %s", buf.String()) + } + + return nil +} + +// checkTCPTLS10 simulates a TCP client connection with TLS 1.0. +// It returns an error if it doesn't receive the expected response. +func checkTCPTLS10(addr string, timeout time.Duration) error { + return checkTCPTLS(addr, timeout, tls.VersionTLS10) +} + +// checkTCPTLS12 simulates a TCP client connection with TLS 1.2. +// It returns an error if it doesn't receive the expected response. +func checkTCPTLS12(addr string, timeout time.Duration) error { + return checkTCPTLS(addr, timeout, tls.VersionTLS12) +} + +// checkHTTPS makes an HTTPS request and checks the given TLS. +// It returns an error if it doesn't receive the expected response. +func checkHTTPS(addr string, timeout time.Duration, tlsVersion uint16) error { + req, err := http.NewRequest(http.MethodGet, "https://"+addr, nil) + if err != nil { + return err + } + req.Header.Set("Host", "foo.bar") + + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "foo.bar", + MinVersion: tls.VersionTLS10, + MaxVersion: tls.VersionTLS12, + }, + }, + Timeout: timeout, + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if resp.TLS.Version != tlsVersion { + return fmt.Errorf("wrong TLS version. wanted %X, got %X", tlsVersion, resp.TLS.Version) + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if !strings.Contains(string(body), "HTTPS") { + return fmt.Errorf("unexpected response: %s", string(body)) + } + + return nil +} + +// checkHTTPSTLS10 makes an HTTP request with TLS version 1.0. +// It returns an error if it doesn't receive the expected response. +func checkHTTPSTLS10(addr string, timeout time.Duration) error { + return checkHTTPS(addr, timeout, tls.VersionTLS10) +} + +// checkHTTPSTLS12 makes an HTTP request with TLS version 1.2. +// It returns an error if it doesn't receive the expected response. +func checkHTTPSTLS12(addr string, timeout time.Duration) error { + return checkHTTPS(addr, timeout, tls.VersionTLS12) +}