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

Issue 3138 - Conformance Tests for BackendTLSPolicy - normative #3212

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
26 changes: 13 additions & 13 deletions conformance/base/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ spec:
spec:
containers:
- name: infra-backend-v1
# From https://github.com/kubernetes-sigs/ingress-controller-conformance/tree/master/images/echoserver
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
# Originally from https://github.com/kubernetes-sigs/ingress-controller-conformance/tree/master/images/echoserver
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9
env:
- name: POD_NAME
valueFrom:
Expand Down Expand Up @@ -185,7 +185,7 @@ spec:
spec:
containers:
- name: infra-backend-v2
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9
env:
- name: POD_NAME
valueFrom:
Expand Down Expand Up @@ -231,7 +231,7 @@ spec:
spec:
containers:
- name: infra-backend-v3
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9
env:
- name: POD_NAME
valueFrom:
Expand Down Expand Up @@ -277,7 +277,7 @@ spec:
spec:
containers:
- name: tls-backend
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9
volumeMounts:
- name: secret-volume
mountPath: /etc/secret-volume
Expand All @@ -300,7 +300,7 @@ spec:
volumes:
- name: secret-volume
secret:
secretName: tls-passthrough-checks-certificate
secretName: tls-checks-certificate
items:
- key: tls.crt
path: crt
Expand Down Expand Up @@ -346,7 +346,7 @@ spec:
spec:
containers:
- name: tls-backend
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9
volumeMounts:
- name: secret-volume
mountPath: /etc/secret-volume
Expand Down Expand Up @@ -408,7 +408,7 @@ spec:
spec:
containers:
- name: app-backend-v1
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9
env:
- name: POD_NAME
valueFrom:
Expand Down Expand Up @@ -454,7 +454,7 @@ spec:
spec:
containers:
- name: app-backend-v2
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9
env:
- name: POD_NAME
valueFrom:
Expand Down Expand Up @@ -507,7 +507,7 @@ spec:
spec:
containers:
- name: web-backend
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9
env:
- name: POD_NAME
valueFrom:
Expand Down Expand Up @@ -554,7 +554,7 @@ spec:
spec:
containers:
- name: grpc-infra-backend-v1
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9
env:
- name: POD_NAME
valueFrom:
Expand Down Expand Up @@ -603,7 +603,7 @@ spec:
spec:
containers:
- name: grpc-infra-backend-v2
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9
env:
- name: POD_NAME
valueFrom:
Expand Down Expand Up @@ -652,7 +652,7 @@ spec:
spec:
containers:
- name: grpc-infra-backend-v3
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9
env:
- name: POD_NAME
valueFrom:
Expand Down
1 change: 1 addition & 0 deletions conformance/echo-basic/.go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module sigs.k8s.io/gateway-api/conformance/echo-basic
go 1.21

