mirror of
				https://github.com/traefik/traefik.git
				synced 2025-10-31 08:21:27 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			425 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			425 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package integration
 | |
| 
 | |
| import (
 | |
| 	"crypto/tls"
 | |
| 	"crypto/x509"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"net/http/httptest"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| 	"github.com/stretchr/testify/suite"
 | |
| 	"github.com/traefik/traefik/v3/integration/try"
 | |
| )
 | |
| 
 | |
| type TCPSuite struct{ BaseSuite }
 | |
| 
 | |
| func TestTCPSuite(t *testing.T) {
 | |
| 	suite.Run(t, new(TCPSuite))
 | |
| }
 | |
| 
 | |
| func (s *TCPSuite) SetupSuite() {
 | |
| 	s.BaseSuite.SetupSuite()
 | |
| 
 | |
| 	s.createComposeProject("tcp")
 | |
| 	s.composeUp()
 | |
| }
 | |
| 
 | |
| func (s *TCPSuite) TearDownSuite() {
 | |
| 	s.BaseSuite.TearDownSuite()
 | |
| }
 | |
| 
 | |
| func (s *TCPSuite) TestMixed() {
 | |
| 	file := s.adaptFile("fixtures/tcp/mixed.toml", struct {
 | |
| 		Whoami       string
 | |
| 		WhoamiA      string
 | |
| 		WhoamiB      string
 | |
| 		WhoamiNoCert string
 | |
| 	}{
 | |
| 		Whoami:       "http://" + s.getComposeServiceIP("whoami") + ":80",
 | |
| 		WhoamiA:      s.getComposeServiceIP("whoami-a") + ":8080",
 | |
| 		WhoamiB:      s.getComposeServiceIP("whoami-b") + ":8080",
 | |
| 		WhoamiNoCert: s.getComposeServiceIP("whoami-no-cert") + ":8080",
 | |
| 	})
 | |
| 
 | |
| 	s.traefikCmd(withConfigFile(file))
 | |
| 
 | |
| 	err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("Path(`/test`)"))
 | |
| 	require.NoError(s.T(), err)
 | |
| 
 | |
| 	// Traefik passes through, termination handled by whoami-a
 | |
| 	out, err := guessWhoTLSPassthrough("127.0.0.1:8093", "whoami-a.test")
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "whoami-a")
 | |
| 
 | |
| 	// Traefik passes through, termination handled by whoami-b
 | |
| 	out, err = guessWhoTLSPassthrough("127.0.0.1:8093", "whoami-b.test")
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "whoami-b")
 | |
| 
 | |
| 	// Termination handled by traefik
 | |
| 	out, err = guessWho("127.0.0.1:8093", "whoami-c.test", true)
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "whoami-no-cert")
 | |
| 
 | |
| 	tr1 := &http.Transport{
 | |
| 		TLSClientConfig: &tls.Config{
 | |
| 			InsecureSkipVerify: true,
 | |
| 		},
 | |
| 	}
 | |
| 	req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:8093/whoami/", nil)
 | |
| 	require.NoError(s.T(), err)
 | |
| 	err = try.RequestWithTransport(req, 10*time.Second, tr1, try.StatusCodeIs(http.StatusOK))
 | |
| 	require.NoError(s.T(), err)
 | |
| 
 | |
| 	req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:8093/not-found/", nil)
 | |
| 	require.NoError(s.T(), err)
 | |
| 	err = try.RequestWithTransport(req, 10*time.Second, tr1, try.StatusCodeIs(http.StatusNotFound))
 | |
| 	require.NoError(s.T(), err)
 | |
| 
 | |
| 	err = try.GetRequest("http://127.0.0.1:8093/test", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
 | |
| 	require.NoError(s.T(), err)
 | |
| 	err = try.GetRequest("http://127.0.0.1:8093/not-found", 500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound))
 | |
| 	require.NoError(s.T(), err)
 | |
| }
 | |
| 
 | |
| func (s *TCPSuite) TestTLSOptions() {
 | |
| 	file := s.adaptFile("fixtures/tcp/multi-tls-options.toml", struct {
 | |
| 		WhoamiNoCert string
 | |
| 	}{
 | |
| 		WhoamiNoCert: s.getComposeServiceIP("whoami-no-cert") + ":8080",
 | |
| 	})
 | |
| 
 | |
| 	s.traefikCmd(withConfigFile(file))
 | |
| 
 | |
| 	err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("HostSNI(`whoami-c.test`)"))
 | |
| 	require.NoError(s.T(), err)
 | |
| 
 | |
| 	// Check that we can use a client tls version <= 1.2 with hostSNI 'whoami-c.test'
 | |
