package server import ( "bufio" "context" "crypto/tls" "crypto/x509" "net/http" "testing" "time" "github.com/quic-go/quic-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/traefik/v3/pkg/config/static" tcprouter "github.com/traefik/traefik/v3/pkg/server/router/tcp" "github.com/traefik/traefik/v3/pkg/types" ) // LocalhostCert is a PEM-encoded TLS cert with SAN IPs // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. // generated from src/crypto/tls: // go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h var ( localhostCert = types.FileOrContent(`-----BEGIN CERTIFICATE----- MIIDOTCCAiGgAwIBAgIQSRJrEpBGFc7tNb1fb5pKFzANBgkqhkiG9w0BAQsFADAS MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEA6Gba5tHV1dAKouAaXO3/ebDUU4rvwCUg/CNaJ2PT5xLD4N1Vcb8r bFSW2HXKq+MPfVdwIKR/1DczEoAGf/JWQTW7EgzlXrCd3rlajEX2D73faWJekD0U aUgz5vtrTXZ90BQL7WvRICd7FlEZ6FPOcPlumiyNmzUqtwGhO+9ad1W5BqJaRI6P YfouNkwR6Na4TzSj5BrqUfP0FwDizKSJ0XXmh8g8G9mtwxOSN3Ru1QFc61Xyeluk POGKBV/q6RBNklTNe0gI8usUMlYyoC7ytppNMW7X2vodAelSu25jgx2anj9fDVZu h7AXF5+4nJS4AAt0n1lNY7nGSsdZas8PbQIDAQABo4GIMIGFMA4GA1UdDwEB/wQE AwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud DgQWBBStsdjh3/JCXXYlQryOrL4Sh7BW5TAuBgNVHREEJzAlggtleGFtcGxlLmNv bYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAxWGI 5NhpF3nwwy/4yB4i/CwwSpLrWUa70NyhvprUBC50PxiXav1TeDzwzLx/o5HyNwsv cxv3HdkLW59i/0SlJSrNnWdfZ19oTcS+6PtLoVyISgtyN6DpkKpdG1cOkW3Cy2P2 +tK/tKHRP1Y/Ra0RiDpOAmqn0gCOFGz8+lqDIor/T7MTpibL3IxqWfPrvfVRHL3B grw/ZQTTIVjjh4JBSW3WyWgNo/ikC1lrVxzl4iPUGptxT36Cr7Zk2Bsg0XqwbOvK 5d+NTDREkSnUbie4GeutujmX3Dsx88UiV6UY/4lHJa6I5leHUNOHahRbpbWeOfs/ WkBKOclmOV2xlTVuPw== -----END CERTIFICATE-----`) // LocalhostKey is the private key for localhostCert. localhostKey = types.FileOrContent(`-----BEGIN RSA PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDoZtrm0dXV0Aqi 4Bpc7f95sNRTiu/AJSD8I1onY9PnEsPg3VVxvytsVJbYdcqr4w99V3AgpH/UNzMS gAZ/8lZBNbsSDOVesJ3euVqMRfYPvd9pYl6QPRRpSDPm+2tNdn3QFAvta9EgJ3sW URnoU85w+W6aLI2bNSq3AaE771p3VbkGolpEjo9h+i42TBHo1rhPNKPkGupR8/QX AOLMpInRdeaHyDwb2a3DE5I3dG7VAVzrVfJ6W6Q84YoFX+rpEE2SVM17SAjy6xQy VjKgLvK2mk0xbtfa+h0B6VK7bmODHZqeP18NVm6HsBcXn7iclLgAC3SfWU1jucZK x1lqzw9tAgMBAAECggEABWzxS1Y2wckblnXY57Z+sl6YdmLV+gxj2r8Qib7g4ZIk lIlWR1OJNfw7kU4eryib4fc6nOh6O4AWZyYqAK6tqNQSS/eVG0LQTLTTEldHyVJL dvBe+MsUQOj4nTndZW+QvFzbcm2D8lY5n2nBSxU5ypVoKZ1EqQzytFcLZpTN7d89 EPj0qDyrV4NZlWAwL1AygCwnlwhMQjXEalVF1ylXwU3QzyZ/6MgvF6d3SSUlh+sq XefuyigXw484cQQgbzopv6niMOmGP3of+yV4JQqUSb3IDmmT68XjGd2Dkxl4iPki 6ZwXf3CCi+c+i/zVEcufgZ3SLf8D99kUGE7v7fZ6AQKBgQD1ZX3RAla9hIhxCf+O 3D+I1j2LMrdjAh0ZKKqwMR4JnHX3mjQI6LwqIctPWTU8wYFECSh9klEclSdCa64s uI/GNpcqPXejd0cAAdqHEEeG5sHMDt0oFSurL4lyud0GtZvwlzLuwEweuDtvT9cJ Wfvl86uyO36IW8JdvUprYDctrQKBgQDycZ697qutBieZlGkHpnYWUAeImVA878sJ w44NuXHvMxBPz+lbJGAg8Cn8fcxNAPqHIraK+kx3po8cZGQywKHUWsxi23ozHoxo +bGqeQb9U661TnfdDspIXia+xilZt3mm5BPzOUuRqlh4Y9SOBpSWRmEhyw76w4ZP OPxjWYAgwQKBgA/FehSYxeJgRjSdo+MWnK66tjHgDJE8bYpUZsP0JC4R9DL5oiaA brd2fI6Y+SbyeNBallObt8LSgzdtnEAbjIH8uDJqyOmknNePRvAvR6mP4xyuR+Bv m+Lgp0DMWTw5J9CKpydZDItc49T/mJ5tPhdFVd+am0NAQnmr1MCZ6nHxAoGABS3Y LkaC9FdFUUqSU8+Chkd/YbOkuyiENdkvl6t2e52jo5DVc1T7mLiIrRQi4SI8N9bN /3oJWCT+uaSLX2ouCtNFunblzWHBrhxnZzTeqVq4SLc8aESAnbslKL4i8/+vYZlN s8xtiNcSvL+lMsOBORSXzpj/4Ot8WwTkn1qyGgECgYBKNTypzAHeLE6yVadFp3nQ Ckq9yzvP/ib05rvgbvrne00YeOxqJ9gtTrzgh7koqJyX1L4NwdkEza4ilDWpucn0 xiUZS4SoaJq6ZvcBYS62Yr1t8n09iG47YL8ibgtmH3L+svaotvpVxVK+d7BLevA/ ZboOWVe3icTy64BT3OQhmg== -----END RSA PRIVATE KEY-----`) ) func TestHTTP3AdvertisedPort(t *testing.T) { certContent, err := localhostCert.Read() require.NoError(t, err) keyContent, err := localhostKey.Read() require.NoError(t, err) tlsCert, err := tls.X509KeyPair(certContent, keyContent) require.NoError(t, err) epConfig := &static.EntryPointsTransport{} epConfig.SetDefaults() entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{ Address: "127.0.0.1:0", Transport: epConfig, ForwardedHeaders: &static.ForwardedHeaders{}, HTTP2: &static.HTTP2Config{}, HTTP3: &static.HTTP3Config{ AdvertisedPort: 8080, }, }, nil, nil) require.NoError(t, err) router, err := tcprouter.NewRouter() require.NoError(t, err) router.AddHTTPTLSConfig("*", &tls.Config{ Certificates: []tls.Certificate{tlsCert}, }) router.SetHTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) }), nil) ctx := context.Background() go entryPoint.Start(ctx) entryPoint.SwitchRouter(router) conn, err := tls.Dial("tcp", entryPoint.listener.Addr().String(), &tls.Config{ InsecureSkipVerify: true, }) require.NoError(t, err) t.Cleanup(func() { _ = conn.Close() entryPoint.Shutdown(ctx) }) // We are racing with the http3Server readiness happening in the goroutine starting the entrypoint time.Sleep(time.Second) request, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:8090", nil) require.NoError(t, err) err = request.Write(conn) require.NoError(t, err) r, err := http.ReadResponse(bufio.NewReader(conn), nil) require.NoError(t, err) assert.NotContains(t, r.Header.Get("Alt-Svc"), ":8090") assert.Contains(t, r.Header.Get("Alt-Svc"), ":8080") } func TestHTTP30RTT(t *testing.T) { certContent, err := localhostCert.Read() require.NoError(t, err) keyContent, err := localhostKey.Read() require.NoError(t, err) tlsCert, err := tls.X509KeyPair(certContent, keyContent) require.NoError(t, err) epConfig := &static.EntryPointsTransport{} epConfig.SetDefaults() entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{ Address: "127.0.0.1:8090", Transport: epConfig, ForwardedHeaders: &static.ForwardedHeaders{}, HTTP2: &static.HTTP2Config{}, HTTP3: &static.HTTP3Config{}, }, nil, nil) require.NoError(t, err) router, err := tcprouter.NewRouter() require.NoError(t, err) router.AddHTTPTLSConfig("example.com", &tls.Config{ Certificates: []tls.Certificate{tlsCert}, }) router.SetHTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) }), nil) ctx := context.Background() go entryPoint.Start(ctx) entryPoint.SwitchRouter(router) // We are racing with the http3Server readiness happening in the goroutine starting the entrypoint. time.Sleep(time.Second) certPool := x509.NewCertPool() certPool.AppendCertsFromPEM(certContent) tlsConf := &tls.Config{ RootCAs: certPool, ServerName: "example.com", NextProtos: []string{"h3"}, } // Define custom cache. gets := make(chan string, 100) puts := make(chan string, 100) cache := newClientSessionCache(tls.NewLRUClientSessionCache(10), gets, puts) tlsConf.ClientSessionCache = cache // This first DialAddrEarly connection is here to populate the cache. earlyConnection, err := quic.DialAddrEarly(context.Background(), "127.0.0.1:8090", tlsConf, &quic.Config{}) require.NoError(t, err) t.Cleanup(func() { _ = earlyConnection.CloseWithError(0, "") entryPoint.Shutdown(ctx) }) // wait for the handshake complete. <-earlyConnection.HandshakeComplete() // 0RTT is always false on the first connection. require.False(t, earlyConnection.ConnectionState().Used0RTT) earlyConnection, err = quic.DialAddrEarly(context.Background(), "127.0.0.1:8090", tlsConf, &quic.Config{}) require.NoError(t, err) <-earlyConnection.HandshakeComplete() // 0RTT need to be false. assert.False(t, earlyConnection.ConnectionState().Used0RTT) } type clientSessionCache struct { cache tls.ClientSessionCache gets chan<- string puts chan<- string } func newClientSessionCache(cache tls.ClientSessionCache, gets, puts chan<- string) *clientSessionCache { return &clientSessionCache{ cache: cache, gets: gets, puts: puts, } } var _ tls.ClientSessionCache = &clientSessionCache{} func (c *clientSessionCache) Get(sessionKey string) (*tls.ClientSessionState, bool) { session, ok := c.cache.Get(sessionKey) c.gets <- sessionKey return session, ok } func (c *clientSessionCache) Put(sessionKey string, cs *tls.ClientSessionState) { c.cache.Put(sessionKey, cs) c.puts <- sessionKey }