require (
github.com/paultag/sniff v0.0.0-20200207005214-cf7e4d167732
golang.org/x/net v0.21.0
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1
Expand Down
2 changes: 2 additions & 0 deletions conformance/echo-basic/.go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/paultag/sniff v0.0.0-20200207005214-cf7e4d167732 h1:nkseUkzjazCNyGhkRwnJ1OiHSwMXazsJQx+Ci+oVLEM=
github.com/paultag/sniff v0.0.0-20200207005214-cf7e4d167732/go.mod h1:J3XXNGJINXLa4yIivdUT0Ad/srv2q0pSOWbbm6El2EY=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
Expand Down
75 changes: 66 additions & 9 deletions conformance/echo-basic/echo-basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ import (
"encoding/pem"
"fmt"
"io"
"net"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"

"github.com/paultag/sniff/parser"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"golang.org/x/net/websocket"
Expand All @@ -48,15 +50,17 @@ type RequestAssertions struct {
Context `json:",inline"`

TLS *TLSAssertions `json:"tls,omitempty"`
SNI string `json:"sni"`
}

// TLSAssertions contains information about the TLS connection.
type TLSAssertions struct {
Version string `json:"version"`
PeerCertificates []string `json:"peerCertificates,omitempty"`
ServerName string `json:"serverName"`
NegotiatedProtocol string `json:"negotiatedProtocol,omitempty"`
CipherSuite string `json:"cipherSuite"`
Version string `json:"version"`
PeerCertificates []string `json:"peerCertificates,omitempty"`
// ServerName is the name sent from the peer using SNI.
ServerName string `json:"serverName"`
NegotiatedProtocol string `json:"negotiatedProtocol,omitempty"`
CipherSuite string `json:"cipherSuite"`
}

type preserveSlashes struct {
Expand Down Expand Up @@ -109,6 +113,7 @@ func main() {
httpMux.HandleFunc("/health", healthHandler)
httpMux.HandleFunc("/status/", statusHandler)
httpMux.HandleFunc("/", echoHandler)
httpMux.HandleFunc("/backendTLS", echoHandler)
httpMux.Handle("/ws", websocket.Handler(wsHandler))
httpHandler := &preserveSlashes{httpMux}

Expand All @@ -124,11 +129,13 @@ func main() {

go runH2CServer(h2cPort, errchan)

// Enable HTTPS if certificate and private key are given.
if os.Getenv("TLS_SERVER_CERT") != "" && os.Getenv("TLS_SERVER_PRIVKEY") != "" {
// Enable HTTPS if server certificate and private key are given. (TLS_SERVER_CERT, TLS_SERVER_PRIVKEY)
// Enable secure backend if CA certificate and key are given. (CA_CERT, CA_CERT_KEY)
if os.Getenv("TLS_SERVER_CERT") != "" && os.Getenv("TLS_SERVER_PRIVKEY") != "" ||
os.Getenv("CA_CERT") != "" && os.Getenv("CA_CERT_KEY") != "" {
go func() {
fmt.Printf("Starting server, listening on port %s (https)\n", httpsPort)
err := listenAndServeTLS(fmt.Sprintf(":%s", httpsPort), os.Getenv("TLS_SERVER_CERT"), os.Getenv("TLS_SERVER_PRIVKEY"), os.Getenv("TLS_CLIENT_CACERTS"), httpHandler)
err := listenAndServeTLS(fmt.Sprintf(":%s", httpsPort), os.Getenv("TLS_SERVER_CERT"), os.Getenv("TLS_SERVER_PRIVKEY"), os.Getenv("CA_CERT"), httpHandler)
if err != nil {
errchan <- err
}
Expand Down Expand Up @@ -201,15 +208,29 @@ func runH2CServer(h2cPort string, errchan chan<- error) {
}

func echoHandler(w http.ResponseWriter, r *http.Request) {
var sni string

fmt.Printf("Echoing back request made to %s to client (%s)\n", r.RequestURI, r.RemoteAddr)

// If the request has form ?delay=[:duration] wait for duration
// For example, ?delay=10s will cause the response to wait 10s before responding
if err := delayResponse(r); err != nil {
err := delayResponse(r)
if err != nil {
processError(w, err, http.StatusInternalServerError)
return
}

// If the request was made to URI backendTLS, then get the server name indication and
// add it to the RequestAssertions. It will be echoed back later.
if strings.Contains(r.RequestURI, "backendTLS") {
sni, err = sniffForSNI(r.RemoteAddr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get how this works. Are we supposed to send an HTTP request and then send a TLS request later?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are just serving this as TLS already you can just do r.TLS.ServerName I think

Copy link
Contributor Author

@candita candita Aug 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that you send an HTTP or HTTPS request and the BackendTLSPolicy adds what is needed for the backend request to succeed (certificate and SNI). The echo service finds the SNI and echoes it back to the test to prove the BackendTLSPolicy was used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still need to dig into this a bit deeper, but I think #3212 (comment) is relevant to this question.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic seems to be: on an HTTP request, on a new TCP listener, accept a new connection, get the TLS handshake data, then close the connection.

Should it not be: accept an HTTPS request and report the SNI?

Copy link
Contributor Author

@candita candita Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two connections - one terminates at the gateway and one authenticates with the backend. I was attempting to get the SNI from the second connection here. Maybe this isn't correct, but just accepting one connection is also not correct. Advice gladly accepted @shaneutt and @howardjohn.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the backend though. We don't want to make a connection to ourself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we want is

Test binary -- (1) TLS 1-->Gateway --(2) TLS 2 --> test app

Connection 1 is establihsed by the go test. Connection 2 is established by the gateway implementation under test.

The test app does not establish a connection, it accepts one.

if err != nil {
// TODO: research if for some test cases there won't be SNI available.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this TODO we should either:

a) resolve it
b) make sure it has an issue (or is part of some issue) somewhere that we can link here

Ideally, we resolve it but iterative PRs are OK too as long as we have good follow-ups between them.

processError(w, err, http.StatusBadGateway)
return
}
}

requestAssertions := RequestAssertions{
r.RequestURI,
r.Host,
Expand All @@ -220,6 +241,7 @@ func echoHandler(w http.ResponseWriter, r *http.Request) {
context,

tlsStateToAssertions(r.TLS),
sni,
}

js, err := json.MarshalIndent(requestAssertions, "", " ")
Expand Down Expand Up @@ -296,6 +318,41 @@ func listenAndServeTLS(addr string, serverCert string, serverPrivKey string, cli
return srv.ListenAndServeTLS(serverCert, serverPrivKey)
}

// sniffForSNI uses the request address to listen for the incoming TLS connection,
// and tries to find the server name indication from that connection.
func sniffForSNI(addr string) (string, error) {
var sni string

// Listen to get the SNI, and store in config.
listener, err := net.Listen("tcp", addr)
if err != nil {
return "", err
}
defer listener.Close()

for {
conn, err := listener.Accept()
if err != nil {
return "", err
}
data := make([]byte, 4096)
_, err = conn.Read(data)
if err != nil {
return "", fmt.Errorf("could not read socket: %v", err)
}
// Take an incoming TLS Client Hello and return the SNI name.
sni, err = parser.GetHostname(data)
if err != nil {
return "", fmt.Errorf("error getting SNI: %v", err)
}
if sni == "" {
return "", fmt.Errorf("no server name indication found")
} else { //nolint:revive
return sni, nil
}
}
}

Comment on lines +321 to +355
Copy link
Member

@shaneutt shaneutt Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we potentially wanna use http.ListenAndServeTLS here instead of a TCP listener so that we can record (and then assert) the client payload?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand. Right now all we want is the SNI in this function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So take this as a question not a statement, but what I was getting at was: wouldn't we want to verify receipt of the payload we sent (which should be unique), in addition to the SNI such that we can be certain the request received was the one intended?

func tlsStateToAssertions(connectionState *tls.ConnectionState) *TLSAssertions {
if connectionState != nil {
var state TLSAssertions
Expand Down
70 changes: 70 additions & 0 deletions conformance/tests/backendtlspolicy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
Copyright 2024 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package tests

import (
"testing"

"k8s.io/apimachinery/pkg/types"

"sigs.k8s.io/gateway-api/conformance/utils/http"
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
"sigs.k8s.io/gateway-api/conformance/utils/suite"
"sigs.k8s.io/gateway-api/pkg/features"
)

func init() {
ConformanceTests = append(ConformanceTests, BackendTLSPolicy)
}

var BackendTLSPolicy = suite.ConformanceTest{
ShortName: "BackendTLSPolicy",
Description: "A single service that is targeted by a BackendTLSPolicy must successfully complete TLS termination",
Features: []features.FeatureName{
features.SupportGateway,
features.SupportBackendTLSPolicy,
},
Manifests: []string{"tests/backendtlspolicy.yaml"},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
ns := "gateway-conformance-infra"
routeNN := types.NamespacedName{Name: "gateway-conformance-infra-test", Namespace: ns}
gwNN := types.NamespacedName{Name: "gateway-backendtlspolicy", Namespace: ns}

kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns})

gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)

serverStr := "abc.example.com"
headers := make(map[string]string)
headers["Host"] = serverStr

// Verify that the response to a call to /backendTLS will return the matching SNI.
t.Run("Simple request targeting BackendTLSPolicy should reach infra-backend", func(t *testing.T) {
http.MakeHTTPSRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr,
http.ExpectedResponse{
Request: http.Request{
Headers: headers,
Host: serverStr,
Path: "/backendTLS",
},
Response: http.Response{StatusCode: 200},
Namespace: "gateway-conformance-infra",
ServerName: serverStr,
})
})
shaneutt marked this conversation as resolved.
Show resolved Hide resolved
},
}
Loading