This is an automated email from the ASF dual-hosted git repository.

AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new a22afa2e feat: allow L4 upstream schemes (tcp/tls/udp) for stream 
backends (#2830)
a22afa2e is described below

commit a22afa2edd5504e9970f26d5898a825a09108a57
Author: AlinsRan <[email protected]>
AuthorDate: Mon Aug 3 08:45:42 2026 +0800

    feat: allow L4 upstream schemes (tcp/tls/udp) for stream backends (#2830)
---
 api/v1alpha1/backendtrafficpolicy_types.go         |   7 +-
 api/v2/apisixupstream_types.go                     |   7 +-
 .../bases/apisix.apache.org_apisixupstreams.yaml   |  16 +++-
 .../apisix.apache.org_backendtrafficpolicies.yaml  |   8 +-
 docs/en/latest/reference/api-reference.md          |   8 +-
 internal/adc/translator/apisixroute_test.go        | 102 +++++++++++++++++++++
 internal/adc/translator/l4route_test.go            |  88 ++++++++++++++++++
 internal/adc/translator/tcproute.go                |   1 -
 test/e2e/crds/v2/streamroute.go                    |  57 ++++++++++++
 test/e2e/gatewayapi/tcproute.go                    |  87 ++++++++++++++++++
 10 files changed, 369 insertions(+), 12 deletions(-)

diff --git a/api/v1alpha1/backendtrafficpolicy_types.go 
b/api/v1alpha1/backendtrafficpolicy_types.go
index 826ec854..b3dc24c3 100644
--- a/api/v1alpha1/backendtrafficpolicy_types.go
+++ b/api/v1alpha1/backendtrafficpolicy_types.go
@@ -47,8 +47,11 @@ type BackendTrafficPolicySpec struct {
        LoadBalancer *LoadBalancer `json:"loadbalancer,omitempty" 
yaml:"loadbalancer,omitempty"`
        // Scheme is the protocol used to communicate with the upstream.
        // Default is `http`.
-       // Can be `http`, `https`, `grpc`, or `grpcs`.
-       // +kubebuilder:validation:Enum=http;https;grpc;grpcs;
+       // For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`.
+       // For L4 proxy, it can be `tcp`, `tls`, or `udp`.
+       // The L4 values apply to stream routes only; using them for an HTTP 
route
+       // makes the upstream unreachable.
+       // +kubebuilder:validation:Enum=http;https;grpc;grpcs;tcp;tls;udp;
        // +kubebuilder:default=http
        Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"`
 
diff --git a/api/v2/apisixupstream_types.go b/api/v2/apisixupstream_types.go
index a9009178..1de75f24 100644
--- a/api/v2/apisixupstream_types.go
+++ b/api/v2/apisixupstream_types.go
@@ -106,9 +106,12 @@ type ApisixUpstreamConfig struct {
 
        // Scheme is the protocol used to communicate with the upstream.
        // Default is `http`.
-       // Can be `http`, `https`, `grpc`, or `grpcs`.
+       // For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`.
+       // For L4 proxy, it can be `tcp`, `tls`, or `udp`.
+       // The L4 values apply to stream routes only; using them for an HTTP 
route
+       // makes the upstream unreachable.
        // +kubebuilder:validation:Optional
-       // +kubebuilder:validation:Enum=http;https;grpc;grpcs;
+       // +kubebuilder:validation:Enum=http;https;grpc;grpcs;tcp;tls;udp;
        Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"`
 
        // Retries defines the number of retry attempts APISIX should make when 
a failure occurs.
diff --git a/config/crd/bases/apisix.apache.org_apisixupstreams.yaml 
b/config/crd/bases/apisix.apache.org_apisixupstreams.yaml
index 0a7ba978..8ac4c125 100644
--- a/config/crd/bases/apisix.apache.org_apisixupstreams.yaml
+++ b/config/crd/bases/apisix.apache.org_apisixupstreams.yaml
@@ -591,12 +591,18 @@ spec:
                       description: |-
                         Scheme is the protocol used to communicate with the 
upstream.
                         Default is `http`.
-                        Can be `http`, `https`, `grpc`, or `grpcs`.
+                        For L7 proxy, it can be `http`, `https`, `grpc`, or 
`grpcs`.
+                        For L4 proxy, it can be `tcp`, `tls`, or `udp`.
+                        The L4 values apply to stream routes only; using them 
for an HTTP route
+                        makes the upstream unreachable.
                       enum:
                       - http
                       - https
                       - grpc
                       - grpcs
+                      - tcp
+                      - tls
+                      - udp
                       type: string
                     subsets:
                       description: |-
@@ -670,12 +676,18 @@ spec:
                 description: |-
                   Scheme is the protocol used to communicate with the upstream.
                   Default is `http`.
-                  Can be `http`, `https`, `grpc`, or `grpcs`.
+                  For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`.
+                  For L4 proxy, it can be `tcp`, `tls`, or `udp`.
+                  The L4 values apply to stream routes only; using them for an 
HTTP route
+                  makes the upstream unreachable.
                 enum:
                 - http
                 - https
                 - grpc
                 - grpcs
+                - tcp
+                - tls
+                - udp
                 type: string
               subsets:
                 description: |-
diff --git a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml 
b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
index 4902cbb2..3b070a78 100644
--- a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
+++ b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
@@ -284,12 +284,18 @@ spec:
                 description: |-
                   Scheme is the protocol used to communicate with the upstream.
                   Default is `http`.
-                  Can be `http`, `https`, `grpc`, or `grpcs`.
+                  For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`.
+                  For L4 proxy, it can be `tcp`, `tls`, or `udp`.
+                  The L4 values apply to stream routes only; using them for an 
HTTP route
+                  makes the upstream unreachable.
                 enum:
                 - http
                 - https
                 - grpc
                 - grpcs
+                - tcp
+                - tls
+                - udp
                 type: string
               targetRefs:
                 description: |-
diff --git a/docs/en/latest/reference/api-reference.md 
b/docs/en/latest/reference/api-reference.md
index 5ccedfde..347ba08e 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -254,7 +254,7 @@ _Appears in:_
 | --- | --- |
 | `targetRefs` 
_[BackendPolicyTargetReferenceWithSectionName](#backendpolicytargetreferencewithsectionname)
 array_ | TargetRef identifies an API object to apply policy to. Currently, 
Backends (i.e. Service, ServiceImport, or any implementation-specific 
backendRef) are the only valid API target references. |
 | `loadbalancer` _[LoadBalancer](#loadbalancer)_ | LoadBalancer represents the 
load balancer configuration for Kubernetes Service. The default strategy is 
round robin. |
-| `scheme` _string_ | Scheme is the protocol used to communicate with the 
upstream. Default is `http`. Can be `http`, `https`, `grpc`, or `grpcs`. |
+| `scheme` _string_ | Scheme is the protocol used to communicate with the 
upstream. Default is `http`. For L7 proxy, it can be `http`, `https`, `grpc`, 
or `grpcs`. For L4 proxy, it can be `tcp`, `tls`, or `udp`. The L4 values apply 
to stream routes only; using them for an HTTP route makes the upstream 
unreachable. |
 | `retries` _integer_ | Retries specify the number of times the gateway should 
retry sending requests when errors such as timeouts or 502 errors occur. |
 | `timeout` _[Timeout](#timeout)_ | Timeout sets the read, send, and connect 
timeouts to the upstream. |
 | `passHost` _string_ | PassHost configures how the host header should be 
determined when a request is forwarded to the upstream. Default is `pass`. Can 
be `pass`, `node` or `rewrite`:<br /> • `pass`: preserve the original Host 
header<br /> • `node`: use the upstream node’s host<br /> • `rewrite`: set to a 
custom host via `upstreamHost` |
@@ -1489,7 +1489,7 @@ ApisixUpstreamConfig defines configuration for upstream 
services.
 | Field | Description |
 | --- | --- |
 | `loadbalancer` _[LoadBalancer](#loadbalancer)_ | LoadBalancer specifies the 
load balancer configuration for Kubernetes Service. |
-| `scheme` _string_ | Scheme is the protocol used to communicate with the 
upstream. Default is `http`. Can be `http`, `https`, `grpc`, or `grpcs`. |
+| `scheme` _string_ | Scheme is the protocol used to communicate with the 
upstream. Default is `http`. For L7 proxy, it can be `http`, `https`, `grpc`, 
or `grpcs`. For L4 proxy, it can be `tcp`, `tls`, or `udp`. The L4 values apply 
to stream routes only; using them for an HTTP route makes the upstream 
unreachable. |
 | `retries` _integer_ | Retries defines the number of retry attempts APISIX 
should make when a failure occurs. Failures include timeouts, network errors, 
or 5xx status codes. |
 | `timeout` _[UpstreamTimeout](#upstreamtimeout)_ | Timeout specifies the 
connection, send, and read timeouts for upstream requests. |
 | `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines the active 
and passive health check configuration for the upstream. |
@@ -1549,7 +1549,7 @@ definitions and custom configuration.
 | `ingressClassName` _string_ | IngressClassName is the name of an 
IngressClass cluster resource. Controller implementations use this field to 
determine whether they should process this ApisixUpstream resource. |
 | `externalNodes` _[ApisixUpstreamExternalNode](#apisixupstreamexternalnode) 
array_ | ExternalNodes defines a static list of backend nodes. These can be 
external hosts outside the cluster or cluster-internal Services specified by 
their DNS name. When this field is set, the upstream will route traffic 
directly to these nodes without DNS resolution or service discovery. |
 | `loadbalancer` _[LoadBalancer](#loadbalancer)_ | LoadBalancer specifies the 
load balancer configuration for Kubernetes Service. |
-| `scheme` _string_ | Scheme is the protocol used to communicate with the 
upstream. Default is `http`. Can be `http`, `https`, `grpc`, or `grpcs`. |
+| `scheme` _string_ | Scheme is the protocol used to communicate with the 
upstream. Default is `http`. For L7 proxy, it can be `http`, `https`, `grpc`, 
or `grpcs`. For L4 proxy, it can be `tcp`, `tls`, or `udp`. The L4 values apply 
to stream routes only; using them for an HTTP route makes the upstream 
unreachable. |
 | `retries` _integer_ | Retries defines the number of retry attempts APISIX 
should make when a failure occurs. Failures include timeouts, network errors, 
or 5xx status codes. |
 | `timeout` _[UpstreamTimeout](#upstreamtimeout)_ | Timeout specifies the 
connection, send, and read timeouts for upstream requests. |
 | `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines the active 
and passive health check configuration for the upstream. |
@@ -1719,7 +1719,7 @@ them if they are set on the port level.
 | Field | Description |
 | --- | --- |
 | `loadbalancer` _[LoadBalancer](#loadbalancer)_ | LoadBalancer specifies the 
load balancer configuration for Kubernetes Service. |
-| `scheme` _string_ | Scheme is the protocol used to communicate with the 
upstream. Default is `http`. Can be `http`, `https`, `grpc`, or `grpcs`. |
+| `scheme` _string_ | Scheme is the protocol used to communicate with the 
upstream. Default is `http`. For L7 proxy, it can be `http`, `https`, `grpc`, 
or `grpcs`. For L4 proxy, it can be `tcp`, `tls`, or `udp`. The L4 values apply 
to stream routes only; using them for an HTTP route makes the upstream 
unreachable. |
 | `retries` _integer_ | Retries defines the number of retry attempts APISIX 
should make when a failure occurs. Failures include timeouts, network errors, 
or 5xx status codes. |
 | `timeout` _[UpstreamTimeout](#upstreamtimeout)_ | Timeout specifies the 
connection, send, and read timeouts for upstream requests. |
 | `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines the active 
and passive health check configuration for the upstream. |
diff --git a/internal/adc/translator/apisixroute_test.go 
b/internal/adc/translator/apisixroute_test.go
index df4c9c07..1a900d3d 100644
--- a/internal/adc/translator/apisixroute_test.go
+++ b/internal/adc/translator/apisixroute_test.go
@@ -18,14 +18,22 @@
 package translator
 
 import (
+       "context"
        "testing"
 
        "github.com/go-logr/logr"
        "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       corev1 "k8s.io/api/core/v1"
+       discoveryv1 "k8s.io/api/discovery/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       k8stypes "k8s.io/apimachinery/pkg/types"
+       "k8s.io/apimachinery/pkg/util/intstr"
+       "k8s.io/utils/ptr"
 
        adc "github.com/apache/apisix-ingress-controller/api/adc"
        apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
 )
 
 func TestBuildRoute_HostsNotSet(t *testing.T) {
@@ -121,3 +129,97 @@ func 
TestBuildRoute_MetadataLabelsDoNotOverwriteControllerLabels(t *testing.T) {
        assert.Equal(t, ar.Namespace, route.Labels["k8s/namespace"])
        assert.Equal(t, "apisix-ingress-controller", route.Labels["manager-by"])
 }
+
+func TestTranslateApisixRouteStreamUpstreamScheme(t *testing.T) {
+       const (
+               namespace   = "default"
+               serviceName = "backend"
+               portName    = "tcp"
+               portNumber  = int32(6000)
+       )
+
+       tests := []struct {
+               scheme   string
+               protocol string
+       }{
+               {scheme: apiv2.SchemeTLS, protocol: "TCP"},
+               {scheme: apiv2.SchemeTCP, protocol: "TCP"},
+               {scheme: apiv2.SchemeUDP, protocol: "UDP"},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.scheme, func(t *testing.T) {
+                       translator := NewTranslator(logr.Discard(), "")
+                       tctx := 
provider.NewDefaultTranslateContext(context.Background())
+
+                       serviceKey := k8stypes.NamespacedName{Namespace: 
namespace, Name: serviceName}
+                       tctx.Services[serviceKey] = &corev1.Service{
+                               ObjectMeta: metav1.ObjectMeta{
+                                       Name:      serviceName,
+                                       Namespace: namespace,
+                               },
+                               Spec: corev1.ServiceSpec{
+                                       Ports: []corev1.ServicePort{{
+                                               Name: portName,
+                                               Port: portNumber,
+                                       }},
+                               },
+                       }
+                       tctx.EndpointSlices[serviceKey] = 
[]discoveryv1.EndpointSlice{{
+                               ObjectMeta: metav1.ObjectMeta{
+                                       Name:      serviceName + "-1",
+                                       Namespace: namespace,
+                               },
+                               Ports: []discoveryv1.EndpointPort{{
+                                       Name: ptr.To(portName),
+                                       Port: ptr.To(portNumber),
+                               }},
+                               Endpoints: []discoveryv1.Endpoint{{
+                                       Addresses: []string{"10.0.0.1"},
+                                       Conditions: 
discoveryv1.EndpointConditions{
+                                               Ready: ptr.To(true),
+                                       },
+                               }},
+                       }}
+                       tctx.Upstreams[serviceKey] = &apiv2.ApisixUpstream{
+                               ObjectMeta: metav1.ObjectMeta{
+                                       Name:      serviceName,
+                                       Namespace: namespace,
+                               },
+                               Spec: apiv2.ApisixUpstreamSpec{
+                                       ApisixUpstreamConfig: 
apiv2.ApisixUpstreamConfig{
+                                               Scheme: tt.scheme,
+                                       },
+                               },
+                       }
+
+                       ar := &apiv2.ApisixRoute{
+                               ObjectMeta: metav1.ObjectMeta{
+                                       Name:      "test-route",
+                                       Namespace: namespace,
+                               },
+                               Spec: apiv2.ApisixRouteSpec{
+                                       Stream: []apiv2.ApisixRouteStream{{
+                                               Name:     "rule1",
+                                               Protocol: tt.protocol,
+                                               Match: 
apiv2.ApisixRouteStreamMatch{
+                                                       IngressPort: 8000,
+                                               },
+                                               Backend: 
apiv2.ApisixRouteStreamBackend{
+                                                       ServiceName: 
serviceName,
+                                                       ServicePort: 
intstr.FromInt32(portNumber),
+                                               },
+                                       }},
+                               },
+                       }
+
+                       result, err := translator.TranslateApisixRoute(tctx, ar)
+                       require.NoError(t, err)
+                       require.Len(t, result.Services, 1)
+                       require.NotNil(t, result.Services[0].Upstream)
+
+                       assert.Equal(t, tt.scheme, 
result.Services[0].Upstream.Scheme)
+                       assert.Equal(t, "10.0.0.1", 
result.Services[0].Upstream.Nodes[0].Host)
+               })
+       }
+}
diff --git a/internal/adc/translator/l4route_test.go 
b/internal/adc/translator/l4route_test.go
index fdf7a87d..c7901fe0 100644
--- a/internal/adc/translator/l4route_test.go
+++ b/internal/adc/translator/l4route_test.go
@@ -24,12 +24,18 @@ import (
        "github.com/go-logr/logr"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
+       corev1 "k8s.io/api/core/v1"
+       discoveryv1 "k8s.io/api/discovery/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        k8stypes "k8s.io/apimachinery/pkg/types"
+       "k8s.io/utils/ptr"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
        gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
 
        "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
        "github.com/apache/apisix-ingress-controller/internal/provider"
+       internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
 )
 
 func TestTranslateTCPRouteWithL4RoutePolicy(t *testing.T) {
@@ -267,3 +273,85 @@ func TestTranslateTLSRouteWithL4RoutePolicy(t *testing.T) {
                })
        }
 }
+
+func TestTranslateTCPRouteUpstreamScheme(t *testing.T) {
+       const (
+               namespace   = "default"
+               serviceName = "backend"
+               portName    = "tcp"
+               portNumber  = int32(6000)
+       )
+
+       translator := NewTranslator(logr.Discard(), "")
+       tctx := provider.NewDefaultTranslateContext(context.Background())
+
+       serviceKey := k8stypes.NamespacedName{Namespace: namespace, Name: 
serviceName}
+       tctx.Services[serviceKey] = &corev1.Service{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      serviceName,
+                       Namespace: namespace,
+               },
+               Spec: corev1.ServiceSpec{
+                       Ports: []corev1.ServicePort{{
+                               Name: portName,
+                               Port: portNumber,
+                       }},
+               },
+       }
+       tctx.EndpointSlices[serviceKey] = []discoveryv1.EndpointSlice{{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      serviceName + "-1",
+                       Namespace: namespace,
+               },
+               Ports: []discoveryv1.EndpointPort{{
+                       Name: ptr.To(portName),
+                       Port: ptr.To(portNumber),
+               }},
+               Endpoints: []discoveryv1.Endpoint{{
+                       Addresses: []string{"10.0.0.1"},
+                       Conditions: discoveryv1.EndpointConditions{
+                               Ready: ptr.To(true),
+                       },
+               }},
+       }}
+       tctx.BackendTrafficPolicies[serviceKey] = 
&v1alpha1.BackendTrafficPolicy{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "backend-policy",
+                       Namespace: namespace,
+               },
+               Spec: v1alpha1.BackendTrafficPolicySpec{
+                       TargetRefs: 
[]v1alpha1.BackendPolicyTargetReferenceWithSectionName{{
+                               LocalPolicyTargetReference: 
gatewayv1.LocalPolicyTargetReference{
+                                       Name: gatewayv1.ObjectName(serviceName),
+                                       Kind: 
gatewayv1.Kind(internaltypes.KindService),
+                               },
+                       }},
+                       Scheme: apiv2.SchemeTLS,
+               },
+       }
+
+       route := &gatewayv1alpha2.TCPRoute{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "my-tcp",
+                       Namespace: namespace,
+               },
+               Spec: gatewayv1alpha2.TCPRouteSpec{
+                       Rules: []gatewayv1alpha2.TCPRouteRule{{
+                               BackendRefs: []gatewayv1alpha2.BackendRef{{
+                                       BackendObjectReference: 
gatewayv1.BackendObjectReference{
+                                               Name: 
gatewayv1.ObjectName(serviceName),
+                                               Port: ptr.To(portNumber),
+                                       },
+                               }},
+                       }},
+               },
+       }
+
+       result, err := translator.TranslateTCPRoute(tctx, route)
+       require.NoError(t, err)
+       require.Len(t, result.Services, 1)
+       require.NotNil(t, result.Services[0].Upstream)
+
+       assert.Equal(t, apiv2.SchemeTLS, result.Services[0].Upstream.Scheme)
+       assert.Equal(t, "10.0.0.1", result.Services[0].Upstream.Nodes[0].Host)
+}
diff --git a/internal/adc/translator/tcproute.go 
b/internal/adc/translator/tcproute.go
index 81e12a34..7327e911 100644
--- a/internal/adc/translator/tcproute.go
+++ b/internal/adc/translator/tcproute.go
@@ -68,7 +68,6 @@ func (t *Translator) TranslateTCPRoute(tctx 
*provider.TranslateContext, tcpRoute
                        if len(upNodes) == 0 {
                                continue
                        }
-                       // TODO: Confirm BackendTrafficPolicy attachment with 
e2e test case.
                        t.AttachBackendTrafficPolicyToUpstream(backend, 
tctx.BackendTrafficPolicies, upstream, tctx.Services)
                        upstream.Nodes = upNodes
                        var (
diff --git a/test/e2e/crds/v2/streamroute.go b/test/e2e/crds/v2/streamroute.go
index 8f407802..b27455b0 100644
--- a/test/e2e/crds/v2/streamroute.go
+++ b/test/e2e/crds/v2/streamroute.go
@@ -25,7 +25,9 @@ import (
        mqtt "github.com/eclipse/paho.mqtt.golang"
        . "github.com/onsi/ginkgo/v2"
        . "github.com/onsi/gomega"
+       "k8s.io/utils/ptr"
 
+       "github.com/apache/apisix-ingress-controller/test/e2e/framework"
        "github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
 )
 
@@ -102,6 +104,61 @@ spec:
                })
        })
 
+       Context("TCP Proxy with TLS upstream", func() {
+               apisixUpstream := `
+apiVersion: apisix.apache.org/v2
+kind: ApisixUpstream
+metadata:
+  name: nginx
+spec:
+  ingressClassName: %s
+  scheme: tls
+`
+               apisixRoute := `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: nginx-tcp-route
+spec:
+  ingressClassName: %s
+  stream:
+  - name: rule1
+    protocol: TCP
+    match:
+      ingressPort: 9100
+    backend:
+      serviceName: nginx
+      servicePort: 443
+`
+               BeforeEach(func() {
+                       s.DeployNginx(framework.NginxOptions{
+                               Namespace: s.Namespace(),
+                               Replicas:  ptr.To(int32(1)),
+                       })
+               })
+
+               It("stream tcp proxy to tls upstream", func() {
+                       // The client speaks plain TCP to APISIX; scheme: tls 
makes APISIX
+                       // establish the TLS session with the upstream on its 
behalf.
+                       err := 
s.CreateResourceFromString(fmt.Sprintf(apisixUpstream, s.Namespace()))
+                       Expect(err).NotTo(HaveOccurred(), "creating 
ApisixUpstream")
+
+                       err = 
s.CreateResourceFromString(fmt.Sprintf(apisixRoute, s.Namespace()))
+                       Expect(err).NotTo(HaveOccurred(), "creating 
ApisixRoute")
+
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Client: s.NewAPISIXClientWithTCPProxy(),
+                               Method: "GET",
+                               Path:   "/",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       scaffold.WithExpectedStatus(200),
+                                       scaffold.WithExpectedHeader("X-Port", 
"443"),
+                                       
scaffold.WithExpectedBodyContains("Hello, World!"),
+                               },
+                       })
+               })
+       })
+
        Context("UDP Proxy", func() {
                apisixRoute := `
 apiVersion: apisix.apache.org/v2
diff --git a/test/e2e/gatewayapi/tcproute.go b/test/e2e/gatewayapi/tcproute.go
index abcfa5bb..a85e22f7 100644
--- a/test/e2e/gatewayapi/tcproute.go
+++ b/test/e2e/gatewayapi/tcproute.go
@@ -25,8 +25,10 @@ import (
        . "github.com/onsi/gomega"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/types"
+       "k8s.io/utils/ptr"
        gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 
+       "github.com/apache/apisix-ingress-controller/test/e2e/framework"
        "github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
 )
 
@@ -112,6 +114,91 @@ spec:
                })
        })
 
+       Context("TCPRoute With BackendTrafficPolicy", func() {
+               var tcpGateway = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+  - name: tcp
+    protocol: TCP
+    port: 80
+    allowedRoutes:
+      kinds:
+      - kind: TCPRoute
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`
+
+               var tcpRoute = `
+apiVersion: gateway.networking.k8s.io/v1alpha2
+kind: TCPRoute
+metadata:
+  name: tcp-tls-upstream
+spec:
+  parentRefs:
+  - name: %s
+    sectionName: tcp
+  rules:
+  - backendRefs:
+    - name: nginx
+      port: 443
+`
+
+               var backendTrafficPolicy = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: nginx-tls
+spec:
+  targetRefs:
+  - name: nginx
+    kind: Service
+    group: ""
+  scheme: tls
+`
+
+               BeforeEach(func() {
+                       
Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(),
 "creating GatewayProxy")
+                       
Expect(s.CreateResourceFromString(s.GetGatewayClassYaml())).NotTo(HaveOccurred(),
 "creating GatewayClass")
+                       
Expect(s.CreateResourceFromString(fmt.Sprintf(tcpGateway, s.Namespace(), 
s.Namespace()))).
+                               NotTo(HaveOccurred(), "creating Gateway")
+                       s.DeployNginx(framework.NginxOptions{
+                               Namespace: s.Namespace(),
+                               Replicas:  ptr.To(int32(1)),
+                       })
+               })
+
+               It("BackendTrafficPolicy scheme tls connects to the upstream 
over TLS", func() {
+                       By("creating BackendTrafficPolicy with scheme: tls")
+                       
Expect(s.CreateResourceFromString(backendTrafficPolicy)).NotTo(HaveOccurred(), 
"creating BackendTrafficPolicy")
+
+                       By("creating TCPRoute to the TLS port of nginx")
+                       s.ResourceApplied("TCPRoute", "tcp-tls-upstream", 
fmt.Sprintf(tcpRoute, s.Namespace()), 1)
+
+                       // The client speaks plain HTTP over TCP; without 
scheme: tls nginx would
+                       // reject the request on its TLS port instead of 
answering with 200.
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Client: s.NewAPISIXClientOnTCPPort(),
+                               Method: "GET",
+                               Path:   "/",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       scaffold.WithExpectedStatus(200),
+                                       scaffold.WithExpectedHeader("X-Port", 
"443"),
+                                       
scaffold.WithExpectedBodyContains("Hello, World!"),
+                               },
+                               Timeout:  time.Minute * 3,
+                               Interval: time.Second * 2,
+                       })
+               })
+       })
+
        Context("TCPRoute With L4RoutePolicy", func() {
                var tcpGateway = `
 apiVersion: gateway.networking.k8s.io/v1

Reply via email to