Fix TCP-TLS/HTTPS routing precedence

Co-authored-by: Mathieu Lonjaret <mathieu.lonjaret@gmail.com>
This commit is contained in:
Romain 2022-05-19 16:44:14 +02:00 committed by GitHub
parent ede2be1f66
commit ac4086d0ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 1031 additions and 36 deletions

View file

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

View file

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

View file

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

View file

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