diff --git a/conformance/tests/tlsroute-terminate-simple-same-namespace.go b/conformance/tests/tlsroute-terminate-simple-same-namespace.go new file mode 100644 index 0000000000..2ff96d9c4b --- /dev/null +++ b/conformance/tests/tlsroute-terminate-simple-same-namespace.go @@ -0,0 +1,129 @@ +/* +Copyright 2025 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 ( + "crypto/tls" + "crypto/x509" + "fmt" + "sync" + "testing" + "time" + + "k8s.io/apimachinery/pkg/types" + + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/pkg/features" + + mqtt "github.com/eclipse/paho.mqtt.golang" +) + +func init() { + ConformanceTests = append(ConformanceTests, TLSRouteTerminateSimpleSameNamespace) +} + +var TLSRouteTerminateSimpleSameNamespace = suite.ConformanceTest{ + ShortName: "TLSRouteTerminateSimpleSameNamespace", + Description: "A single TLSRoute in the gateway-conformance-infra namespace attaches to a Gateway using Terminate mode in the same namespace", + Features: []features.FeatureName{ + features.SupportGateway, + features.SupportTLSRoute, + features.SupportTLSRouteModeTerminate, + }, + Provisional: true, + Manifests: []string{"tests/tlsroute-terminate-simple-same-namespace.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "gateway-conformance-mqtt-test", Namespace: ns} + gwNN := types.NamespacedName{Name: "gateway-tlsroute-terminate", Namespace: ns} + caCertNN := types.NamespacedName{Name: "tls-checks-ca-certificate", Namespace: ns} + + kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns}) + + gwAddr, hostnames := kubernetes.GatewayAndTLSRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + if len(hostnames) != 1 { + t.Fatalf("unexpected error in test configuration, found %d hostnames", len(hostnames)) + } + serverStr := string(hostnames[0]) + + caConfigMap, err := kubernetes.GetConfigMapData(suite.Client, caCertNN) + if err != nil { + t.Fatalf("unexpected error finding TLS secret: %v", err) + } + caString, ok := caConfigMap["ca.crt"] + if !ok { + t.Fatalf("ca.crt not found in configmap: %s/%s", caCertNN.Namespace, caCertNN.Name) + } + + t.Run("Simple MQTT TLS request matching TLSRoute should reach mqtt-backend", func(t *testing.T) { + t.Logf("Establishing MQTT connection to host %s via %s", serverStr, gwAddr) + + certpool := x509.NewCertPool() + if !certpool.AppendCertsFromPEM([]byte(caString)) { + t.Fatal("Failed to append CA certificate") + } + + opts := mqtt.NewClientOptions() + opts.AddBroker(fmt.Sprintf("tls://%s", gwAddr)) + opts.SetTLSConfig(&tls.Config{ + RootCAs: certpool, + ServerName: serverStr, + MinVersion: tls.VersionTLS13, + }) + opts.SetConnectRetry(true) + + var wg sync.WaitGroup + wg.Add(1) + + topic := "test/tlsroute-terminate" + opts.OnConnect = func(c mqtt.Client) { + t.Log("Connected to MQTT broker") + + if token := c.Subscribe(topic, 0, func(_ mqtt.Client, msg mqtt.Message) { + t.Logf("Received message: %s\n", string(msg.Payload())) + wg.Done() + }); token.Wait() && token.Error() != nil { + t.Fatalf("Failed to subscribe: %v", token.Error()) + } + + t.Log("Subscribed, publishing test message...") + if token := c.Publish(topic, 0, false, "Hello TLSRoute Terminate MQTT!"); token.Wait() && token.Error() != nil { + t.Fatalf("Failed to publish: %v", token.Error()) + } + } + + client := mqtt.NewClient(opts) + if token := client.Connect(); token.Wait() && token.Error() != nil { + t.Fatalf("Connection failed: %v", token.Error()) + } + + waitCh := make(chan struct{}) + go func() { + wg.Wait() + close(waitCh) + }() + + select { + case <-waitCh: + t.Log("Round-trip test succeeded") + case <-time.After(5 * time.Second): + t.Fatal("Timed out waiting for message") + } + }) + }, +} diff --git a/conformance/tests/tlsroute-terminate-simple-same-namespace.yaml b/conformance/tests/tlsroute-terminate-simple-same-namespace.yaml new file mode 100644 index 0000000000..00b8e6fee1 --- /dev/null +++ b/conformance/tests/tlsroute-terminate-simple-same-namespace.yaml @@ -0,0 +1,95 @@ +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: TLSRoute +metadata: + name: gateway-conformance-mqtt-test + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: gateway-tlsroute-terminate + namespace: gateway-conformance-infra + hostnames: + - tls.example.com + rules: + - backendRefs: + - name: mqtt-backend + port: 1883 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway-tlsroute-terminate + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: mqtt + port: 8883 + protocol: TLS + hostname: tls.example.com + allowedRoutes: + namespaces: + from: Same + kinds: + - kind: TLSRoute + tls: + mode: Terminate + certificateRefs: + - name: tls-terminate-checks-certificate +--- +apiVersion: v1 +kind: Service +metadata: + name: mqtt-backend + namespace: gateway-conformance-infra +spec: + selector: + app: mqtt-backend + ports: + - protocol: TCP + port: 1883 + targetPort: 1883 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mqtt-backend + namespace: gateway-conformance-infra + labels: + app: mqtt-backend +spec: + replicas: 1 + selector: + matchLabels: + app: mqtt-backend + template: + metadata: + labels: + app: mqtt-backend + spec: + containers: + - name: mqtt-backend + # https://hub.docker.com/_/eclipse-mosquitto + image: eclipse-mosquitto:2 + volumeMounts: + - name: config + mountPath: /mosquitto/config/mosquitto.conf + subPath: mosquitto.conf + ports: + - containerPort: 1883 + resources: + requests: + cpu: 10m + volumes: + - name: config + configMap: + name: mosquitto-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: mosquitto-config + namespace: gateway-conformance-infra +data: + mosquitto.conf: | + listener 1883 + allow_anonymous true diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go index d32899e20e..f395bb06bf 100644 --- a/conformance/utils/kubernetes/helpers.go +++ b/conformance/utils/kubernetes/helpers.go @@ -1037,3 +1037,17 @@ func BackendTLSPolicyMustHaveLatestConditions(t *testing.T, r *gatewayv1.Backend } } } + +// GetConfigMapData fetches the named ConfigMap +func GetConfigMapData(client client.Client, name types.NamespacedName) (map[string]string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + configMap := &v1.ConfigMap{} + err := client.Get(ctx, name, configMap) + if err != nil { + return nil, fmt.Errorf("error fetching ConfigMap: %w", err) + } + + return configMap.Data, nil +} diff --git a/conformance/utils/suite/profiles.go b/conformance/utils/suite/profiles.go index 996fe346dd..f7e21f7f6c 100644 --- a/conformance/utils/suite/profiles.go +++ b/conformance/utils/suite/profiles.go @@ -93,7 +93,10 @@ var ( features.SupportReferenceGrant, features.SupportTLSRoute, ), - ExtendedFeatures: features.SetsToNamesSet(features.GatewayExtendedFeatures), + ExtendedFeatures: features.SetsToNamesSet( + features.GatewayExtendedFeatures, + features.TLSRouteExtendedFeatures, + ), } // GatewayGRPCConformanceProfile is a ConformanceProfile that covers testing GRPC diff --git a/conformance/utils/suite/suite.go b/conformance/utils/suite/suite.go index 5df2c722d9..dd3a3cd87c 100644 --- a/conformance/utils/suite/suite.go +++ b/conformance/utils/suite/suite.go @@ -384,6 +384,10 @@ func (suite *ConformanceTestSuite) Setup(t *testing.T, tests []ConformanceTest) secret = kubernetes.MustCreateCASignedCertSecret(t, "gateway-conformance-infra", "tls-checks-certificate", []string{"abc.example.com", "spiffe://abc.example.com/test-identity", "other.example.com"}, ca, caPrivKey) suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup) + // The following secret is used for TLSRoute mode Terminate validation + secret = kubernetes.MustCreateCASignedCertSecret(t, "gateway-conformance-infra", "tls-terminate-checks-certificate", []string{"tls.example.com"}, ca, caPrivKey) + suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup) + // The following CA ceritficate is used for BackendTLSPolicy testing to intentionally force TLS validation to fail. caConfigMap, _, _ = kubernetes.MustCreateCACertConfigMap(t, "gateway-conformance-infra", "mismatch-ca-certificate") suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{caConfigMap}, suite.Cleanup) diff --git a/go.mod b/go.mod index 0536504b04..1808f801f7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module sigs.k8s.io/gateway-api go 1.24.0 require ( + github.com/eclipse/paho.mqtt.golang v1.5.1 github.com/miekg/dns v1.1.68 github.com/stretchr/testify v1.11.1 golang.org/x/net v0.46.0 diff --git a/go.sum b/go.sum index d4c3b81a6c..0bff497b03 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= +github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= diff --git a/pkg/features/features.go b/pkg/features/features.go index 1fbf43f0e6..52e58cc8c2 100644 --- a/pkg/features/features.go +++ b/pkg/features/features.go @@ -58,6 +58,7 @@ var ( Insert(HTTPRouteCoreFeatures.UnsortedList()...). Insert(HTTPRouteExtendedFeatures.UnsortedList()...). Insert(TLSRouteCoreFeatures.UnsortedList()...). + Insert(TLSRouteExtendedFeatures.UnsortedList()...). Insert(MeshCoreFeatures.UnsortedList()...). Insert(MeshExtendedFeatures.UnsortedList()...). Insert(GRPCRouteCoreFeatures.UnsortedList()...). diff --git a/pkg/features/tlsroute.go b/pkg/features/tlsroute.go index 90d68c3d34..b0dd499fba 100644 --- a/pkg/features/tlsroute.go +++ b/pkg/features/tlsroute.go @@ -25,16 +25,33 @@ import "k8s.io/apimachinery/pkg/util/sets" const ( // This option indicates support for TLSRoute SupportTLSRoute FeatureName = "TLSRoute" + + // This option indicates support for TLSRoute mode Terminate (extended conformance) + SupportTLSRouteModeTerminate FeatureName = "TLSRouteModeTerminate" ) -// TLSRouteFeature contains metadata for the TLSRoute feature. -var TLSRouteFeature = Feature{ - Name: SupportTLSRoute, - Channel: FeatureChannelExperimental, -} +var ( + // TLSRouteFeature contains metadata for the TLSRoute feature. + TLSRouteFeature = Feature{ + Name: SupportTLSRoute, + Channel: FeatureChannelExperimental, + } + // TLSRouteModeTerminate contains metadata for the TLSRouteModeTerminate feature. + TLSRouteModeTerminateFeature = Feature{ + Name: SupportTLSRouteModeTerminate, + Channel: FeatureChannelExperimental, + } +) // TLSCoreFeatures includes all the supported features for the TLSRoute API at // a Core level of support. var TLSRouteCoreFeatures = sets.New( TLSRouteFeature, ) + +// TLSRouteExtendedFeatures includes all extended features for TLSRoute +// conformance and can be used to opt-in to run all TLSRoute extended features tests. +// This does not include any Core Features. +var TLSRouteExtendedFeatures = sets.New( + TLSRouteModeTerminateFeature, +)