Skip to content

Commit 6d9ab9e

Browse files
committed
WIP Issue 3138 - Conformance Tests for BackendTLSPolicy - normative
1 parent 9787352 commit 6d9ab9e

File tree

10 files changed

+325
-10
lines changed

10 files changed

+325
-10
lines changed

conformance/base/manifests.yaml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,3 +730,66 @@ data:
730730
foo.bar.com:53 {
731731
whoami
732732
}
733+
---
734+
apiVersion: v1
735+
kind: Service
736+
metadata:
737+
name: backendtlspolicy-test
738+
namespace: gateway-conformance-infra
739+
spec:
740+
selector:
741+
app: backendtlspolicy-test
742+
ports:
743+
- protocol: TCP
744+
port: 443
745+
targetPort: 8443
746+
---
747+
apiVersion: apps/v1
748+
kind: Deployment
749+
metadata:
750+
name: backendtlspolicy-test
751+
namespace: gateway-conformance-infra
752+
labels:
753+
app: backendtlspolicy-test
754+
spec:
755+
replicas: 1
756+
selector:
757+
matchLabels:
758+
app: backendtlspolicy-test
759+
template:
760+
metadata:
761+
labels:
762+
app: backendtlspolicy-test
763+
spec:
764+
containers:
765+
- name: backendtlspolicy-test
766+
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
767+
volumeMounts:
768+
- name: secret-volume
769+
mountPath: /etc/secret-volume
770+
env:
771+
- name: POD_NAME
772+
valueFrom:
773+
fieldRef:
774+
fieldPath: metadata.name
775+
- name: NAMESPACE
776+
valueFrom:
777+
fieldRef:
778+
fieldPath: metadata.namespace
779+
- name: CA_CERT
780+
value: /etc/secret-volume/crt
781+
- name: CA_CERT_KEY
782+
value: /etc/secret-volume/key
783+
resources:
784+
requests:
785+
cpu: 10m
786+
volumes:
787+
- name: secret-volume
788+
secret:
789+
secretName: backend-tls-checks-certificate
790+
items:
791+
- key: tls.crt
792+
path: crt
793+
- key: tls.key
794+
path: key
795+
---

