Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chotiwat/ssl passthrough e2e #10988

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion test/e2e/framework/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ func newDeployment(name, namespace, image string, port int32, replicas int32, co
{
Name: name,
Image: image,
Env: []corev1.EnvVar{},
Env: env,
Ports: []corev1.ContainerPort{
{
Name: "http",
Expand Down
35 changes: 27 additions & 8 deletions test/e2e/framework/httpexpect/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, addr 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, addr 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
}
Expand Down
294 changes: 195 additions & 99 deletions test/e2e/settings/ssl_passthrough.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -75,114 +75,210 @@ 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"
url := "https://" + net.JoinHostPort(host, "443")
echoName := "echopass"
secretName := host

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",
}

/* 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)

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,
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: secretName,
},
},
},
},
}
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.EnsureIngress(ingressDef)

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")
}
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.EnsureIngress(ingressDef)

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

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"
urlBad := "https://" + net.JoinHostPort(hostBad, "443")
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(url).
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(urlBad).
ForceResolve(f.GetNginxIP(), 443).
Expect().
Status(http.StatusNotFound)

f.HTTPTestClientWithTLSConfig(tlsConfig).
GET("/").
WithURL(url).
ForceResolve(f.GetNginxIP(), 443).
Expect().
Status(http.StatusOK)

f.HTTPTestClientWithTLSConfig(tlsConfigBad).
GET("/").
WithURL(urlBad).
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: len(host) / 3,
}, err
}
}

ginkgo.It("should handle known traffic without Host header", func() {
f.HTTPTestClientWithTLSConfig(tlsConfig).
GET("/").
WithURL(url).
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 insecure traffic without Host header", func() {
//nolint:gosec // Ignore the gosec error in testing
f.HTTPTestClientWithTLSConfig(&tls.Config{ServerName: host, InsecureSkipVerify: true}).
GET("/").
WithURL(url).
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)
ginkgo.It("should handle known traffic with Host header", func() {
f.HTTPTestClientWithTLSConfig(tlsConfig).
GET("/").
WithURL(url).
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: 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() {
//nolint:gosec // Ignore the gosec error in testing
f.HTTPTestClientWithTLSConfig(&tls.Config{ServerName: host, InsecureSkipVerify: true}).
GET("/").
WithURL(url).
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
}
Loading