From 8292585bb4b9b6264a1363e908b1272e57631476 Mon Sep 17 00:00:00 2001 From: Chotiwat Chawannakul Date: Fri, 16 Feb 2024 01:38:01 -0800 Subject: [PATCH] add e2e tests --- test/e2e/framework/httpexpect/request.go | 35 ++- test/e2e/settings/ssl_passthrough.go | 282 +++++++++++++++-------- 2 files changed, 215 insertions(+), 102 deletions(-) diff --git a/test/e2e/framework/httpexpect/request.go b/test/e2e/framework/httpexpect/request.go index 4daba136e..c3854a6eb 100644 --- a/test/e2e/framework/httpexpect/request.go +++ b/test/e2e/framework/httpexpect/request.go @@ -80,22 +80,41 @@ func (h *HTTPRequest) ForceResolve(ip string, port uint16) *HTTPRequest { h.chain.fail(fmt.Sprintf("invalid ip address: %s", ip)) return h } - dialer := &net.Dialer{ - Timeout: h.client.Timeout, - KeepAlive: h.client.Timeout, - DualStack: true, - } resolveAddr := fmt.Sprintf("%s:%d", ip, int(port)) + return h.WithDialContextMiddleware(func(next DialContextFunc) DialContextFunc { + return func(ctx context.Context, network, _ string) (net.Conn, error) { + return next(ctx, network, resolveAddr) + } + }) +} + +// DialContextFunc is the function signature for `DialContext` +type DialContextFunc func(ctx context.Context, network, addr string) (net.Conn, error) + +// WithDialContextMiddleware sets the `DialContext` function of the client +// transport with a new function returns from `fn`. An existing `DialContext` +// is passed into `fn` so the new function can act as a middleware by calling +// the old one. +func (h *HTTPRequest) WithDialContextMiddleware(fn func(next DialContextFunc) DialContextFunc) *HTTPRequest { oldTransport, ok := h.client.Transport.(*http.Transport) if !ok { h.chain.fail("invalid old transport address") return h } - newTransport := oldTransport.Clone() - newTransport.DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) { - return dialer.DialContext(ctx, network, resolveAddr) + var nextDialContext DialContextFunc + if oldTransport.DialContext != nil { + nextDialContext = oldTransport.DialContext + } else { + dialer := &net.Dialer{ + Timeout: h.client.Timeout, + KeepAlive: h.client.Timeout, + DualStack: true, + } + nextDialContext = dialer.DialContext } + newTransport := oldTransport.Clone() + newTransport.DialContext = fn(nextDialContext) h.client.Transport = newTransport return h } diff --git a/test/e2e/settings/ssl_passthrough.go b/test/e2e/settings/ssl_passthrough.go index b10511bde..2aa11eb07 100644 --- a/test/e2e/settings/ssl_passthrough.go +++ b/test/e2e/settings/ssl_passthrough.go @@ -27,10 +27,10 @@ import ( "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/ingress-nginx/test/e2e/framework" + "k8s.io/ingress-nginx/test/e2e/framework/httpexpect" ) var _ = framework.IngressNginxDescribe("[Flag] enable-ssl-passthrough", func() { @@ -75,114 +75,208 @@ var _ = framework.IngressNginxDescribe("[Flag] enable-ssl-passthrough", func() { Status(http.StatusNotFound) }) - ginkgo.It("should pass unknown traffic to default backend and handle known traffic", func() { + ginkgo.Context("when handling traffic", func() { + var tlsConfig *tls.Config host := "testpassthrough.com" echoName := "echopass" + secretName := host - /* Even with enable-ssl-passthrough enabled, only annotated ingresses may receive the traffic */ - annotations := map[string]string{ - "nginx.ingress.kubernetes.io/ssl-passthrough": "true", - } + ginkgo.BeforeEach(func() { + /* Even with enable-ssl-passthrough enabled, only annotated ingresses may receive the traffic */ + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/ssl-passthrough": "true", + } - ingressDef := framework.NewSingleIngressWithTLS(host, - "/", - host, - []string{host}, - f.Namespace, - echoName, - 80, - annotations) - tlsConfig, err := framework.CreateIngressTLSSecret(f.KubeClientSet, - ingressDef.Spec.TLS[0].Hosts, - ingressDef.Spec.TLS[0].SecretName, - ingressDef.Namespace) + ingressDef := framework.NewSingleIngress(host, + "/", + host, + f.Namespace, + echoName, + 80, + annotations) + var err error + tlsConfig, err = framework.CreateIngressTLSSecret(f.KubeClientSet, + []string{host}, + secretName, + ingressDef.Namespace) - volumeMount := []corev1.VolumeMount{ - { - Name: "certs", - ReadOnly: true, - MountPath: "/certs", - }, - } - volume := []corev1.Volume{ - { - Name: "certs", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: ingressDef.Spec.TLS[0].SecretName, + volumeMount := []corev1.VolumeMount{ + { + Name: "certs", + ReadOnly: true, + MountPath: "/certs", + }, + } + volume := []corev1.Volume{ + { + Name: "certs", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + }, }, }, - }, - } - envs := []corev1.EnvVar{ - { - Name: "HTTPBUN_SSL_CERT", - Value: "/certs/tls.crt", - }, - { - Name: "HTTPBUN_SSL_KEY", - Value: "/certs/tls.key", - }, - } + } + envs := []corev1.EnvVar{ + { + Name: "HTTPBUN_SSL_CERT", + Value: "/certs/tls.crt", + }, + { + Name: "HTTPBUN_SSL_KEY", + Value: "/certs/tls.key", + }, + } - f.NewDeploymentWithOpts("echopass", - framework.HTTPBunImage, - 80, - 1, - nil, - nil, - envs, - volumeMount, - volume, - false) + f.NewDeploymentWithOpts("echopass", + framework.HTTPBunImage, + 80, + 1, + nil, + nil, + envs, + volumeMount, + volume, + false) - f.EnsureIngress(ingressDef) + f.EnsureIngress(ingressDef) - assert.Nil(ginkgo.GinkgoT(), err) - framework.WaitForTLS(f.GetURL(framework.HTTPS), tlsConfig) + assert.Nil(ginkgo.GinkgoT(), err) + framework.WaitForTLS(f.GetURL(framework.HTTPS), tlsConfig) - f.WaitForNginxServer(host, - func(server string) bool { - return strings.Contains(server, "listen 442") + f.WaitForNginxServer(host, + func(server string) bool { + return strings.Contains(server, "listen 442") + }) + }) + + ginkgo.It("should pass unknown traffic to default backend and handle known traffic", func() { + /* This one should not receive traffic as it does not contain passthrough annotation */ + hostBad := "noannotationnopassthrough.com" + ingBad := f.EnsureIngress(framework.NewSingleIngressWithTLS(hostBad, + "/", + hostBad, + []string{hostBad}, + f.Namespace, + echoName, + 80, + nil)) + tlsConfigBad, err := framework.CreateIngressTLSSecret(f.KubeClientSet, + ingBad.Spec.TLS[0].Hosts, + ingBad.Spec.TLS[0].SecretName, + ingBad.Namespace) + assert.Nil(ginkgo.GinkgoT(), err) + framework.WaitForTLS(f.GetURL(framework.HTTPS), tlsConfigBad) + + f.WaitForNginxServer(hostBad, + func(server string) bool { + return strings.Contains(server, "listen 442") + }) + + //nolint:gosec // Ignore the gosec error in testing + f.HTTPTestClientWithTLSConfig(&tls.Config{ServerName: host, InsecureSkipVerify: true}). + GET("/"). + WithURL("https://"+net.JoinHostPort(host, "443")). + ForceResolve(f.GetNginxIP(), 443). + Expect(). + Status(http.StatusOK) + + //nolint:gosec // Ignore the gosec error in testing + f.HTTPTestClientWithTLSConfig(&tls.Config{ServerName: hostBad, InsecureSkipVerify: true}). + GET("/"). + WithURL("https://"+net.JoinHostPort(hostBad, "443")). + ForceResolve(f.GetNginxIP(), 443). + Expect(). + Status(http.StatusNotFound) + + //nolint:gosec // Ignore the gosec error in testing + f.HTTPTestClientWithTLSConfig(tlsConfig). + GET("/"). + WithURL("https://"+net.JoinHostPort(host, "443")). + ForceResolve(f.GetNginxIP(), 443). + Expect(). + Status(http.StatusOK) + + //nolint:gosec // Ignore the gosec error in testing + f.HTTPTestClientWithTLSConfig(tlsConfigBad). + GET("/"). + WithURL("https://"+net.JoinHostPort(hostBad, "443")). + ForceResolve(f.GetNginxIP(), 443). + Expect(). + Status(http.StatusNotFound) + }) + + ginkgo.Context("on throttled connections", func() { + throttleMiddleware := func(next httpexpect.DialContextFunc) httpexpect.DialContextFunc { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + // Wrap the connection with a throttled writer to simulate real + // world traffic where streaming data may arrive in chunks + conn, err := next(ctx, network, addr) + return &writeThrottledConn{ + Conn: conn, + chunkSize: 50, + }, err + } + } + tries := 3 + + ginkgo.It("should handle known traffic without Host header", func() { + for i := 0; i < tries; i++ { + //nolint:gosec // Ignore the gosec error in testing + f.HTTPTestClientWithTLSConfig(&tls.Config{ServerName: host, InsecureSkipVerify: true}). + GET("/"). + WithURL("https://"+net.JoinHostPort(host, "443")). + ForceResolve(f.GetNginxIP(), 443). + WithDialContextMiddleware(throttleMiddleware). + Expect(). + Status(http.StatusOK) + } }) - /* This one should not receive traffic as it does not contain passthrough annotation */ - hostBad := "noannotationnopassthrough.com" - ingBad := f.EnsureIngress(framework.NewSingleIngressWithTLS(hostBad, - "/", - hostBad, - []string{hostBad}, - f.Namespace, - echoName, - 80, - nil)) - tlsConfigBad, err := framework.CreateIngressTLSSecret(f.KubeClientSet, - ingBad.Spec.TLS[0].Hosts, - ingBad.Spec.TLS[0].SecretName, - ingBad.Namespace) - assert.Nil(ginkgo.GinkgoT(), err) - framework.WaitForTLS(f.GetURL(framework.HTTPS), tlsConfigBad) - - f.WaitForNginxServer(hostBad, - func(server string) bool { - return strings.Contains(server, "listen 442") + ginkgo.It("should handle known traffic with Host header", func() { + for i := 0; i < tries; i++ { + //nolint:gosec // Ignore the gosec error in testing + f.HTTPTestClientWithTLSConfig(tlsConfig). + GET("/"). + WithURL("https://"+net.JoinHostPort(host, "443")). + WithHeader("Host", host). + ForceResolve(f.GetNginxIP(), 443). + WithDialContextMiddleware(throttleMiddleware). + Expect(). + Status(http.StatusOK) + } }) - //nolint:gosec // Ignore the gosec error in testing - f.HTTPTestClientWithTLSConfig(&tls.Config{ServerName: host, InsecureSkipVerify: true}). - GET("/"). - WithURL("https://"+net.JoinHostPort(host, "443")). - ForceResolve(f.GetNginxIP(), 443). - Expect(). - Status(http.StatusOK) - - //nolint:gosec // Ignore the gosec error in testing - f.HTTPTestClientWithTLSConfig(&tls.Config{ServerName: hostBad, InsecureSkipVerify: true}). - GET("/"). - WithURL("https://"+net.JoinHostPort(hostBad, "443")). - ForceResolve(f.GetNginxIP(), 443). - Expect(). - Status(http.StatusNotFound) + ginkgo.It("should handle insecure traffic with Host header", func() { + for i := 0; i < tries; i++ { + //nolint:gosec // Ignore the gosec error in testing + f.HTTPTestClientWithTLSConfig(&tls.Config{ServerName: host, InsecureSkipVerify: true}). + GET("/"). + WithURL("https://"+net.JoinHostPort(host, "443")). + WithHeader("Host", host). + ForceResolve(f.GetNginxIP(), 443). + WithDialContextMiddleware(throttleMiddleware). + Expect(). + Status(http.StatusOK) + } + }) + }) }) }) }) + +type writeThrottledConn struct { + net.Conn + chunkSize int +} + +// Write writes data to the connection `chunkSize` bytes (or less) at a time. +func (c *writeThrottledConn) Write(b []byte) (n int, err error) { + for i := 0; i < len(b); i += c.chunkSize { + n, err := c.Conn.Write(b[i:min(i+c.chunkSize, len(b))]) + if err != nil { + return i + n, err + } + } + return len(b), nil +}