Skip to content

Commit

Permalink
WIP Issue 3138 - Conformance Tests for BackendTLSPolicy - normative
Browse files Browse the repository at this point in the history
  • Loading branch information
candita committed Aug 13, 2024
1 parent 9787352 commit 6d9ab9e
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 10 deletions.
63 changes: 63 additions & 0 deletions conformance/base/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -730,3 +730,66 @@ data:
foo.bar.com:53 {
whoami
}
---
apiVersion: v1
kind: Service
metadata:
name: backendtlspolicy-test
namespace: gateway-conformance-infra
spec:
selector:
app: backendtlspolicy-test
ports:
- protocol: TCP
port: 443
targetPort: 8443
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backendtlspolicy-test
namespace: gateway-conformance-infra
labels:
app: backendtlspolicy-test
spec:
replicas: 1
selector:
matchLabels:
app: backendtlspolicy-test
template:
metadata:
labels:
app: backendtlspolicy-test
spec:
containers:
- name: backendtlspolicy-test
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
volumeMounts:
- name: secret-volume
mountPath: /etc/secret-volume
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: CA_CERT
value: /etc/secret-volume/crt
- name: CA_CERT_KEY
value: /etc/secret-volume/key
resources:
requests:
cpu: 10m
volumes:
- name: secret-volume
secret:
secretName: backend-tls-checks-certificate
items:
- key: tls.crt
path: crt
- key: tls.key
path: key
---
73 changes: 64 additions & 9 deletions conformance/echo-basic/echo-basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ 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 +51,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 SNI.
ServerName string `json:"serverName"`
NegotiatedProtocol string `json:"negotiatedProtocol,omitempty"`
CipherSuite string `json:"cipherSuite"`
}

type preserveSlashes struct {
Expand Down Expand Up @@ -109,6 +114,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 +130,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 +209,27 @@ 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)
if err != nil {
// Todo: research if for some test cases there won't be one
}
}

requestAssertions := RequestAssertions{
r.RequestURI,
r.Host,
Expand All @@ -220,6 +240,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 +317,40 @@ 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")
}
return sni, nil
}
}

func tlsStateToAssertions(connectionState *tls.ConnectionState) *TLSAssertions {
if connectionState != nil {
var state TLSAssertions
Expand Down
64 changes: 64 additions & 0 deletions conformance/tests/backendtlspolicy-normative.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
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, BackendTLSPolicyNormative)
}

var BackendTLSPolicyNormative = suite.ConformanceTest{
ShortName: "BackendTLSPolicyNormative",
Description: "A single service that is targeted by a BackendTLSPolicy must successfully complete TLS termination",
Features: []features.SupportedFeature{
features.SupportGateway,
features.SupportBackendTLSPolicy,
},
Manifests: []string{"tests/backendtlspolicy-normative.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)

t.Run("Simple request targeting BackendTLSPolicy should reach infra-backend", func(t *testing.T) {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr,
http.ExpectedResponse{
Request: http.Request{
Path: "/backendTLS",
},
Response: http.Response{StatusCode: 200},
Backend: "infra-backend-v1",
Namespace: "gateway-conformance-infra",
SNI: "abc.example.com",
// TODO - in addition to the SNI, we also need to check if the cert that was seen is the correct one.
})
})
},
}
34 changes: 34 additions & 0 deletions conformance/tests/backendtlspolicy-normative.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
name: gateway-backendtlspolicy
namespace: gateway-conformance-infra
spec:
gatewayClassName: "{GATEWAY_CLASS_NAME}"
listeners:
- name: https
port: 443
protocol: HTTPS
hostname: "*.example.com"
allowedRoutes:
namespaces:
from: Same
kinds:
- kind: HTTPRoute
---
apiVersion: gateway.networking.k8s.io/v1alpha3
kind: BackendTLSPolicy
metadata:
name: normative-test-backendtlspolicy
namespace: gateway-conformance-infra
spec:
targetRefs:
- group: ""
kind: Service
name: "backendtlspolicy-test"
validation:
caCertificateRefs:
group: ""
kind: Secret
name: "backend-tls-checks-certificate"
hostname: "abc.example.com"
3 changes: 3 additions & 0 deletions conformance/utils/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ type ExpectedResponse struct {

// User Given TestCase name
TestCaseName string

// SNI is the server name indication seen by the backend.
SNI string
}

// Request can be used as both the request to make and a means to verify
Expand Down
Loading

0 comments on commit 6d9ab9e

Please sign in to comment.