mirror of
				https://github.com/traefik/traefik.git
				synced 2025-10-31 16:31:16 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			862 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			862 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package tcp
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"crypto/rand"
 | |
| 	"crypto/rsa"
 | |
| 	"crypto/tls"
 | |
| 	"crypto/x509"
 | |
| 	"crypto/x509/pkix"
 | |
| 	"errors"
 | |
| 	"io"
 | |
| 	"math/big"
 | |
| 	"net"
 | |
| 	"net/url"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/pires/go-proxyproto"
 | |
| 	"github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
 | |
| 	"github.com/spiffe/go-spiffe/v2/spiffeid"
 | |
| 	"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
 | |
| 	"github.com/spiffe/go-spiffe/v2/svid/x509svid"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| 	"github.com/traefik/traefik/v3/pkg/config/dynamic"
 | |
| 	traefiktls "github.com/traefik/traefik/v3/pkg/tls"
 | |
| 	"github.com/traefik/traefik/v3/pkg/types"
 | |
| )
 | |
| 
 | |
| // LocalhostCert is a PEM-encoded TLS cert
 | |
| // for host example.com, www.example.com
 | |
| // expiring at Jan 29 16:00:00 2084 GMT.
 | |
| // go run $GOROOT/src/crypto/tls/generate_cert.go  --rsa-bits 1024 --host example.com,www.example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
 | |
| var LocalhostCert = []byte(`-----BEGIN CERTIFICATE-----
 | |
| MIICDDCCAXWgAwIBAgIQH20JmcOlcRWHNuf62SYwszANBgkqhkiG9w0BAQsFADAS
 | |
| MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
 | |
| MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
 | |
| iQKBgQC0qINy3F4oq6viDnlpDDE5J08iSRGggg6EylJKBKZfphEG2ufgK78Dufl3
 | |
| +7b0LlEY2AeZHwviHODqC9a6ihj1ZYQk0/djAh+OeOhFEWu+9T/VP8gVFarFqT8D
 | |
| Opy+hrG7YJivUIzwb4fmJQRI7FajzsnGyM6LiXLU+0qzb7ZO/QIDAQABo2EwXzAO
 | |
| BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw
 | |
| AwEB/zAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0G
 | |
| CSqGSIb3DQEBCwUAA4GBAB+eluoQYzyyMfeEEAOtlldevx5MtDENT05NB0WI+91R
 | |
| we7mX8lv763u0XuCWPxbHszhclI6FFjoQef0Z1NYLRm8ZRq58QqWDFZ3E6wdDK+B
 | |
| +OWvkW+hRavo6R9LzIZPfbv8yBo4M9PK/DXw8hLqH7VkkI+Gh793iH7Ugd4A7wvT
 | |
| -----END CERTIFICATE-----`)
 | |
| 
 | |
| // LocalhostKey is the private key for localhostCert.
 | |
| var LocalhostKey = []byte(`-----BEGIN PRIVATE KEY-----
 | |
| MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALSog3LcXiirq+IO
 | |
| eWkMMTknTyJJEaCCDoTKUkoEpl+mEQba5+ArvwO5+Xf7tvQuURjYB5kfC+Ic4OoL
 | |
| 1rqKGPVlhCTT92MCH4546EURa771P9U/yBUVqsWpPwM6nL6GsbtgmK9QjPBvh+Yl
 | |
| BEjsVqPOycbIzouJctT7SrNvtk79AgMBAAECgYB1wMT1MBgbkFIXpXGTfAP1id61
 | |
| rUTVBxCpkypx3ngHLjo46qRq5Hi72BN4FlTY8fugIudI8giP2FztkMvkiLDc4m0p
 | |
| Gn+QMJzjlBjjTuNLvLy4aSmNRLIC3mtbx9PdU71DQswEpJHFj/vmsxbuSrG1I1YE
 | |
| r1reuSo2ow6fOAjXLQJBANpz+RkOiPSPuvl+gi1sp2pLuynUJVDVqWZi386YRpfg
 | |
| DiKCLpqwqYDkOozm/fwFALvwXKGmsyyL43HO8eI+2NsCQQDTtY32V+02GPecdsyq
 | |
| msK06EPVTSaYwj9Mm+q709KsmYFHLXDqXjcKV4UgKYKRPz7my1fXodMmGmfuh1a3
 | |
| /HMHAkEAmOQKN0tA90mRJwUvvvMIyRBv0fq0kzq28P3KfiF9ZtZdjjFmxMVYHOmf
 | |
| QPZ6VGR7+w1jB5BQXqEZcpHQIPSzeQJBAIy9tZJ/AYNlNbcegxEnsSjy/6VdlLsY
 | |
| 51vWi0Yym2uC4R6gZuBnoc+OP0ISVmqY0Qg9RjhjrCs4gr9f2ZaWjSECQCxqZMq1
 | |
| 3viJ8BGCC0m/5jv1EHur3YgwphYCkf4Li6DKwIdMLk1WXkTcPIY3V2Jqj8rPEB5V
 | |
| rqPRSAtd/h6oZbs=
 | |
| -----END PRIVATE KEY-----`)
 | |