| 	out, err := guessWhoTLSMaxVersion("127.0.0.1:8093", "whoami-c.test", true, tls.VersionTLS12)
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "whoami-no-cert")
 | |
| 
 | |
| 	// Check that we can use a client tls version <= 1.3 with hostSNI 'whoami-d.test'
 | |
| 	out, err = guessWhoTLSMaxVersion("127.0.0.1:8093", "whoami-d.test", true, tls.VersionTLS13)
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "whoami-no-cert")
 | |
| 
 | |
| 	// Check that we cannot use a client tls version <= 1.2 with hostSNI 'whoami-d.test'
 | |
| 	_, err = guessWhoTLSMaxVersion("127.0.0.1:8093", "whoami-d.test", true, tls.VersionTLS12)
 | |
| 	assert.ErrorContains(s.T(), err, "protocol version not supported")
 | |
| 
 | |
| 	// Check that we can't reach a route with an invalid mTLS configuration.
 | |
| 	conn, err := tls.Dial("tcp", "127.0.0.1:8093", &tls.Config{
 | |
| 		ServerName:         "whoami-i.test",
 | |
| 		InsecureSkipVerify: true,
 | |
| 	})
 | |
| 	assert.Nil(s.T(), conn)
 | |
| 	assert.Error(s.T(), err)
 | |
| }
 | |
| 
 | |
| func (s *TCPSuite) TestNonTLSFallback() {
 | |
| 	file := s.adaptFile("fixtures/tcp/non-tls-fallback.toml", struct {
 | |
| 		WhoamiA      string
 | |
| 		WhoamiB      string
 | |
| 		WhoamiNoCert string
 | |
| 		WhoamiNoTLS  string
 | |
| 	}{
 | |
| 		WhoamiA:      s.getComposeServiceIP("whoami-a") + ":8080",
 | |
| 		WhoamiB:      s.getComposeServiceIP("whoami-b") + ":8080",
 | |
| 		WhoamiNoCert: s.getComposeServiceIP("whoami-no-cert") + ":8080",
 | |
| 		WhoamiNoTLS:  s.getComposeServiceIP("whoami-no-tls") + ":8080",
 | |
| 	})
 | |
| 
 | |
| 	s.traefikCmd(withConfigFile(file))
 | |
| 
 | |
| 	err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("HostSNI(`*`)"))
 | |
| 	require.NoError(s.T(), err)
 | |
| 
 | |
| 	// Traefik passes through, termination handled by whoami-a
 | |
| 	out, err := guessWhoTLSPassthrough("127.0.0.1:8093", "whoami-a.test")
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "whoami-a")
 | |
| 
 | |
| 	// Traefik passes through, termination handled by whoami-b
 | |
| 	out, err = guessWhoTLSPassthrough("127.0.0.1:8093", "whoami-b.test")
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "whoami-b")
 | |
| 
 | |
| 	// Termination handled by traefik
 | |
| 	out, err = guessWho("127.0.0.1:8093", "whoami-c.test", true)
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "whoami-no-cert")
 | |
| 
 | |
| 	out, err = guessWho("127.0.0.1:8093", "", false)
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "whoami-no-tls")
 | |
| }
 | |
| 
 | |
| func (s *TCPSuite) TestNonTlsTcp() {
 | |
| 	file := s.adaptFile("fixtures/tcp/non-tls.toml", struct {
 | |
| 		WhoamiNoTLS string
 | |
| 	}{
 | |
| 		WhoamiNoTLS: s.getComposeServiceIP("whoami-no-tls") + ":8080",
 | |
| 	})
 | |
| 
 | |
| 	s.traefikCmd(withConfigFile(file))
 | |
| 
 | |
| 	err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("HostSNI(`*`)"))
 | |
| 	require.NoError(s.T(), err)
 | |
| 
 | |
| 	// Traefik will forward every requests on the given port to whoami-no-tls
 | |
| 	out, err := guessWho("127.0.0.1:8093", "", false)
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "whoami-no-tls")
 | |
| }
 | |
| 
 | |
| func (s *TCPSuite) TestCatchAllNoTLS() {
 | |
| 	file := s.adaptFile("fixtures/tcp/catch-all-no-tls.toml", struct {
 | |
| 		WhoamiBannerAddress string
 | |
| 	}{
 | |
| 		WhoamiBannerAddress: s.getComposeServiceIP("whoami-banner") + ":8080",
 | |
| 	})
 | |
| 
 | |
| 	s.traefikCmd(withConfigFile(file))
 | |
| 
 | |
| 	err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("HostSNI(`*`)"))
 | |
| 	require.NoError(s.T(), err)
 | |