conformance/echo-basic/echo-basic.go

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@ import (
2323
"encoding/pem"
2424
"fmt"
2525
"io"
26+
"net"
2627
"net/http"
2728
"os"
2829
"regexp"
2930
"strconv"
3031
"strings"
3132
"time"
3233

34+
"github.com/paultag/sniff/parser"
35+
3336
"golang.org/x/net/http2"
3437
"golang.org/x/net/http2/h2c"
3538
"golang.org/x/net/websocket"
@@ -48,15 +51,17 @@ type RequestAssertions struct {
4851
Context `json:",inline"`
4952

5053
TLS *TLSAssertions `json:"tls,omitempty"`
54+
SNI string `json:"sni"`
5155
}
5256

5357
// TLSAssertions contains information about the TLS connection.
5458
type TLSAssertions struct {
55-
Version string `json:"version"`
56-
PeerCertificates []string `json:"peerCertificates,omitempty"`
57-
ServerName string `json:"serverName"`
58-
NegotiatedProtocol string `json:"negotiatedProtocol,omitempty"`
59-
CipherSuite string `json:"cipherSuite"`
59+
Version string `json:"version"`
60+
PeerCertificates []string `json:"peerCertificates,omitempty"`
61+
// ServerName is the SNI.
62+
ServerName string `json:"serverName"`
63+
NegotiatedProtocol string `json:"negotiatedProtocol,omitempty"`
64+
CipherSuite string `json:"cipherSuite"`
6065
}
6166

6267
type preserveSlashes struct {
@@ -109,6 +114,7 @@ func main() {
109114
httpMux.HandleFunc("/health", healthHandler)
110115
httpMux.HandleFunc("/status/", statusHandler)
111116
httpMux.HandleFunc("/", echoHandler)
117+
httpMux.HandleFunc("/backendTLS", echoHandler)
112118
httpMux.Handle("/ws", websocket.Handler(wsHandler))
113119
httpHandler := &preserveSlashes{httpMux}
114120

@@ -124,11 +130,13 @@ func main() {
124130

125131
go runH2CServer(h2cPort, errchan)
126132

127-
// Enable HTTPS if certificate and private key are given.
128-
if os.Getenv("TLS_SERVER_CERT") != "" && os.Getenv("TLS_SERVER_PRIVKEY") != "" {
133+
// Enable HTTPS if server certificate and private key are given. (TLS_SERVER_CERT, TLS_SERVER_PRIVKEY)
134+
// Enable secure backend if CA certificate and key are given. (CA_CERT, CA_CERT_KEY)
135+
if os.Getenv("TLS_SERVER_CERT") != "" && os.Getenv("TLS_SERVER_PRIVKEY") != "" ||
136+
os.Getenv("CA_CERT") != "" && os.Getenv("CA_CERT_KEY") != "" {
129137
go func() {
130138
fmt.Printf("Starting server, listening on port %s (https)\n", httpsPort)
131-
err := listenAndServeTLS(fmt.Sprintf(":%s", httpsPort), os.Getenv("TLS_SERVER_CERT"), os.Getenv("TLS_SERVER_PRIVKEY"), os.Getenv("TLS_CLIENT_CACERTS"), httpHandler)
139+
err := listenAndServeTLS(fmt.Sprintf(":%s", httpsPort), os.Getenv("TLS_SERVER_CERT"), os.Getenv("TLS_SERVER_PRIVKEY"), os.Getenv("CA_CERT"), httpHandler)
132140
if err != nil {
133141
errchan <- err
134142
}
@@ -201,15 +209,27 @@ func runH2CServer(h2cPort string, errchan chan<- error) {
201209
}
202210

203211
func echoHandler(w http.ResponseWriter, r *http.Request) {
212+
var sni string
213+
204214
fmt.Printf("Echoing back request made to %s to client (%s)\n", r.RequestURI, r.RemoteAddr)
205215

206216
// If the request has form ?delay=[:duration] wait for duration
207217
// For example, ?delay=10s will cause the response to wait 10s before responding
208-
if err := delayResponse(r); err != nil {
218+
err := delayResponse(r)
219+
if err != nil {
209220
processError(w, err, http.StatusInternalServerError)
210221
return
211222
}
212223

224+
// If the request was made to URI backendTLS, then get the server name indication and
225+
// add it to the RequestAssertions. It will be echoed back later.
226+
if strings.Contains(r.RequestURI, "backendTLS") {
227+
sni, err = sniffForSNI(r.RemoteAddr)
228+
if err != nil {
229+
// Todo: research if for some test cases there won't be one
230+
}
231+
}
232+
213233
requestAssertions := RequestAssertions{
214234
r.RequestURI,
215235
r.Host,
@@ -220,6 +240,7 @@ func echoHandler(w http.ResponseWriter, r *http.Request) {
220240
context,
221241

222242
tlsStateToAssertions(r.TLS),
243+
sni,
223244
}
224245

225246
js, err := json.MarshalIndent(requestAssertions, "", " ")
@@ -296,6 +317,40 @@ func listenAndServeTLS(addr string, serverCert string, serverPrivKey string, cli
296317
return srv.ListenAndServeTLS(serverCert, serverPrivKey)
297318
}
298319

320+
// sniffForSNI uses the request address to listen for the incoming TLS connection,
321+
// and tries to find the server name indication from that connection.
322+
func sniffForSNI(addr string) (string, error) {
323+
var sni string
324+
325+
// Listen to get the SNI, and store in config.
326+
listener, err := net.Listen("tcp", addr)
327+
if err != nil {
328+
return "", err
329+
}
330+
defer listener.Close()
331+
332+
for {
333+
conn, err := listener.Accept()
334+
if err != nil {
335+
return "", err
336+
}
337+
data := make([]byte, 4096)
338+
_, err = conn.Read(data)
339+
if err != nil {
340+
return "", fmt.Errorf("could not read socket: %v", err)
341+
}
342+
// Take an incoming TLS Client Hello and return the SNI name.
343+
sni, err = parser.GetHostname(data[:])
344+
if err != nil {
345+
return "", fmt.Errorf("error getting SNI: %v", err)
346+
}
347+
if sni == "" {
348+
return "", fmt.Errorf("no server name indication found")
349+
}
350+
return sni, nil
351+
}
352+
}
353+
299354
func tlsStateToAssertions(connectionState *tls.ConnectionState) *TLSAssertions {
300355
if connectionState != nil {
301356
var state TLSAssertions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package tests
18+
19+
import (
20+
"testing"
21+
22+
"k8s.io/apimachinery/pkg/types"
23+
"sigs.k8s.io/gateway-api/conformance/utils/http"
24+
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
25+
"sigs.k8s.io/gateway-api/conformance/utils/suite"
26+
"sigs.k8s.io/gateway-api/pkg/features"
27+
)
28+
29+
func init() {
30+
ConformanceTests = append(ConformanceTests, BackendTLSPolicyNormative)
31+
}
32+
33+
var BackendTLSPolicyNormative = suite.ConformanceTest{
34+
ShortName: "BackendTLSPolicyNormative",
35+
Description: "A single service that is targeted by a BackendTLSPolicy must successfully complete TLS termination",
36+
Features: []features.SupportedFeature{
37+
features.SupportGateway,
38+
features.SupportBackendTLSPolicy,
39+
},
40+
Manifests: []string{"tests/backendtlspolicy-normative.yaml"},
41+
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
42+
ns := "gateway-conformance-infra"
43+
routeNN := types.NamespacedName{Name: "gateway-conformance-infra-test", Namespace: ns}
44+
gwNN := types.NamespacedName{Name: "gateway-backendtlspolicy", Namespace: ns}
45+
46+
kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns})
47+
48+
gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
49+
50+
t.Run("Simple request targeting BackendTLSPolicy should reach infra-backend", func(t *testing.T) {
51+
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr,
52+
http.ExpectedResponse{
53+
Request: http.Request{
54+
Path: "/backendTLS",
55+
},
56+
Response: http.Response{StatusCode: 200},
57+
Backend: "infra-backend-v1",
58+
Namespace: "gateway-conformance-infra",
59+
SNI: "abc.example.com",
60+
// TODO - in addition to the SNI, we also need to check if the cert that was seen is the correct one.
61+
})
62+
})
63+
},
64+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
apiVersion: gateway.networking.k8s.io/v1beta1
2+
kind: Gateway
3+
metadata:
4+
name: gateway-backendtlspolicy
5+
namespace: gateway-conformance-infra
6+
spec:
7+
gatewayClassName: "{GATEWAY_CLASS_NAME}"
8+
listeners:
9+
- name: https
10+
port: 443
11+
protocol: HTTPS
12+
hostname: "*.example.com"
13+
allowedRoutes:
14+
namespaces:
15+
from: Same
16+
kinds:
17+
- kind: HTTPRoute
18+
---
19+
apiVersion: gateway.networking.k8s.io/v1alpha3
20+
kind: BackendTLSPolicy
21+
metadata:
22+
name: normative-test-backendtlspolicy
23+
namespace: gateway-conformance-infra
24+
spec:
25+
targetRefs:
26+
- group: ""
27+
kind: Service
28+
name: "backendtlspolicy-test"
29+
validation:
30+
caCertificateRefs:
31+
group: ""
32+
kind: Secret
33+
name: "backend-tls-checks-certificate"
34+
hostname: "abc.example.com"

conformance/utils/http/http.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ type ExpectedResponse struct {
5858

5959
// User Given TestCase name
6060
TestCaseName string
61+
62+
// SNI is the server name indication seen by the backend.
63+
SNI string
6164
}
6265

6366
// Request can be used as both the request to make and a means to verify

0 commit comments

Comments
 (0)