| 
 | |
| //	openssl req -newkey rsa:2048 \
 | |
| //	   -new -nodes -x509 \
 | |
| //	   -days 3650 \
 | |
| //	   -out cert.pem \
 | |
| //	   -keyout key.pem \
 | |
| //	   -subj "/CN=example.com"
 | |
| //	   -addext "subjectAltName = DNS:example.com"
 | |
| var mTLSCert = []byte(`-----BEGIN CERTIFICATE-----
 | |
| MIIDJTCCAg2gAwIBAgIUYKnGcLnmMosOSKqTn4ydAMURE4gwDQYJKoZIhvcNAQEL
 | |
| BQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjAwODEzMDkyNzIwWhcNMzAw
 | |
| ODExMDkyNzIwWjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN
 | |
| AQEBBQADggEPADCCAQoCggEBAOAe+QM1c9lZ2TPRgoiuPAq2A3Pfu+i82lmqrTJ0
 | |
| PR2Cx1fPbccCUTFJPlxSDiaMrwtvqw1yP9L2Pu/vJK5BY4YDVDtFGKjpRBau1otJ
 | |
| iY50O5qMo3sfLqR4/1VsQGlLVZYLD3dyc4ZTmOp8+7tJ2SyGorojbIKfimZT7XD7
 | |
| dzrVr4h4Gn+SzzOnoKyx29uaNRP+XuMYHmHyQcJE03pUGhkTOvMwBlF96QdQ9WG0
 | |
| D+1CxRciEsZXE+imKBHoaTgrTkpnFHzsrIEw+OHQYf30zuT/k/lkgv1vqEwINHjz
 | |
| W2VeTur5eqVvA7zZdoEXMRy7BUvh/nZk5AXkXAmZLn0eUg8CAwEAAaNrMGkwHQYD
 | |
| VR0OBBYEFEDrbhPDt+hi3ZOzk6S/CFAVHwk0MB8GA1UdIwQYMBaAFEDrbhPDt+hi
 | |
| 3ZOzk6S/CFAVHwk0MA8GA1UdEwEB/wQFMAMBAf8wFgYDVR0RBA8wDYILZXhhbXBs
 | |
| ZS5jb20wDQYJKoZIhvcNAQELBQADggEBAG/JRJWeUNx2mDJAk8W7Syq3gmQB7s9f
 | |
| +yY/XVRJZGahOPilABqFpC6GVn2HWuvuOqy8/RGk9ja5abKVXqE6YKrljqo3XfzB
 | |
| KQcOz4SFirpkHvNCiEcK3kggN3wJWqL2QyXAxWldBBBCO9yx7a3cux31C//sTUOG
 | |
| xq4JZDg171U1UOpfN1t0BFMdt05XZFEM247N7Dcf7HoXwAa7eyLKgtKWqPDqGrFa
 | |
| fvGDDKK9X/KVsU2x9V3pG+LsJg7ogUnSyD2r5G1F3Y8OVs2T/783PaN0M35fDL38
 | |
| 09VbsxA2GasOHZrghUzT4UvZWWZbWEmG975hFYvdj6DlK9K0s5TdKIs=
 | |
| -----END CERTIFICATE-----`)
 | |
| 
 | |