| 
 | |
| 	// Traefik will forward every requests on the given port to whoami-no-tls
 | |
| 	out, err := welcome("127.0.0.1:8093")
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "Welcome")
 | |
| }
 | |
| 
 | |
| func (s *TCPSuite) TestCatchAllNoTLSWithHTTPS() {
 | |
| 	file := s.adaptFile("fixtures/tcp/catch-all-no-tls-with-https.toml", struct {
 | |
| 		WhoamiNoTLSAddress string
 | |
| 		WhoamiURL          string
 | |
| 	}{
 | |
| 		WhoamiNoTLSAddress: s.getComposeServiceIP("whoami-no-tls") + ":8080",
 | |
| 		WhoamiURL:          "http://" + s.getComposeServiceIP("whoami") + ":80",
 | |
| 	})
 | |
| 
 | |
| 	s.traefikCmd(withConfigFile(file))
 | |
| 
 | |
| 	err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("HostSNI(`*`)"))
 | |
| 	require.NoError(s.T(), err)
 | |
| 
 | |
| 	req := httptest.NewRequest(http.MethodGet, "https://127.0.0.1:8093/test", nil)
 | |
| 	req.RequestURI = ""
 | |
| 
 | |
| 	err = try.RequestWithTransport(req, 500*time.Millisecond, &http.Transport{
 | |
| 		TLSClientConfig: &tls.Config{
 | |
| 			InsecureSkipVerify: true,
 | |
| 		},
 | |
| 	}, try.StatusCodeIs(http.StatusOK))
 | |
| 	require.NoError(s.T(), err)
 | |
| }
 | |
| 
 | |
| func (s *TCPSuite) TestMiddlewareAllowList() {
 | |
| 	file := s.adaptFile("fixtures/tcp/ip-allowlist.toml", struct {
 | |
| 		WhoamiA string
 | |
| 		WhoamiB string
 | |
| 	}{
 | |
| 		WhoamiA: s.getComposeServiceIP("whoami-a") + ":8080",
 | |
| 		WhoamiB: s.getComposeServiceIP("whoami-b") + ":8080",
 | |
| 	})
 | |
| 
 | |
| 	s.traefikCmd(withConfigFile(file))
 | |
| 
 | |
| 	err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("HostSNI(`whoami-a.test`)"))
 | |
| 	require.NoError(s.T(), err)
 | |
| 
 | |
| 	// Traefik not passes through, ipAllowList closes connection
 | |
| 	_, err = guessWhoTLSPassthrough("127.0.0.1:8093", "whoami-a.test")
 | |
| 	assert.ErrorIs(s.T(), err, io.EOF)
 | |
| 
 | |
| 	// Traefik passes through, termination handled by whoami-b
 | |
| 	out, err := guessWhoTLSPassthrough("127.0.0.1:8093", "whoami-b.test")
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "whoami-b")
 | |
| }
 | |
| 
 | |
| func (s *TCPSuite) TestMiddlewareWhiteList() {
 | |
| 	file := s.adaptFile("fixtures/tcp/ip-whitelist.toml", struct {
 | |
| 		WhoamiA string
 | |
| 		WhoamiB string
 | |
| 	}{
 | |
| 		WhoamiA: s.getComposeServiceIP("whoami-a") + ":8080",
 | |
| 		WhoamiB: s.getComposeServiceIP("whoami-b") + ":8080",
 | |
| 	})
 | |
| 
 | |
| 	s.traefikCmd(withConfigFile(file))
 | |
| 
 | |
| 	err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("HostSNI(`whoami-a.test`)"))
 | |
| 	require.NoError(s.T(), err)
 | |
| 
 | |
| 	// Traefik not passes through, ipWhiteList closes connection
 | |
| 	_, err = guessWhoTLSPassthrough("127.0.0.1:8093", "whoami-a.test")
 | |
| 	assert.ErrorIs(s.T(), err, io.EOF)
 | |
| 
 | |
| 	// Traefik passes through, termination handled by whoami-b
 | |
| 	out, err := guessWhoTLSPassthrough("127.0.0.1:8093", "whoami-b.test")
 | |
| 	require.NoError(s.T(), err)
 | |
| 	assert.Contains(s.T(), out, "whoami-b")
 | |
| }
 | |
| 
 | |
| func (s *TCPSuite) TestWRR() {
 | |
| 	file := s.adaptFile("fixtures/tcp/wrr.toml", struct {
 | |
| 		WhoamiB  string
 | |
| 		WhoamiAB string
 | |
| 	}{
 | |
| 		WhoamiB:  s.getComposeServiceIP("whoami-b") + ":8080",
 | |
| 		WhoamiAB: s.getComposeServiceIP("whoami-ab") + ":8080",
 | |
| 	})
 | |
| 
 | |
| 	s.traefikCmd(withConfigFile(file))
 | |
| 
 | |
| 	err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("HostSNI(`whoami-b.test`)"))
 | |