| var mTLSKey = []byte(`-----BEGIN PRIVATE KEY-----
 | |
| MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDgHvkDNXPZWdkz
 | |
| 0YKIrjwKtgNz37vovNpZqq0ydD0dgsdXz23HAlExST5cUg4mjK8Lb6sNcj/S9j7v
 | |
| 7ySuQWOGA1Q7RRio6UQWrtaLSYmOdDuajKN7Hy6keP9VbEBpS1WWCw93cnOGU5jq
 | |
| fPu7SdkshqK6I2yCn4pmU+1w+3c61a+IeBp/ks8zp6CssdvbmjUT/l7jGB5h8kHC
 | |
| RNN6VBoZEzrzMAZRfekHUPVhtA/tQsUXIhLGVxPopigR6Gk4K05KZxR87KyBMPjh
 | |
| 0GH99M7k/5P5ZIL9b6hMCDR481tlXk7q+XqlbwO82XaBFzEcuwVL4f52ZOQF5FwJ
 | |
| mS59HlIPAgMBAAECggEAAKLV3hZ2v7UrkqQTlMO50+X0WI3YAK8Yh4yedTgzPDQ0
 | |
| 0KD8FMaC6HrmvGhXNfDMRmIIwD8Ew1qDjzbEieIRoD2+LXTivwf6c34HidmplEfs
 | |
| K2IezKin/zuArgNio2ndUlGxt4sRnN373x5/sGZjQWcYayLSmgRN5kByuhFco0Qa
 | |
| oSrXcXNUlb+KgRQXPDU4+M35tPHvLdyg+tko/m/5uK9dc9MNvGZHOMBKg0VNURJb
 | |
| V1l3dR+evwvpqHzBvWiqN/YOiUUvIxlFKA35hJkfCl7ivFs4CLqqFNCKDao95fWe
 | |
| s0UR9iMakT48jXV76IfwZnyX10OhIWzKls5trjDL8QKBgQD3thQJ8e0FL9y1W+Ph
 | |
| mCdEaoffSPkgSn64wIsQ9bMmv4y+KYBK5AhqaHgYm4LgW4x1+CURNFu+YFEyaNNA
 | |
| kNCXFyRX3Em3vxcShP5jIqg+f07mtXPKntWP/zBeKQWgdHX371oFTfaAlNuKX/7S
 | |
| n0jBYjr4Iof1bnquMQvUoHCYWwKBgQDnntFU9/AQGaQIvhfeU1XKFkQ/BfhIsd27
 | |
| RlmiCi0ee9Ce74cMAhWr/9yg0XUxzrh+Ui1xnkMVTZ5P8tWIxROokznLUTGJA5rs
 | |
| zB+ovCPFZcquTwNzn7SBnpHTR0OqJd8sd89P5ST2SqufeSF/gGi5sTs4EocOLCpZ
 | |
| EPVIfm47XQKBgB4d5RHQeCDJUOw739jtxthqm1pqZN+oLwAHaOEG/mEXqOT15sM0
 | |
| NlG5oeBcB+1/M/Sj1t3gn8blrvmSBR00fifgiGqmPdA5S3TU9pjW/d2bXNxv80QP
 | |
| S6fWPusz0ZtQjYc3cppygCXh808/nJu/AfmBF+pTSHRumjvTery/RPFBAoGBAMi/
 | |
| zCta4cTylEvHhqR5kiefePMu120aTFYeuV1KeKStJ7o5XNE5lVMIZk80e+D5jMpf
 | |
| q2eIhhgWuBoPHKh4N3uqbzMbYlWgvEx09xOmTVKv0SWW8iTqzOZza2y1nZ4BSRcf
 | |
| mJ1ku86EFZAYysHZp+saA3usA0ZzXRjpK87zVdM5AoGBAOSqI+t48PnPtaUDFdpd
 | |
| taNNVDbcecJatm3w8VDWnarahfWe66FIqc9wUkqekqAgwZLa0AGdUalvXfGrHfNs
 | |
| PtvuNc5EImfSkuPBYLBslNxtjbBvAYgacEdY+gRhn2TeIUApnND58lCWsKbNHLFZ
 | |
| ajIPbTY+Fe9OTOFTN48ujXNn
 | |
| -----END PRIVATE KEY-----`)
 | |
| 
 | |
| func TestConflictingConfig(t *testing.T) {
 | |
| 	dialerManager := NewDialerManager(nil)
 | |
| 
 | |
| 	dynamicConf := map[string]*dynamic.TCPServersTransport{
 | |
| 		"test": {
 | |
| 			TLS: &dynamic.TLSClientConfig{
 | |
| 				ServerName: "foobar",
 | |
| 				Spiffe:     &dynamic.Spiffe{},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	dialerManager.Update(dynamicConf)
 | |
| 
 | |
| 	_, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false)
 | |
| 	require.Error(t, err)
 | |
| }
 | |
| 
 | |
| func TestNoTLS(t *testing.T) {
 | |
| 	backendListener, err := net.Listen("tcp", ":0")
 | |
| 	require.NoError(t, err)
 | |
| 	defer backendListener.Close()
 | |
| 
 | |
| 	go fakeServer(t, backendListener)
 | |
| 
 | |
| 	_, port, err := net.SplitHostPort(backendListener.Addr().String())
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	dialerManager := NewDialerManager(nil)
 | |
| 
 | |
| 	dynamicConf := map[string]*dynamic.TCPServersTransport{
 | |
| 		"test": {
 | |
| 			TLS: &dynamic.TLSClientConfig{},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	dialerManager.Update(dynamicConf)
 | |
| 
 | |
| 	dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	conn, err := dialer.Dial("tcp", ":"+port, nil)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	_, err = conn.Write([]byte("ping\n"))
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	buf := make([]byte, 64)
 | |
| 	n, err := conn.Read(buf)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	assert.Equal(t, 4, n)
 | |
| 	assert.Equal(t, "PONG", string(buf[:4]))
 | |
| 
 | |
| 	err = conn.Close()
 | |
| 	require.NoError(t, err)
 | |
| }
 | |
| 
 | |
| func TestTLS(t *testing.T) {
 | |
| 	cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	backendListener, err := net.Listen("tcp", ":0")
 | |
| 	require.NoError(t, err)
 | |
| 	defer backendListener.Close()
 | |
| 
 | |
| 	tlsListener := tls.NewListener(backendListener, &tls.Config{Certificates: []tls.Certificate{cert}})
 | |
| 	defer tlsListener.Close()
 | |
| 
 | |
| 	go fakeServer(t, tlsListener)
 | |
| 
 | |
| 	_, port, err := net.SplitHostPort(tlsListener.Addr().String())
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	dialerManager := NewDialerManager(nil)
 | |
| 
 | |
| 	dynamicConf := map[string]*dynamic.TCPServersTransport{
 | |
| 		"test": {
 | |
| 			TLS: &dynamic.TLSClientConfig{
 | |
| 				ServerName: "example.com",
 | |
| 				RootCAs:    []types.FileOrContent{types.FileOrContent(LocalhostCert)},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	dialerManager.Update(dynamicConf)
 | |
| 
 | |
| 	dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	conn, err := dialer.Dial("tcp", ":"+port, nil)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	_, err = conn.Write([]byte("ping\n"))
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	err = conn.(*tls.Conn).CloseWrite()
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	var buf []byte
 | |
| 	buffer := bytes.NewBuffer(buf)
 | |
| 	n, err := io.Copy(buffer, conn)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	assert.Equal(t, int64(4), n)
 | |
| 	assert.Equal(t, "PONG", buffer.String())
 | |
| }
 | |
| 
 | |
| func TestTLSWithInsecureSkipVerify(t *testing.T) {
 | |
| 	cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	backendListener, err := net.Listen("tcp", ":0")
 | |
| 	require.NoError(t, err)
 | |
| 	defer backendListener.Close()
 | |
| 
 | |
| 	tlsListener := tls.NewListener(backendListener, &tls.Config{Certificates: []tls.Certificate{cert}})
 | |
| 	defer tlsListener.Close()
 | |
| 
 | |
| 	go fakeServer(t, tlsListener)
 | |
| 
 | |
| 	_, port, err := net.SplitHostPort(tlsListener.Addr().String())
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	dialerManager := NewDialerManager(nil)
 | |
| 
 | |
| 	dynamicConf := map[string]*dynamic.TCPServersTransport{
 | |
| 		"test": {
 | |
| 			TLS: &dynamic.TLSClientConfig{
 | |
| 				ServerName:         "bad-domain.com",
 | |
| 				RootCAs:            []types.FileOrContent{types.FileOrContent(LocalhostCert)},
 | |
| 				InsecureSkipVerify: true,
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	dialerManager.Update(dynamicConf)
 | |
| 
 | |
| 	dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	conn, err := dialer.Dial("tcp", ":"+port, nil)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	_, err = conn.Write([]byte("ping\n"))
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	err = conn.(*tls.Conn).CloseWrite()
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	var buf []byte
 | |
| 	buffer := bytes.NewBuffer(buf)
 | |
| 	n, err := io.Copy(buffer, conn)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	assert.Equal(t, int64(4), n)
 | |
| 	assert.Equal(t, "PONG", buffer.String())
 | |
| }
 | |
| 
 | |
| func TestMTLS(t *testing.T) {
 | |
| 	cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	clientPool := x509.NewCertPool()
 | |
| 	clientPool.AppendCertsFromPEM(mTLSCert)
 | |
| 
 | |
| 	backendListener, err := net.Listen("tcp", ":0")
 | |
| 	require.NoError(t, err)
 | |
| 	defer backendListener.Close()
 | |
| 
 | |
| 	tlsListener := tls.NewListener(backendListener, &tls.Config{
 | |
| 		// For TLS
 | |
| 		Certificates: []tls.Certificate{cert},
 | |
| 
 | |
| 		// For mTLS
 | |
| 		ClientAuth: tls.RequireAndVerifyClientCert,
 | |
| 		ClientCAs:  clientPool,
 | |
| 	})
 | |
| 	defer tlsListener.Close()
 | |
| 
 | |
| 	go fakeServer(t, tlsListener)
 | |
| 
 | |
| 	_, port, err := net.SplitHostPort(tlsListener.Addr().String())
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	dialerManager := NewDialerManager(nil)
 | |
| 
 | |
| 	dynamicConf := map[string]*dynamic.TCPServersTransport{
 | |
| 		"test": {
 | |
| 			TLS: &dynamic.TLSClientConfig{
 | |
| 				ServerName: "example.com",
 | |
| 				// For TLS
 | |
| 				RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
 | |
| 
 | |
| 				// For mTLS
 | |
| 				Certificates: traefiktls.Certificates{
 | |
| 					traefiktls.Certificate{
 | |
| 						CertFile: types.FileOrContent(mTLSCert),
 | |
| 						KeyFile:  types.FileOrContent(mTLSKey),
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	dialerManager.Update(dynamicConf)
 | |
| 
 | |
| 	dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	conn, err := dialer.Dial("tcp", ":"+port, nil)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	_, err = conn.Write([]byte("ping\n"))
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	err = conn.(*tls.Conn).CloseWrite()
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	var buf []byte
 | |
| 	buffer := bytes.NewBuffer(buf)
 | |
| 	n, err := io.Copy(buffer, conn)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	assert.Equal(t, int64(4), n)
 | |
| 	assert.Equal(t, "PONG", buffer.String())
 | |
| }
 | |
| 
 | |
| func TestSpiffeMTLS(t *testing.T) {
 | |
| 	backendListener, err := net.Listen("tcp", ":0")
 | |
| 	require.NoError(t, err)
 | |
| 	defer backendListener.Close()
 | |
| 
 | |
| 	trustDomain := spiffeid.RequireTrustDomainFromString("spiffe://traefik.test")
 | |
| 
 | |
| 	pki := newFakeSpiffePKI(t, trustDomain)
 | |
| 
 | |
| 	serverSVID := pki.genSVID(t, spiffeid.RequireFromPath(trustDomain, "/server"))
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	serverSource := fakeSpiffeSource{
 | |
| 		svid:   serverSVID,
 | |
| 		bundle: pki.bundle,
 | |
| 	}
 | |
| 
 | |
| 	// go-spiffe's `tlsconfig.MTLSServerConfig` (that should be used here) does not set a certificate on
 | |
| 	// the returned `tls.Config` and relies instead on `GetCertificate` being always called.
 | |
| 	// But it turns out that `StartTLS` from `httptest.Server`, enforces a default certificate
 | |
| 	// if no certificate is previously set on the configured TLS config.
 | |
| 	// It makes the test server always serve the httptest default certificate, and not the SPIFFE certificate,
 | |
| 	// as GetCertificate is in that case never called (there's a default cert, and SNI is not used).
 | |
| 	// To bypass this issue, we're manually extracting the server ceritificate from the server SVID
 | |
| 	// and use another initialization method that forces serving the server SPIFFE certificate.
 | |
| 	serverCert, err := tlsconfig.GetCertificate(&serverSource)(nil)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	tlsListener := tls.NewListener(backendListener, tlsconfig.MTLSWebServerConfig(
 | |
| 		serverCert,
 | |
| 		&serverSource,
 | |
| 		tlsconfig.AuthorizeAny(),
 | |
| 	))
 | |
| 	defer tlsListener.Close()
 | |
| 
 | |
| 	_, port, err := net.SplitHostPort(tlsListener.Addr().String())
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	clientSVID := pki.genSVID(t, spiffeid.RequireFromPath(trustDomain, "/client"))
 | |
| 
 | |
| 	clientSource := fakeSpiffeSource{
 | |
| 		svid:   clientSVID,
 | |
| 		bundle: pki.bundle,
 | |
| 	}
 | |
| 
 | |
| 	testCases := []struct {
 | |
| 		desc         string
 | |
| 		config       dynamic.Spiffe
 | |
| 		clientSource SpiffeX509Source
 | |
| 		wantError    bool
 | |
| 	}{
 | |
| 		{
 | |
| 			desc:         "supports SPIFFE mTLS",
 | |
| 			config:       dynamic.Spiffe{},
 | |
| 			clientSource: &clientSource,
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "allows expected server SPIFFE ID",
 | |
| 			config: dynamic.Spiffe{
 | |
| 				IDs: []string{"spiffe://traefik.test/server"},
 | |
| 			},
 | |
| 			clientSource: &clientSource,
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "blocks unexpected server SPIFFE ID",
 | |
| 			config: dynamic.Spiffe{
 | |
| 				IDs: []string{"spiffe://traefik.test/not-server"},
 | |
| 			},
 | |
| 			clientSource: &clientSource,
 | |
| 			wantError:    true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "allows expected server trust domain",
 | |
| 			config: dynamic.Spiffe{
 | |
| 				TrustDomain: "spiffe://traefik.test",
 | |
| 			},
 | |
| 			clientSource: &clientSource,
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "denies unexpected server trust domain",
 | |
| 			config: dynamic.Spiffe{
 | |
| 				TrustDomain: "spiffe://not-traefik.test",
 | |
| 			},
 | |
| 			clientSource: &clientSource,
 | |
| 			wantError:    true,
 | |
| 		},
 | |
| 		{
 | |
| 			desc: "spiffe IDs allowlist takes precedence",
 | |
| 			config: dynamic.Spiffe{
 | |
| 				IDs:         []string{"spiffe://traefik.test/not-server"},
 | |
| 				TrustDomain: "spiffe://not-traefik.test",
 | |
| 			},
 | |
| 			clientSource: &clientSource,
 | |
| 			wantError:    true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, test := range testCases {
 | |
| 		t.Run(test.desc, func(t *testing.T) {
 | |
| 			go fakeServer(t, tlsListener)
 | |
| 
 | |
| 			dialerManager := NewDialerManager(test.clientSource)
 | |
| 
 | |
| 			dynamicConf := map[string]*dynamic.TCPServersTransport{
 | |
| 				"test": {
 | |
| 					TLS: &dynamic.TLSClientConfig{
 | |
| 						Spiffe: &test.config,
 | |
| 					},
 | |
| 				},
 | |
| 			}
 | |
| 
 | |
| 			dialerManager.Update(dynamicConf)
 | |
| 
 | |
| 			dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true)
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			conn, err := dialer.Dial("tcp", ":"+port, nil)
 | |
| 
 | |
| 			if test.wantError {
 | |
| 				require.Error(t, err)
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			_, err = conn.Write([]byte("ping\n"))
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			err = conn.(*tls.Conn).CloseWrite()
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			var buf []byte
 | |
| 			buffer := bytes.NewBuffer(buf)
 | |
| 			n, err := io.Copy(buffer, conn)
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			assert.Equal(t, int64(4), n)
 | |
| 			assert.Equal(t, "PONG", buffer.String())
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestProxyProtocol(t *testing.T) {
 | |
| 	testCases := []struct {
 | |
| 		desc    string
 | |
| 		version int
 | |
| 	}{
 | |
| 		{
 | |
| 			desc:    "proxy protocol v1",
 | |
| 			version: 1,
 | |
| 		},
 | |
| 		{
 | |
| 			desc:    "proxy protocol v2",
 | |
| 			version: 2,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, test := range testCases {
 | |
| 		t.Run(test.desc, func(t *testing.T) {
 | |
| 			backendListener, err := net.Listen("tcp", ":0")
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			var version int
 | |
| 			var localAddr, remoteAddr string
 | |
| 			proxyBackendListener := proxyproto.Listener{
 | |
| 				Listener: backendListener,
 | |
| 				ValidateHeader: func(h *proxyproto.Header) error {
 | |
| 					version = int(h.Version)
 | |
| 					localAddr = h.DestinationAddr.String()
 | |
| 					remoteAddr = h.SourceAddr.String()
 | |
| 					return nil
 | |
| 				},
 | |
| 				Policy: func(upstream net.Addr) (proxyproto.Policy, error) {
 | |
| 					switch test.version {
 | |
| 					case 1, 2:
 | |
| 						return proxyproto.USE, nil
 | |
| 					default:
 | |
| 						return proxyproto.REQUIRE, errors.New("unsupported version")
 | |
| 					}
 | |
| 				},
 | |
| 			}
 | |
| 			defer proxyBackendListener.Close()
 | |
| 
 | |
| 			go fakeServer(t, &proxyBackendListener)
 | |
| 
 | |
| 			_, port, err := net.SplitHostPort(backendListener.Addr().String())
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			dialerManager := NewDialerManager(nil)
 | |
| 			dialerManager.Update(map[string]*dynamic.TCPServersTransport{
 | |
| 				"test": {
 | |
| 					ProxyProtocol: &dynamic.ProxyProtocol{
 | |
| 						Version: test.version,
 | |
| 					},
 | |
| 				},
 | |
| 			})
 | |
| 
 | |
| 			dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false)
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			clientConn := &fakeClientConn{
 | |
| 				localAddr: &net.TCPAddr{
 | |
| 					IP:   net.ParseIP("2.2.2.2"),
 | |
| 					Port: 12345,
 | |
| 				},
 | |
| 				remoteAddr: &net.TCPAddr{
 | |
| 					IP:   net.ParseIP("1.1.1.1"),
 | |
| 					Port: 12345,
 | |
| 				},
 | |
| 			}
 | |
| 
 | |
| 			conn, err := dialer.Dial("tcp", ":"+port, clientConn)
 | |
| 			require.NoError(t, err)
 | |
| 			defer conn.Close()
 | |
| 
 | |
| 			_, err = conn.Write([]byte("ping"))
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			buf := make([]byte, 64)
 | |
| 			n, err := conn.Read(buf)
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			assert.Equal(t, 4, n)
 | |
| 			assert.Equal(t, "PONG", string(buf[:4]))
 | |
| 			assert.Equal(t, test.version, version)
 | |
| 			assert.Equal(t, "2.2.2.2:12345", localAddr)
 | |
| 			assert.Equal(t, "1.1.1.1:12345", remoteAddr)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestProxyProtocolWithTLS(t *testing.T) {
 | |
| 	testCases := []struct {
 | |
| 		desc    string
 | |
| 		version int
 | |
| 	}{
 | |
| 		{
 | |
| 			desc:    "proxy protocol v1",
 | |
| 			version: 1,
 | |
| 		},
 | |
| 		{
 | |
| 			desc:    "proxy protocol v2",
 | |
| 			version: 2,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, test := range testCases {
 | |
| 		t.Run(test.desc, func(t *testing.T) {
 | |
| 			cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			backendListener, err := net.Listen("tcp", ":0")
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			var version int
 | |
| 			var localAddr, remoteAddr string
 | |
| 			proxyBackendListener := proxyproto.Listener{
 | |
| 				Listener: backendListener,
 | |
| 				ValidateHeader: func(h *proxyproto.Header) error {
 | |
| 					version = int(h.Version)
 | |
| 					localAddr = h.DestinationAddr.String()
 | |
| 					remoteAddr = h.SourceAddr.String()
 | |
| 					return nil
 | |
| 				},
 | |
| 				Policy: func(upstream net.Addr) (proxyproto.Policy, error) {
 | |
| 					switch test.version {
 | |
| 					case 1, 2:
 | |
| 						return proxyproto.USE, nil
 | |
| 					default:
 | |
| 						return proxyproto.REQUIRE, errors.New("unsupported version")
 | |
| 					}
 | |
| 				},
 | |
| 			}
 | |
| 			defer proxyBackendListener.Close()
 | |
| 
 | |
| 			go func() {
 | |
| 				conn, err := proxyBackendListener.Accept()
 | |
| 				require.NoError(t, err)
 | |
| 				defer conn.Close()
 | |
| 
 | |
| 				// Now wrap with TLS and perform handshake
 | |
| 				tlsConn := tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{cert}})
 | |
| 				defer tlsConn.Close()
 | |
| 
 | |
| 				err = tlsConn.Handshake()
 | |
| 				require.NoError(t, err)
 | |
| 
 | |
| 				buf := make([]byte, 64)
 | |
| 				n, err := tlsConn.Read(buf)
 | |
| 				require.NoError(t, err)
 | |
| 
 | |
| 				if bytes.Equal(buf[:n], []byte("ping")) {
 | |
| 					_, _ = tlsConn.Write([]byte("PONG"))
 | |
| 				}
 | |
| 			}()
 | |
| 
 | |
| 			_, port, err := net.SplitHostPort(backendListener.Addr().String())
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			dialerManager := NewDialerManager(nil)
 | |
| 			dialerManager.Update(map[string]*dynamic.TCPServersTransport{
 | |
| 				"test": {
 | |
| 					TLS: &dynamic.TLSClientConfig{
 | |
| 						ServerName:         "example.com",
 | |
| 						RootCAs:            []types.FileOrContent{types.FileOrContent(LocalhostCert)},
 | |
| 						InsecureSkipVerify: true,
 | |
| 					},
 | |
| 					ProxyProtocol: &dynamic.ProxyProtocol{
 | |
| 						Version: test.version,
 | |
| 					},
 | |
| 				},
 | |
| 			})
 | |
| 
 | |
| 			dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{
 | |
| 				ServersTransport: "test",
 | |
| 			}, true)
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			clientConn := &fakeClientConn{
 | |
| 				localAddr: &net.TCPAddr{
 | |
| 					IP:   net.ParseIP("2.2.2.2"),
 | |
| 					Port: 12345,
 | |
| 				},
 | |
| 				remoteAddr: &net.TCPAddr{
 | |
| 					IP:   net.ParseIP("1.1.1.1"),
 | |
| 					Port: 12345,
 | |
| 				},
 | |
| 			}
 | |
| 
 | |
| 			conn, err := dialer.Dial("tcp", ":"+port, clientConn)
 | |
| 			require.NoError(t, err)
 | |
| 			defer conn.Close()
 | |
| 
 | |
| 			_, err = conn.Write([]byte("ping"))
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			buf := make([]byte, 64)
 | |
| 			n, err := conn.Read(buf)
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			assert.Equal(t, 4, n)
 | |
| 			assert.Equal(t, "PONG", string(buf[:4]))
 | |
| 			assert.Equal(t, test.version, version)
 | |
| 			assert.Equal(t, "2.2.2.2:12345", localAddr)
 | |
| 			assert.Equal(t, "1.1.1.1:12345", remoteAddr)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestProxyProtocolDisabled(t *testing.T) {
 | |
| 	backendListener, err := net.Listen("tcp", ":0")
 | |
| 	require.NoError(t, err)
 | |
| 	defer backendListener.Close()
 | |
| 
 | |
| 	go func() {
 | |
| 		conn, err := backendListener.Accept()
 | |
| 		require.NoError(t, err)
 | |
| 		defer conn.Close()
 | |
| 
 | |
| 		buf := make([]byte, 64)
 | |
| 		n, err := conn.Read(buf)
 | |
| 		require.NoError(t, err)
 | |
| 
 | |
| 		if bytes.Equal(buf[:n], []byte("ping")) {
 | |
| 			_, _ = conn.Write([]byte("PONG"))
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	_, port, err := net.SplitHostPort(backendListener.Addr().String())
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	// No proxy protocol configuration.
 | |
| 	dialerManager := NewDialerManager(nil)
 | |
| 	dialerManager.Update(map[string]*dynamic.TCPServersTransport{
 | |
| 		"test": {},
 | |
| 	})
 | |
| 
 | |
| 	dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	conn, err := dialer.Dial("tcp", ":"+port, nil)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	_, err = conn.Write([]byte("ping"))
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	buf := make([]byte, 64)
 | |
| 	n, err := conn.Read(buf)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	assert.Equal(t, 4, n)
 | |
| 	assert.Equal(t, "PONG", string(buf[:4]))
 | |
| }
 | |
| 
 | |
| type fakeClientConn struct {
 | |
| 	remoteAddr *net.TCPAddr
 | |
| 	localAddr  *net.TCPAddr
 | |
| }
 | |
| 
 | |
| func (f fakeClientConn) LocalAddr() net.Addr {
 | |
| 	return f.localAddr
 | |
| }
 | |
| 
 | |
| func (f fakeClientConn) RemoteAddr() net.Addr {
 | |
| 	return f.remoteAddr
 | |
| }
 | |
| 
 | |
| // fakeSpiffePKI simulates a SPIFFE aware PKI and allows generating multiple valid SVIDs.
 | |
| type fakeSpiffePKI struct {
 | |
| 	caPrivateKey *rsa.PrivateKey
 | |
| 
 | |
| 	bundle *x509bundle.Bundle
 | |
| }
 | |
| 
 | |
| func newFakeSpiffePKI(t *testing.T, trustDomain spiffeid.TrustDomain) fakeSpiffePKI {
 | |
| 	t.Helper()
 | |
| 
 | |
| 	caPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	caTemplate := x509.Certificate{
 | |
| 		SerialNumber: big.NewInt(2000),
 | |
| 		Subject: pkix.Name{
 | |
| 			Organization: []string{"spiffe"},
 | |
| 		},
 | |
| 		URIs:         []*url.URL{spiffeid.RequireFromPath(trustDomain, "/ca").URL()},
 | |
| 		NotBefore:    time.Now(),
 | |
| 		NotAfter:     time.Now().Add(time.Hour),
 | |
| 		SubjectKeyId: []byte("ca"),
 | |
| 		KeyUsage: x509.KeyUsageCertSign |
 | |
| 			x509.KeyUsageCRLSign,
 | |
| 		BasicConstraintsValid: true,
 | |
| 		IsCA:                  true,
 | |
| 		PublicKey:             caPrivateKey.Public(),
 | |
| 	}
 | |
| 
 | |
| 	caCertDER, err := x509.CreateCertificate(
 | |
| 		rand.Reader,
 | |
| 		&caTemplate,
 | |
| 		&caTemplate,
 | |
| 		caPrivateKey.Public(),
 | |
| 		caPrivateKey,
 | |
| 	)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	bundle, err := x509bundle.ParseRaw(
 | |
| 		trustDomain,
 | |
| 		caCertDER,
 | |
| 	)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	return fakeSpiffePKI{
 | |
| 		bundle:       bundle,
 | |
| 		caPrivateKey: caPrivateKey,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (f *fakeSpiffePKI) genSVID(t *testing.T, id spiffeid.ID) *x509svid.SVID {
 | |
| 	t.Helper()
 | |
| 
 | |
| 	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	template := x509.Certificate{
 | |
| 		SerialNumber: big.NewInt(200001),
 | |
| 		URIs:         []*url.URL{id.URL()},
 | |
| 		NotBefore:    time.Now(),
 | |
| 		NotAfter:     time.Now().Add(time.Hour),
 | |
| 		SubjectKeyId: []byte("svid"),
 | |
| 		KeyUsage: x509.KeyUsageKeyEncipherment |
 | |
| 			x509.KeyUsageKeyAgreement |
 | |
| 			x509.KeyUsageDigitalSignature,
 | |
| 		ExtKeyUsage: []x509.ExtKeyUsage{
 | |
| 			x509.ExtKeyUsageServerAuth,
 | |
| 			x509.ExtKeyUsageClientAuth,
 | |
| 		},
 | |
| 		BasicConstraintsValid: true,
 | |
| 		PublicKey:             privateKey.PublicKey,
 | |
| 		IPAddresses:           []net.IP{net.ParseIP("127.0.0.1")},
 | |
| 	}
 | |
| 
 | |
| 	certDER, err := x509.CreateCertificate(
 | |
| 		rand.Reader,
 | |
| 		&template,
 | |
| 		f.bundle.X509Authorities()[0],
 | |
| 		privateKey.Public(),
 | |
| 		f.caPrivateKey,
 | |
| 	)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	keyPKCS8, err := x509.MarshalPKCS8PrivateKey(privateKey)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	svid, err := x509svid.ParseRaw(certDER, keyPKCS8)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	return svid
 | |
| }
 | |
| 
 | |
| // fakeSpiffeSource allows retrieving statically an SVID and its associated bundle.
 | |
| type fakeSpiffeSource struct {
 | |
| 	bundle *x509bundle.Bundle
 | |
| 	svid   *x509svid.SVID
 | |
| }
 | |
| 
 | |
| func (s *fakeSpiffeSource) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) {
 | |
| 	return s.bundle, nil
 | |
| }
 | |
| 
 | |
| func (s *fakeSpiffeSource) GetX509SVID() (*x509svid.SVID, error) {
 | |
| 	return s.svid, nil
 | |
| }
 |