| 	require.NoError(s.T(), err)
 | |
| 
 | |
| 	call := map[string]int{}
 | |
| 	for range 4 {
 | |
| 		// Traefik passes through, termination handled by whoami-b or whoami-bb
 | |
| 		out, err := guessWhoTLSPassthrough("127.0.0.1:8093", "whoami-b.test")
 | |
| 		require.NoError(s.T(), err)
 | |
| 		switch {
 | |
| 		case strings.Contains(out, "whoami-b"):
 | |
| 			call["whoami-b"]++
 | |
| 		case strings.Contains(out, "whoami-ab"):
 | |
| 			call["whoami-ab"]++
 | |
| 		default:
 | |
| 			call["unknown"]++
 | |
| 		}
 | |
| 		time.Sleep(time.Second)
 | |
| 	}
 | |
| 
 | |
| 	assert.Equal(s.T(), map[string]int{"whoami-b": 3, "whoami-ab": 1}, call)
 | |
| }
 | |
| 
 | |
| func welcome(addr string) (string, error) {
 | |
| 	tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	conn, err := net.DialTCP("tcp", nil, tcpAddr)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	defer conn.Close()
 | |
| 
 | |
| 	out := make([]byte, 2048)
 | |
| 	n, err := conn.Read(out)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	return string(out[:n]), nil
 | |
| }
 | |
| 
 | |
| func guessWho(addr, serverName string, tlsCall bool) (string, error) {
 | |
| 	return guessWhoTLSMaxVersion(addr, serverName, tlsCall, 0)
 | |
| }
 | |
| 
 | |
| func guessWhoTLSMaxVersion(addr, serverName string, tlsCall bool, tlsMaxVersion uint16) (string, error) {
 | |
| 	var conn net.Conn
 | |
| 	var err error
 | |
| 
 | |
| 	if tlsCall {
 | |
| 		conn, err = tls.Dial("tcp", addr, &tls.Config{
 | |
| 			ServerName:         serverName,
 | |
| 			InsecureSkipVerify: true,
 | |
| 			MinVersion:         0,
 | |
| 			MaxVersion:         tlsMaxVersion,
 | |
| 		})
 | |
| 	} else {
 | |
| 		tcpAddr, err2 := net.ResolveTCPAddr("tcp", addr)
 | |
| 		if err2 != nil {
 | |
| 			return "", err2
 | |
| 		}
 | |
| 
 | |
| 		conn, err = net.DialTCP("tcp", nil, tcpAddr)
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	defer conn.Close()
 | |
| 
 | |
| 	_, err = conn.Write([]byte("WHO"))
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	out := make([]byte, 2048)
 | |
| 	n, err := conn.Read(out)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	return string(out[:n]), nil
 | |
| }
 | |
| 
 | |
| // guessWhoTLSPassthrough guesses service identity and ensures that the
 | |
| // certificate is valid for the given server name.
 | |
| func guessWhoTLSPassthrough(addr, serverName string) (string, error) {
 | |
| 	var conn net.Conn
 | |
| 	var err error
 | |
| 
 | |
| 	conn, err = tls.Dial("tcp", addr, &tls.Config{
 | |
| 		ServerName:         serverName,
 | |
| 		InsecureSkipVerify: true,
 | |
| 		MinVersion:         0,
 | |
| 		MaxVersion:         0,
 | |
| 		VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
 | |
| 			if len(rawCerts) > 1 {
 | |
| 				return errors.New("tls: more than one certificates from peer")
 | |
| 			}
 | |
| 
 | |
| 			cert, err := x509.ParseCertificate(rawCerts[0])
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("tls: failed to parse certificate from peer: %w", err)
 | |
| 			}
 | |
| 
 | |
| 			if cert.Subject.CommonName == serverName {
 | |
| 				return nil
 | |
| 			}
 | |
| 
 | |
| 			if err = cert.VerifyHostname(serverName); err == nil {
 | |
| 				return nil
 | |
| 			}
 | |
| 
 | |
| 			return fmt.Errorf("tls: no valid certificate for serverName %s", serverName)
 | |
| 		},
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	defer conn.Close()
 | |
| 
 | |
| 	_, err = conn.Write([]byte("WHO"))
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	out := make([]byte, 2048)
 | |
| 	n, err := conn.Read(out)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	return string(out[:n]), nil
 | |
| }
 |