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 f28b3429 feat: support resolve svc.ports[].appProtocol (#2601)
f28b3429 is described below

commit f28b34292be45587184d9faa8cb5bfc477d5d1b8
Author: AlinsRan <[email protected]>
AuthorDate: Thu Oct 16 15:37:01 2025 +0800

    feat: support resolve svc.ports[].appProtocol (#2601)
---
 Makefile                                |   2 +-
 api/v2/apisixroute_types.go             |   2 +-
 api/v2/zz_generated.deepcopy.go         |   5 +
 internal/adc/translator/apisixroute.go  |  88 +++++++---
 internal/adc/translator/gatewayproxy.go |   2 +-
 internal/adc/translator/grpcroute.go    |   2 +-
 internal/adc/translator/httproute.go    |  43 +++--
 internal/adc/translator/ingress.go      | 284 +++++++++++++++++++-------------
 internal/adc/translator/tcproute.go     |   2 +-
 internal/adc/translator/tlsroute.go     |   2 +-
 internal/adc/translator/udproute.go     |   2 +-
 internal/types/k8s.go                   |   7 +
 test/e2e/crds/v2/route.go               | 144 ++++++++++++++++
 test/e2e/framework/manifests/nginx.yaml |  48 +++++-
 test/e2e/gatewayapi/httproute.go        | 103 ++++++++++++
 test/e2e/ingress/ingress.go             | 127 ++++++++++++++
 16 files changed, 699 insertions(+), 164 deletions(-)

diff --git a/Makefile b/Makefile
index 74207326..9d237283 100644
--- a/Makefile
+++ b/Makefile
@@ -52,7 +52,7 @@ GO_LDFLAGS ?= "-X=$(VERSYM)=$(VERSION) 
-X=$(GITSHASYM)=$(GITSHA) -X=$(BUILDOSSYM
 # gateway-api
 GATEAY_API_VERSION ?= v1.3.0
 ## 
https://github.com/kubernetes-sigs/gateway-api/blob/v1.3.0/pkg/features/httproute.go
-SUPPORTED_EXTENDED_FEATURES = 
"HTTPRouteDestinationPortMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteRequestMirror,HTTPRouteSchemeRedirect,GatewayAddressEmpty,HTTPRouteResponseHeaderModification,GatewayPort8080,HTTPRouteHostRewrite,HTTPRouteQueryParamMatching,HTTPRoutePathRewrite"
+SUPPORTED_EXTENDED_FEATURES = 
"HTTPRouteDestinationPortMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteRequestMirror,HTTPRouteSchemeRedirect,GatewayAddressEmpty,HTTPRouteResponseHeaderModification,GatewayPort8080,HTTPRouteHostRewrite,HTTPRouteQueryParamMatching,HTTPRoutePathRewrite,HTTPRouteBackendProtocolWebSocket"
 CONFORMANCE_TEST_REPORT_OUTPUT ?= 
$(DIR)/apisix-ingress-controller-conformance-report.yaml
 ## 
https://github.com/kubernetes-sigs/gateway-api/blob/v1.3.0/conformance/utils/suite/profiles.go
 CONFORMANCE_PROFILES ?= GATEWAY-HTTP,GATEWAY-GRPC,GATEWAY-TLS
diff --git a/api/v2/apisixroute_types.go b/api/v2/apisixroute_types.go
index 376a3907..9c7bbe86 100644
--- a/api/v2/apisixroute_types.go
+++ b/api/v2/apisixroute_types.go
@@ -100,7 +100,7 @@ type ApisixRouteHTTP struct {
 
        // Websocket enables or disables websocket support for this route.
        // +kubebuilder:validation:Optional
-       Websocket bool `json:"websocket" yaml:"websocket"`
+       Websocket *bool `json:"websocket" yaml:"websocket"`
        // PluginConfigName specifies the name of the plugin config to apply.
        PluginConfigName string `json:"plugin_config_name,omitempty" 
yaml:"plugin_config_name,omitempty"`
        // PluginConfigNamespace specifies the namespace of the plugin config.
diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go
index de92bb0e..73be4b23 100644
--- a/api/v2/zz_generated.deepcopy.go
+++ b/api/v2/zz_generated.deepcopy.go
@@ -755,6 +755,11 @@ func (in *ApisixRouteHTTP) DeepCopyInto(out 
*ApisixRouteHTTP) {
                        (*in)[i].DeepCopyInto(&(*out)[i])
                }
        }
+       if in.Websocket != nil {
+               in, out := &in.Websocket, &out.Websocket
+               *out = new(bool)
+               **out = **in
+       }
        if in.Plugins != nil {
                in, out := &in.Plugins, &out.Plugins
                *out = make([]ApisixRoutePlugin, len(*in))
diff --git a/internal/adc/translator/apisixroute.go 
b/internal/adc/translator/apisixroute.go
index 58a162c9..1e130538 100644
--- a/internal/adc/translator/apisixroute.go
+++ b/internal/adc/translator/apisixroute.go
@@ -24,7 +24,7 @@ import (
        "strconv"
 
        "github.com/pkg/errors"
-       v1 "k8s.io/api/core/v1"
+       corev1 "k8s.io/api/core/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/types"
        "k8s.io/apimachinery/pkg/util/intstr"
@@ -70,10 +70,10 @@ func (t *Translator) translateHTTPRule(tctx 
*provider.TranslateContext, ar *apiv
                return nil, err
        }
 
+       var enableWebsocket *bool
        service := t.buildService(ar, rule, ruleIndex)
-       t.buildRoute(ar, service, rule, plugins, timeout, vars)
-       t.buildUpstream(tctx, service, ar, rule)
-
+       t.buildUpstream(tctx, service, ar, rule, &enableWebsocket)
+       t.buildRoute(ar, service, rule, plugins, timeout, vars, 
&enableWebsocket)
        return service, nil
 }
 
@@ -139,7 +139,7 @@ func (t *Translator) loadRoutePlugins(tctx 
*provider.TranslateContext, ar *apiv2
        }
 }
 
-func (t *Translator) buildPluginConfig(plugin apiv2.ApisixRoutePlugin, 
namespace string, secrets map[types.NamespacedName]*v1.Secret) map[string]any {
+func (t *Translator) buildPluginConfig(plugin apiv2.ApisixRoutePlugin, 
namespace string, secrets map[types.NamespacedName]*corev1.Secret) 
map[string]any {
        config := make(map[string]any)
        if len(plugin.Config.Raw) > 0 {
                if err := json.Unmarshal(plugin.Config.Raw, &config); err != 
nil {
@@ -179,13 +179,16 @@ func (t *Translator) addAuthenticationPlugins(rule 
apiv2.ApisixRouteHTTP, plugin
        }
 }
 
-func (t *Translator) buildRoute(ar *apiv2.ApisixRoute, service *adc.Service, 
rule apiv2.ApisixRouteHTTP, plugins adc.Plugins, timeout *adc.Timeout, vars 
adc.Vars) {
+func (t *Translator) buildRoute(ar *apiv2.ApisixRoute, service *adc.Service, 
rule apiv2.ApisixRouteHTTP, plugins adc.Plugins, timeout *adc.Timeout, vars 
adc.Vars, enableWebsocket **bool) {
        route := adc.NewDefaultRoute()
        route.Name = adc.ComposeRouteName(ar.Namespace, ar.Name, rule.Name)
        route.ID = id.GenID(route.Name)
        route.Desc = "Created by apisix-ingress-controller, DO NOT modify it 
manually"
        route.Labels = label.GenLabel(ar)
-       route.EnableWebsocket = ptr.To(rule.Websocket)
+       route.EnableWebsocket = rule.Websocket
+       if route.EnableWebsocket == nil && *enableWebsocket != nil {
+               route.EnableWebsocket = *enableWebsocket
+       }
        route.FilterFunc = rule.Match.FilterFunc
        route.Hosts = rule.Match.Hosts
        route.Methods = rule.Match.Methods
@@ -202,7 +205,7 @@ func (t *Translator) buildRoute(ar *apiv2.ApisixRoute, 
service *adc.Service, rul
        service.Routes = []*adc.Route{route}
 }
 
-func (t *Translator) buildUpstream(tctx *provider.TranslateContext, service 
*adc.Service, ar *apiv2.ApisixRoute, rule apiv2.ApisixRouteHTTP) {
+func (t *Translator) buildUpstream(tctx *provider.TranslateContext, service 
*adc.Service, ar *apiv2.ApisixRoute, rule apiv2.ApisixRouteHTTP, 
enableWebsocket **bool) {
        var (
                upstreams         = make([]*adc.Upstream, 0)
                weightedUpstreams = 
make([]adc.TrafficSplitConfigRuleWeightedUpstream, 0)
@@ -211,7 +214,7 @@ func (t *Translator) buildUpstream(tctx 
*provider.TranslateContext, service *adc
        for _, backend := range rule.Backends {
                // try to get the apisixupstream with the same name as the 
backend service to be upstream config.
                // err is ignored because it does not care about the 
externalNodes of the apisixupstream.
-               upstream, err := t.translateApisixRouteHTTPBackend(tctx, ar, 
backend)
+               upstream, err := t.translateApisixRouteHTTPBackend(tctx, ar, 
backend, enableWebsocket)
                if err != nil {
                        t.Log.Error(err, "failed to translate ApisixRoute 
backend", "backend", backend)
                        continue
@@ -310,7 +313,7 @@ func (t *Translator) buildService(ar *apiv2.ApisixRoute, 
rule apiv2.ApisixRouteH
        return service
 }
 
-func getPortFromService(svc *v1.Service, backendSvcPort intstr.IntOrString) 
(int32, error) {
+func getPortFromService(svc *corev1.Service, backendSvcPort 
intstr.IntOrString) (int32, error) {
        var port int32
        if backendSvcPort.Type == intstr.Int {
                port = int32(backendSvcPort.IntValue())
@@ -330,7 +333,31 @@ func getPortFromService(svc *v1.Service, backendSvcPort 
intstr.IntOrString) (int
        return port, nil
 }
 
-func (t *Translator) translateApisixRouteHTTPBackend(tctx 
*provider.TranslateContext, ar *apiv2.ApisixRoute, backend 
apiv2.ApisixRouteHTTPBackend) (*adc.Upstream, error) {
+func findMatchingServicePort(svc *corev1.Service, backendSvcPort 
intstr.IntOrString) (*corev1.ServicePort, error) {
+       var servicePort *corev1.ServicePort
+       var portNumber int32 = -1
+       var servicePortName string
+       switch backendSvcPort.Type {
+       case intstr.Int:
+               portNumber = backendSvcPort.IntVal
+       case intstr.String:
+               servicePortName = backendSvcPort.StrVal
+       }
+       for _, svcPort := range svc.Spec.Ports {
+               p := svcPort
+               if p.Port == portNumber || (p.Name != "" && p.Name == 
servicePortName) {
+                       servicePort = &p
+                       break
+               }
+       }
+       if servicePort == nil {
+               return nil, errors.Errorf("service port %s not found in service 
%s", backendSvcPort.String(), svc.Name)
+       }
+
+       return servicePort, nil
+}
+
+func (t *Translator) translateApisixRouteHTTPBackend(tctx 
*provider.TranslateContext, ar *apiv2.ApisixRoute, backend 
apiv2.ApisixRouteHTTPBackend, enableWebsocket **bool) (*adc.Upstream, error) {
        auNN := types.NamespacedName{
                Namespace: ar.Namespace,
                Name:      backend.ServiceName,
@@ -352,50 +379,57 @@ func (t *Translator) translateApisixRouteHTTPBackend(tctx 
*provider.TranslateCon
                upstream = u
        }
        var (
-               err   error
-               nodes adc.UpstreamNodes
+               err      error
+               nodes    adc.UpstreamNodes
+               protocol string
        )
        if backend.ResolveGranularity == apiv2.ResolveGranularityService {
-               nodes, err = 
t.translateApisixRouteBackendResolveGranularityService(tctx, auNN, backend)
+               nodes, protocol, err = 
t.translateApisixRouteBackendResolveGranularityService(tctx, auNN, backend)
        } else {
-               nodes, err = 
t.translateApisixRouteBackendResolveGranularityEndpoint(tctx, auNN, backend)
+               nodes, protocol, err = 
t.translateApisixRouteBackendResolveGranularityEndpoint(tctx, auNN, backend)
        }
        if err != nil {
                return nil, err
        }
        upstream.Nodes = nodes
+       if upstream.Scheme == "" {
+               upstream.Scheme = appProtocolToUpstreamScheme(protocol)
+       }
+       if protocol == internaltypes.AppProtocolWS || protocol == 
internaltypes.AppProtocolWSS {
+               *enableWebsocket = ptr.To(true)
+       }
        if backend.Weight != nil {
                upstream.Labels["meta_weight"] = 
strconv.FormatInt(int64(*backend.Weight), 10)
        }
        return upstream, nil
 }
 
-func (t *Translator) translateApisixRouteBackendResolveGranularityService(tctx 
*provider.TranslateContext, arNN types.NamespacedName, backend 
apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
+func (t *Translator) translateApisixRouteBackendResolveGranularityService(tctx 
*provider.TranslateContext, arNN types.NamespacedName, backend 
apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, string, error) {
        serviceNN := types.NamespacedName{
                Namespace: arNN.Namespace,
                Name:      backend.ServiceName,
        }
        svc, ok := tctx.Services[serviceNN]
        if !ok {
-               return nil, errors.Errorf("service not found, ApisixRoute: %s, 
Service: %s", arNN, serviceNN)
+               return nil, "", errors.Errorf("service not found, ApisixRoute: 
%s, Service: %s", arNN, serviceNN)
        }
        if svc.Spec.ClusterIP == "" {
-               return nil, errors.Errorf("conflict headless service and 
backend resolve granularity, ApisixRoute: %s, Service: %s", arNN, serviceNN)
+               return nil, "", errors.Errorf("conflict headless service and 
backend resolve granularity, ApisixRoute: %s, Service: %s", arNN, serviceNN)
        }
-       port, err := getPortFromService(svc, backend.ServicePort)
+       port, err := findMatchingServicePort(svc, backend.ServicePort)
        if err != nil {
-               return nil, err
+               return nil, "", err
        }
        return adc.UpstreamNodes{
                {
                        Host:   svc.Spec.ClusterIP,
-                       Port:   int(port),
+                       Port:   int(port.Port),
                        Weight: *cmp.Or(backend.Weight, 
ptr.To(apiv2.DefaultWeight)),
                },
-       }, nil
+       }, ptr.Deref(port.AppProtocol, ""), nil
 }
 
-func (t *Translator) translateApisixRouteStreamBackendResolveGranularity(tctx 
*provider.TranslateContext, arNN types.NamespacedName, backend 
apiv2.ApisixRouteStreamBackend) (adc.UpstreamNodes, error) {
+func (t *Translator) translateApisixRouteStreamBackendResolveGranularity(tctx 
*provider.TranslateContext, arNN types.NamespacedName, backend 
apiv2.ApisixRouteStreamBackend) (adc.UpstreamNodes, string, error) {
        tsBackend := apiv2.ApisixRouteHTTPBackend{
                ServiceName:        backend.ServiceName,
                ServicePort:        backend.ServicePort,
@@ -409,18 +443,18 @@ func (t *Translator) 
translateApisixRouteStreamBackendResolveGranularity(tctx *p
        }
 }
 
-func (t *Translator) 
translateApisixRouteBackendResolveGranularityEndpoint(tctx 
*provider.TranslateContext, arNN types.NamespacedName, backend 
apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
+func (t *Translator) 
translateApisixRouteBackendResolveGranularityEndpoint(tctx 
*provider.TranslateContext, arNN types.NamespacedName, backend 
apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, string, error) {
        serviceNN := types.NamespacedName{
                Namespace: arNN.Namespace,
                Name:      backend.ServiceName,
        }
        svc, ok := tctx.Services[serviceNN]
        if !ok {
-               return nil, errors.Errorf("service not found, ApisixRoute: %s, 
Service: %s", arNN, serviceNN)
+               return nil, "", errors.Errorf("service not found, ApisixRoute: 
%s, Service: %s", arNN, serviceNN)
        }
        port, err := getPortFromService(svc, backend.ServicePort)
        if err != nil {
-               return nil, err
+               return nil, "", err
        }
        weight := int32(*cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight)))
        backendRef := gatewayv1.BackendRef{
@@ -482,7 +516,7 @@ func (t *Translator) translateApisixRouteStreamBackend(tctx 
*provider.TranslateC
                }
                upstream = u
        }
-       nodes, err := 
t.translateApisixRouteStreamBackendResolveGranularity(tctx, 
utils.NamespacedName(ar), backend)
+       nodes, _, err := 
t.translateApisixRouteStreamBackendResolveGranularity(tctx, 
utils.NamespacedName(ar), backend)
        if err != nil {
                return nil, err
        }
diff --git a/internal/adc/translator/gatewayproxy.go 
b/internal/adc/translator/gatewayproxy.go
index 6636816f..259c2ac2 100644
--- a/internal/adc/translator/gatewayproxy.go
+++ b/internal/adc/translator/gatewayproxy.go
@@ -98,7 +98,7 @@ func (t *Translator) TranslateGatewayProxyToConfig(tctx 
*provider.TranslateConte
                        if endpoint == nil {
                                return nil, nil
                        }
-                       upstreamNodes, err := 
t.TranslateBackendRefWithFilter(tctx, gatewayv1.BackendRef{
+                       upstreamNodes, _, err := 
t.TranslateBackendRefWithFilter(tctx, gatewayv1.BackendRef{
                                BackendObjectReference: 
gatewayv1.BackendObjectReference{
                                        Name:      
gatewayv1.ObjectName(provider.ControlPlane.Service.Name),
                                        Namespace: 
(*gatewayv1.Namespace)(&gatewayProxy.Namespace),
diff --git a/internal/adc/translator/grpcroute.go 
b/internal/adc/translator/grpcroute.go
index d24fc8bc..abe6dfab 100644
--- a/internal/adc/translator/grpcroute.go
+++ b/internal/adc/translator/grpcroute.go
@@ -183,7 +183,7 @@ func (t *Translator) TranslateGRPCRoute(tctx 
*provider.TranslateContext, grpcRou
                                backend.Namespace = &namespace
                        }
                        upstream := adctypes.NewDefaultUpstream()
-                       upNodes, err := t.translateBackendRef(tctx, 
backend.BackendRef, DefaultEndpointFilter)
+                       upNodes, _, err := t.translateBackendRef(tctx, 
backend.BackendRef, DefaultEndpointFilter)
                        if err != nil {
                                backendErr = err
                                continue
diff --git a/internal/adc/translator/httproute.go 
b/internal/adc/translator/httproute.go
index ebee4a45..12b5c6ff 100644
--- a/internal/adc/translator/httproute.go
+++ b/internal/adc/translator/httproute.go
@@ -391,13 +391,15 @@ func DefaultEndpointFilter(endpoint 
*discoveryv1.Endpoint) bool {
        return true
 }
 
-func (t *Translator) TranslateBackendRefWithFilter(tctx 
*provider.TranslateContext, ref gatewayv1.BackendRef, endpointFilter 
func(*discoveryv1.Endpoint) bool) (adctypes.UpstreamNodes, error) {
+func (t *Translator) TranslateBackendRefWithFilter(tctx 
*provider.TranslateContext, ref gatewayv1.BackendRef, endpointFilter 
func(*discoveryv1.Endpoint) bool) (adctypes.UpstreamNodes, string, error) {
        return t.translateBackendRef(tctx, ref, endpointFilter)
 }
 
-func (t *Translator) translateBackendRef(tctx *provider.TranslateContext, ref 
gatewayv1.BackendRef, endpointFilter func(*discoveryv1.Endpoint) bool) 
(adctypes.UpstreamNodes, error) {
+func (t *Translator) translateBackendRef(tctx *provider.TranslateContext, ref 
gatewayv1.BackendRef, endpointFilter func(*discoveryv1.Endpoint) bool) 
(adctypes.UpstreamNodes, string, error) {
+       nodes := adctypes.UpstreamNodes{}
+       var protocol string
        if ref.Kind != nil && *ref.Kind != internaltypes.KindService {
-               return adctypes.UpstreamNodes{}, fmt.Errorf("kind %s is not 
supported", *ref.Kind)
+               return nodes, protocol, fmt.Errorf("kind %s is not supported", 
*ref.Kind)
        }
 
        key := types.NamespacedName{
@@ -406,7 +408,7 @@ func (t *Translator) translateBackendRef(tctx 
*provider.TranslateContext, ref ga
        }
        service, ok := tctx.Services[key]
        if !ok {
-               return adctypes.UpstreamNodes{}, fmt.Errorf("service %s not 
found", key)
+               return nodes, protocol, fmt.Errorf("service %s not found", key)
        }
 
        weight := 1
@@ -425,7 +427,7 @@ func (t *Translator) translateBackendRef(tctx 
*provider.TranslateContext, ref ga
                                Port:   port,
                                Weight: weight,
                        },
-               }, nil
+               }, protocol, nil
        }
 
        var portName *string
@@ -433,16 +435,18 @@ func (t *Translator) translateBackendRef(tctx 
*provider.TranslateContext, ref ga
                for _, p := range service.Spec.Ports {
                        if int(p.Port) == int(*ref.Port) {
                                portName = ptr.To(p.Name)
+                               protocol = ptr.Deref(p.AppProtocol, "")
                                break
                        }
                }
                if portName == nil {
-                       return adctypes.UpstreamNodes{}, nil
+                       return adctypes.UpstreamNodes{}, protocol, nil
                }
        }
 
        endpointSlices := tctx.EndpointSlices[key]
-       return t.translateEndpointSlice(portName, weight, endpointSlices, 
endpointFilter), nil
+       nodes = t.translateEndpointSlice(portName, weight, endpointSlices, 
endpointFilter)
+       return nodes, protocol, nil
 }
 
 // calculateHTTPRoutePriority calculates the priority of the HTTP route.
@@ -544,6 +548,7 @@ func (t *Translator) TranslateHTTPRoute(tctx 
*provider.TranslateContext, httpRou
                        upstreams         = make([]*adctypes.Upstream, 0)
                        weightedUpstreams = 
make([]adctypes.TrafficSplitConfigRuleWeightedUpstream, 0)
                        backendErr        error
+                       enableWebsocket   *bool
                )
 
                for _, backend := range rule.BackendRefs {
@@ -552,7 +557,7 @@ func (t *Translator) TranslateHTTPRoute(tctx 
*provider.TranslateContext, httpRou
                                backend.Namespace = &namespace
                        }
                        upstream := adctypes.NewDefaultUpstream()
-                       upNodes, err := t.translateBackendRef(tctx, 
backend.BackendRef, DefaultEndpointFilter)
+                       upNodes, protocol, err := t.translateBackendRef(tctx, 
backend.BackendRef, DefaultEndpointFilter)
                        if err != nil {
                                backendErr = err
                                continue
@@ -560,10 +565,13 @@ func (t *Translator) TranslateHTTPRoute(tctx 
*provider.TranslateContext, httpRou
                        if len(upNodes) == 0 {
                                continue
                        }
+                       if protocol == internaltypes.AppProtocolWS || protocol 
== internaltypes.AppProtocolWSS {
+                               enableWebsocket = ptr.To(true)
+                       }
 
                        
t.AttachBackendTrafficPolicyToUpstream(backend.BackendRef, 
tctx.BackendTrafficPolicies, upstream)
                        upstream.Nodes = upNodes
-
+                       upstream.Scheme = appProtocolToUpstreamScheme(protocol)
                        var (
                                kind string
                                port int32
@@ -683,7 +691,7 @@ func (t *Translator) TranslateHTTPRoute(tctx 
*provider.TranslateContext, httpRou
                        route.Name = name
                        route.ID = id.GenID(name)
                        route.Labels = labels
-                       route.EnableWebsocket = ptr.To(true)
+                       route.EnableWebsocket = enableWebsocket
 
                        // Set the route priority
                        priority := calculateHTTPRoutePriority(&match, 
ruleIndex, hosts)
@@ -825,3 +833,18 @@ func (t *Translator) 
translateHTTPRouteHeaderMatchToVars(header gatewayv1.HTTPHe
        }
        return HeaderMatchToVars(matchType, string(header.Name), header.Value)
 }
+
+func appProtocolToUpstreamScheme(appProtocol string) string {
+       switch appProtocol {
+       case internaltypes.AppProtocolHTTP:
+               return apiv2.SchemeHTTP
+       case internaltypes.AppProtocolHTTPS:
+               return apiv2.SchemeHTTPS
+       case internaltypes.AppProtocolWS:
+               return apiv2.SchemeHTTP
+       case internaltypes.AppProtocolWSS:
+               return apiv2.SchemeHTTPS
+       default:
+               return ""
+       }
+}
diff --git a/internal/adc/translator/ingress.go 
b/internal/adc/translator/ingress.go
index 98cb4c3e..50bd5bc4 100644
--- a/internal/adc/translator/ingress.go
+++ b/internal/adc/translator/ingress.go
@@ -25,6 +25,7 @@ import (
        discoveryv1 "k8s.io/api/discovery/v1"
        networkingv1 "k8s.io/api/networking/v1"
        "k8s.io/apimachinery/pkg/types"
+       "k8s.io/utils/ptr"
 
        adctypes "github.com/apache/apisix-ingress-controller/api/adc"
        "github.com/apache/apisix-ingress-controller/internal/controller/label"
@@ -69,12 +70,46 @@ func (t *Translator) translateIngressTLS(namespace, name 
string, tlsIndex int, i
        return ssl, nil
 }
 
-func (t *Translator) TranslateIngress(tctx *provider.TranslateContext, obj 
*networkingv1.Ingress) (*TranslateResult, error) {
+func (t *Translator) TranslateIngress(
+       tctx *provider.TranslateContext,
+       obj *networkingv1.Ingress,
+) (*TranslateResult, error) {
        result := &TranslateResult{}
 
        labels := label.GenLabel(obj)
 
        // handle TLS configuration, convert to SSL objects
+       if err := t.translateIngressTLSSection(tctx, obj, result, labels); err 
!= nil {
+               return nil, err
+       }
+
+       // process Ingress rules, convert to Service and Route objects
+       for i, rule := range obj.Spec.Rules {
+               if rule.HTTP == nil {
+                       continue
+               }
+
+               hosts := []string{}
+               if rule.Host != "" {
+                       hosts = append(hosts, rule.Host)
+               }
+
+               for j, path := range rule.HTTP.Paths {
+                       if svc := t.buildServiceFromIngressPath(tctx, obj, 
&path, i, j, hosts, labels); svc != nil {
+                               result.Services = append(result.Services, svc)
+                       }
+               }
+       }
+
+       return result, nil
+}
+
+func (t *Translator) translateIngressTLSSection(
+       tctx *provider.TranslateContext,
+       obj *networkingv1.Ingress,
+       result *TranslateResult,
+       labels map[string]string,
+) error {
        for tlsIndex, tls := range obj.Spec.TLS {
                if tls.SecretName == "" {
                        continue
@@ -88,137 +123,150 @@ func (t *Translator) TranslateIngress(tctx 
*provider.TranslateContext, obj *netw
                }
                ssl, err := t.translateIngressTLS(obj.Namespace, obj.Name, 
tlsIndex, &tls, secret, labels)
                if err != nil {
-                       return nil, err
+                       return err
                }
-
                result.SSL = append(result.SSL, ssl)
        }
+       return nil
+}
 
-       // process Ingress rules, convert to Service and Route objects
-       for i, rule := range obj.Spec.Rules {
-               // extract hostnames
-               var hosts []string
-               if rule.Host != "" {
-                       hosts = append(hosts, rule.Host)
-               }
-               // if there is no HTTP path, skip
-               if rule.HTTP == nil {
-                       continue
-               }
+func (t *Translator) buildServiceFromIngressPath(
+       tctx *provider.TranslateContext,
+       obj *networkingv1.Ingress,
+       path *networkingv1.HTTPIngressPath,
+       ruleIndex, pathIndex int,
+       hosts []string,
+       labels map[string]string,
+) *adctypes.Service {
+       if path.Backend.Service == nil {
+               return nil
+       }
 
-               // create a service for each path
-               for j, path := range rule.HTTP.Paths {
-                       if path.Backend.Service == nil {
-                               continue
-                       }
+       service := adctypes.NewDefaultService()
+       service.Labels = labels
+       service.Name = adctypes.ComposeServiceNameWithRule(obj.Namespace, 
obj.Name, fmt.Sprintf("%d-%d", ruleIndex, pathIndex))
+       service.ID = id.GenID(service.Name)
+       service.Hosts = hosts
 
-                       service := adctypes.NewDefaultService()
-                       service.Labels = labels
-                       service.Name = 
adctypes.ComposeServiceNameWithRule(obj.Namespace, obj.Name, 
fmt.Sprintf("%d-%d", i, j))
-                       service.ID = id.GenID(service.Name)
-                       service.Hosts = hosts
+       upstream := adctypes.NewDefaultUpstream()
+       protocol := t.resolveIngressUpstream(tctx, obj, path.Backend.Service, 
upstream)
+       service.Upstream = upstream
 
-                       // create an upstream
-                       upstream := adctypes.NewDefaultUpstream()
+       route := buildRouteFromIngressPath(obj, path, ruleIndex, pathIndex, 
labels)
+       if protocol == internaltypes.AppProtocolWS || protocol == 
internaltypes.AppProtocolWSS {
+               route.EnableWebsocket = ptr.To(true)
+       }
+       service.Routes = []*adctypes.Route{route}
 
-                       // get the EndpointSlice of the backend service
-                       backendService := path.Backend.Service
-                       if backendService != nil {
-                               backendRef := convertBackendRef(obj.Namespace, 
backendService.Name, internaltypes.KindService)
-                               
t.AttachBackendTrafficPolicyToUpstream(backendRef, tctx.BackendTrafficPolicies, 
upstream)
-                       }
+       t.fillHTTPRoutePoliciesForIngress(tctx, service.Routes)
+       return service
+}
 
-                       // get the service port configuration
-                       var servicePort int32 = 0
-                       var servicePortName string
-                       if backendService.Port.Number != 0 {
-                               servicePort = backendService.Port.Number
-                       } else if backendService.Port.Name != "" {
-                               servicePortName = backendService.Port.Name
-                       }
+func (t *Translator) resolveIngressUpstream(
+       tctx *provider.TranslateContext,
+       obj *networkingv1.Ingress,
+       backendService *networkingv1.IngressServiceBackend,
+       upstream *adctypes.Upstream,
+) string {
+       backendRef := convertBackendRef(obj.Namespace, backendService.Name, 
internaltypes.KindService)
+       t.AttachBackendTrafficPolicyToUpstream(backendRef, 
tctx.BackendTrafficPolicies, upstream)
+       // determine service port/port name
+       var protocol string
+       var servicePort int32 = 0
+       var servicePortName string
+       if backendService.Port.Number != 0 {
+               servicePort = backendService.Port.Number
+       } else if backendService.Port.Name != "" {
+               servicePortName = backendService.Port.Name
+       }
 
-                       getService := tctx.Services[types.NamespacedName{
-                               Namespace: obj.Namespace,
-                               Name:      backendService.Name,
-                       }]
-                       if getService == nil {
-                               continue
-                       }
-                       if getService.Spec.Type == 
corev1.ServiceTypeExternalName {
-                               defaultServicePort := 80
-                               if servicePort > 0 {
-                                       defaultServicePort = int(servicePort)
-                               }
-                               upstream.Nodes = adctypes.UpstreamNodes{
-                                       {
-                                               Host:   
getService.Spec.ExternalName,
-                                               Port:   defaultServicePort,
-                                               Weight: 1,
-                                       },
-                               }
-                       } else {
-                               var getServicePort *corev1.ServicePort
-                               for _, port := range getService.Spec.Ports {
-                                       port := port
-                                       if servicePort > 0 && port.Port == 
servicePort {
-                                               getServicePort = &port
-                                               break
-                                       }
-                                       if servicePortName != "" && port.Name 
== servicePortName {
-                                               getServicePort = &port
-                                               break
-                                       }
-                               }
-                               endpointSlices := 
tctx.EndpointSlices[types.NamespacedName{
-                                       Namespace: obj.Namespace,
-                                       Name:      backendService.Name,
-                               }]
-                               // convert the EndpointSlice to upstream nodes
-                               if len(endpointSlices) > 0 {
-                                       upstream.Nodes = 
t.translateEndpointSliceForIngress(1, endpointSlices, getServicePort)
-                               }
-                       }
+       getService := tctx.Services[types.NamespacedName{
+               Namespace: obj.Namespace,
+               Name:      backendService.Name,
+       }]
+       if getService == nil {
+               return protocol
+       }
 
-                       service.Upstream = upstream
-
-                       // create a route
-                       route := adctypes.NewDefaultRoute()
-                       route.Name = adctypes.ComposeRouteName(obj.Namespace, 
obj.Name, fmt.Sprintf("%d-%d", i, j))
-                       route.ID = id.GenID(route.Name)
-                       route.Labels = labels
-
-                       uris := []string{path.Path}
-                       if path.PathType != nil {
-                               switch *path.PathType {
-                               case networkingv1.PathTypePrefix:
-                                       // As per the specification of Ingress 
path matching rule:
-                                       // if the last element of the path is a 
substring of the
-                                       // last element in request path, it is 
not a match, e.g. /foo/bar
-                                       // matches /foo/bar/baz, but does not 
match /foo/barbaz.
-                                       // While in APISIX, /foo/bar matches 
both /foo/bar/baz and
-                                       // /foo/barbaz.
-                                       // In order to be conformant with 
Ingress specification, here
-                                       // we create two paths here, the first 
is the path itself
-                                       // (exact match), the other is path + 
"/*" (prefix match).
-                                       prefix := path.Path
-                                       if strings.HasSuffix(prefix, "/") {
-                                               prefix += "*"
-                                       } else {
-                                               prefix += "/*"
-                                       }
-                                       uris = append(uris, prefix)
-                               case 
networkingv1.PathTypeImplementationSpecific:
-                                       uris = []string{"/*"}
-                               }
-                       }
-                       route.Uris = uris
-                       service.Routes = []*adctypes.Route{route}
-                       t.fillHTTPRoutePoliciesForIngress(tctx, service.Routes)
-                       result.Services = append(result.Services, service)
+       if getService.Spec.Type == corev1.ServiceTypeExternalName {
+               defaultServicePort := 80
+               if servicePort > 0 {
+                       defaultServicePort = int(servicePort)
                }
+               upstream.Nodes = adctypes.UpstreamNodes{
+                       {
+                               Host:   getService.Spec.ExternalName,
+                               Port:   defaultServicePort,
+                               Weight: 1,
+                       },
+               }
+               return protocol
        }
 
-       return result, nil
+       // find matching service port object
+       var getServicePort *corev1.ServicePort
+       for _, port := range getService.Spec.Ports {
+               p := port
+               if servicePort > 0 && p.Port == servicePort {
+                       getServicePort = &p
+                       break
+               }
+               if servicePortName != "" && p.Name == servicePortName {
+                       getServicePort = &p
+                       break
+               }
+       }
+
+       if getServicePort != nil && getServicePort.AppProtocol != nil {
+               protocol = *getServicePort.AppProtocol
+               if upstream.Scheme == "" {
+                       upstream.Scheme = 
appProtocolToUpstreamScheme(*getServicePort.AppProtocol)
+               }
+       }
+
+       endpointSlices := tctx.EndpointSlices[types.NamespacedName{
+               Namespace: obj.Namespace,
+               Name:      backendService.Name,
+       }]
+       if len(endpointSlices) > 0 {
+               upstream.Nodes = t.translateEndpointSliceForIngress(1, 
endpointSlices, getServicePort)
+       }
+
+       return protocol
+}
+
+func buildRouteFromIngressPath(
+       obj *networkingv1.Ingress,
+       path *networkingv1.HTTPIngressPath,
+       ruleIndex, pathIndex int,
+       labels map[string]string,
+) *adctypes.Route {
+       route := adctypes.NewDefaultRoute()
+       route.Name = adctypes.ComposeRouteName(obj.Namespace, obj.Name, 
fmt.Sprintf("%d-%d", ruleIndex, pathIndex))
+       route.ID = id.GenID(route.Name)
+       route.Labels = labels
+
+       uris := []string{path.Path}
+       if path.PathType != nil {
+               switch *path.PathType {
+               case networkingv1.PathTypePrefix:
+                       // As per the specification of Ingress path matching 
rule:
+                       // if the last element of the path is a substring of the
+                       // last element in request path, it is not a match, 
e.g. /foo/bar
+                       // matches /foo/bar/baz, but does not match /foo/barbaz.
+                       // While in APISIX, /foo/bar matches both /foo/bar/baz 
and
+                       // /foo/barbaz.
+                       // In order to be conformant with Ingress 
specification, here
+                       // we create two paths here, the first is the path 
itself
+                       // (exact match), the other is path + "/*" (prefix 
match).
+                       prefix := strings.TrimSuffix(path.Path, "/") + "/*"
+                       uris = append(uris, prefix)
+               case networkingv1.PathTypeImplementationSpecific:
+                       uris = []string{"/*"}
+               }
+       }
+       route.Uris = uris
+       return route
 }
 
 // translateEndpointSliceForIngress create upstream nodes from EndpointSlice
diff --git a/internal/adc/translator/tcproute.go 
b/internal/adc/translator/tcproute.go
index ffef927c..fc9c0b13 100644
--- a/internal/adc/translator/tcproute.go
+++ b/internal/adc/translator/tcproute.go
@@ -61,7 +61,7 @@ func (t *Translator) TranslateTCPRoute(tctx 
*provider.TranslateContext, tcpRoute
                                backend.Namespace = &namespace
                        }
                        upstream := newDefaultUpstreamWithoutScheme()
-                       upNodes, err := t.translateBackendRef(tctx, backend, 
DefaultEndpointFilter)
+                       upNodes, _, err := t.translateBackendRef(tctx, backend, 
DefaultEndpointFilter)
                        if err != nil {
                                continue
                        }
diff --git a/internal/adc/translator/tlsroute.go 
b/internal/adc/translator/tlsroute.go
index 85bfba0c..b1eb5fa0 100644
--- a/internal/adc/translator/tlsroute.go
+++ b/internal/adc/translator/tlsroute.go
@@ -54,7 +54,7 @@ func (t *Translator) TranslateTLSRoute(tctx 
*provider.TranslateContext, tlsRoute
                                backend.Namespace = &namespace
                        }
                        upstream := newDefaultUpstreamWithoutScheme()
-                       upNodes, err := t.translateBackendRef(tctx, backend, 
DefaultEndpointFilter)
+                       upNodes, _, err := t.translateBackendRef(tctx, backend, 
DefaultEndpointFilter)
                        if err != nil {
                                continue
                        }
diff --git a/internal/adc/translator/udproute.go 
b/internal/adc/translator/udproute.go
index 983130cb..5cc09a10 100644
--- a/internal/adc/translator/udproute.go
+++ b/internal/adc/translator/udproute.go
@@ -50,7 +50,7 @@ func (t *Translator) TranslateUDPRoute(tctx 
*provider.TranslateContext, udpRoute
                                backend.Namespace = &namespace
                        }
                        upstream := newDefaultUpstreamWithoutScheme()
-                       upNodes, err := t.translateBackendRef(tctx, backend, 
DefaultEndpointFilter)
+                       upNodes, _, err := t.translateBackendRef(tctx, backend, 
DefaultEndpointFilter)
                        if err != nil {
                                continue
                        }
diff --git a/internal/types/k8s.go b/internal/types/k8s.go
index 914442b4..5506a426 100644
--- a/internal/types/k8s.go
+++ b/internal/types/k8s.go
@@ -57,6 +57,13 @@ const (
        KindApisixUpstream       = "ApisixUpstream"
 )
 
+const (
+       AppProtocolHTTP  = "http"
+       AppProtocolHTTPS = "https"
+       AppProtocolWS    = "kubernetes.io/ws"
+       AppProtocolWSS   = "kubernetes.io/wss"
+)
+
 func KindOf(obj any) string {
        switch obj.(type) {
        case *gatewayv1.Gateway:
diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go
index 69c5f83d..5509b3af 100644
--- a/test/e2e/crds/v2/route.go
+++ b/test/e2e/crds/v2/route.go
@@ -19,6 +19,7 @@ package v2
 
 import (
        "context"
+       "crypto/tls"
        "fmt"
        "io"
        "math"
@@ -34,6 +35,7 @@ import (
        "github.com/stretchr/testify/assert"
        "k8s.io/apimachinery/pkg/types"
        "k8s.io/apimachinery/pkg/util/wait"
+       "k8s.io/utils/ptr"
 
        apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
        "github.com/apache/apisix-ingress-controller/test/e2e/framework"
@@ -2033,4 +2035,146 @@ spec:
                        })
                })
        })
+
+       Context("Test Services With AppProtocol", func() {
+               const apisixRoute = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: nginx
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule0
+    match:
+      hosts:
+      - nginx.example
+      paths:
+      - /v1
+    backends:
+    - serviceName: nginx
+      servicePort: 443
+`
+               const apisixRouteWithGranularityService = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: nginx-v2
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule0
+    match:
+      hosts:
+      - nginx.example
+      paths:
+      - /v2
+    backends:
+    - serviceName: nginx
+      servicePort: 443
+      resolveGranularity: service
+`
+               const apisixRouteWithBackendWSS = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: default
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule0
+    match:
+      hosts:
+      - api6.com
+      paths:
+      - /ws
+    backends:
+    - serviceName: nginx
+      servicePort: 8443
+      resolveGranularity: service
+`
+
+               const apisixTlsSpec = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: test-tls
+spec:
+  ingressClassName: %s
+  hosts:
+  - api6.com
+  secret:
+    name: test-tls-secret
+    namespace: %s
+`
+               BeforeEach(func() {
+                       s.DeployNginx(framework.NginxOptions{
+                               Namespace: s.Namespace(),
+                               Replicas:  ptr.To(int32(1)),
+                       })
+               })
+
+               It("HTTPS Backend", func() {
+                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "nginx"},
+                               new(apiv2.ApisixRoute), 
fmt.Sprintf(apisixRoute, s.Namespace()))
+                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "nginx-v2"},
+                               new(apiv2.ApisixRoute), 
fmt.Sprintf(apisixRouteWithGranularityService, s.Namespace()))
+
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/v1",
+                               Host:   "nginx.example",
+                               Check:  
scaffold.WithExpectedStatus(http.StatusOK),
+                       })
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/v2",
+                               Host:   "nginx.example",
+                               Check:  
scaffold.WithExpectedStatus(http.StatusOK),
+                       })
+               })
+
+               It("WSS Backend", func() {
+                       err := s.NewKubeTlsSecret("test-tls-secret", Cert, Key)
+                       Expect(err).NotTo(HaveOccurred(), "creating TLS secret")
+                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "test-tls"},
+                               &apiv2.ApisixTls{}, fmt.Sprintf(apisixTlsSpec, 
s.Namespace(), s.Namespace()))
+
+                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "default"},
+                               new(apiv2.ApisixRoute), 
fmt.Sprintf(apisixRouteWithBackendWSS, s.Namespace()))
+                       time.Sleep(6 * time.Second)
+
+                       By("verify wss connection")
+                       u := url.URL{
+                               Scheme: "wss",
+                               Host:   s.GetAPISIXHTTPSEndpoint(),
+                               Path:   "/ws",
+                       }
+                       headers := http.Header{"Host": []string{"api6.com"}}
+                       dialer := websocket.Dialer{
+                               TLSClientConfig: &tls.Config{
+                                       InsecureSkipVerify: true,
+                                       ServerName:         "api6.com",
+                               },
+                       }
+
+                       conn, resp, err := dialer.Dial(u.String(), headers)
+                       Expect(err).ShouldNot(HaveOccurred(), "WebSocket 
handshake")
+                       
Expect(resp.StatusCode).Should(Equal(http.StatusSwitchingProtocols))
+
+                       defer func() {
+                               _ = conn.Close()
+                       }()
+
+                       By("send and receive message through WebSocket")
+                       testMessage := "hello, this is APISIX"
+                       err = conn.WriteMessage(websocket.TextMessage, 
[]byte(testMessage))
+                       Expect(err).ShouldNot(HaveOccurred(), "writing 
WebSocket message")
+
+                       // Then our echo
+                       _, msg, err := conn.ReadMessage()
+                       Expect(err).ShouldNot(HaveOccurred(), "reading echo 
message")
+                       Expect(string(msg)).To(Equal(testMessage), "message 
content verification")
+               })
+       })
 })
diff --git a/test/e2e/framework/manifests/nginx.yaml 
b/test/e2e/framework/manifests/nginx.yaml
index 053e383f..a01d5549 100644
--- a/test/e2e/framework/manifests/nginx.yaml
+++ b/test/e2e/framework/manifests/nginx.yaml
@@ -42,6 +42,39 @@ data:
         location / {
           return 200 'Hello, World!';
         }
+
+        location /ws {
+          content_by_lua_block {
+            local server = require "resty.websocket.server"
+            local wb, err = server:new {
+              timeout = 5000,          -- 5 seconds timeout
+              max_payload_len = 65535  -- max message length
+            }
+            if not wb then
+              ngx.log(ngx.ERR, "failed to create websocket: ", err)
+              return ngx.exit(444)
+            end
+
+            while true do
+              local data, typ, err = wb:recv_frame()
+              if wb.fatal then
+                ngx.log(ngx.ERR, "failed to receive frame: ", err)
+                break
+              end
+
+              if typ == "close" then
+                wb:send_close()
+                break
+              elseif typ == "text" then
+                wb:send_text(data)   -- echo text
+              elseif typ == "binary" then
+                wb:send_binary(data) -- echo binary
+              elseif typ == "ping" then
+                wb:send_pong()
+              end
+            end
+          }
+        }
       }
     }
 
@@ -77,7 +110,7 @@ spec:
               path: /healthz
               port: 80
             timeoutSeconds: 2
-          image: "nginx:1.21.4"
+          image: "openresty/openresty:1.27.1.2-4-bullseye-fat"
           imagePullPolicy: IfNotPresent
           name: nginx
           ports:
@@ -88,7 +121,7 @@ spec:
               name: "https"
               protocol: "TCP"
           volumeMounts:
-            - mountPath: /etc/nginx/nginx.conf
+            - mountPath: /usr/local/openresty/nginx/conf/nginx.conf
               name: nginx-config
               subPath: nginx.conf
             - mountPath: /etc/nginx/ssl
@@ -110,6 +143,17 @@ spec:
       port: 443
       protocol: TCP
       targetPort: 443
+      appProtocol: https
+    - name: ws
+      port: 8080
+      protocol: TCP
+      targetPort: 80
+      appProtocol: kubernetes.io/ws
+    - name: wss
+      port: 8443
+      protocol: TCP
+      targetPort: 443
+      appProtocol: kubernetes.io/wss
   type: ClusterIP
 ---
 apiVersion: v1
diff --git a/test/e2e/gatewayapi/httproute.go b/test/e2e/gatewayapi/httproute.go
index 6429fdde..d36daeb4 100644
--- a/test/e2e/gatewayapi/httproute.go
+++ b/test/e2e/gatewayapi/httproute.go
@@ -19,11 +19,14 @@ package gatewayapi
 
 import (
        "context"
+       "crypto/tls"
        "fmt"
        "net/http"
+       "net/url"
        "strings"
        "time"
 
+       "github.com/gorilla/websocket"
        . "github.com/onsi/ginkgo/v2"
        . "github.com/onsi/gomega"
        "github.com/pkg/errors"
@@ -2439,4 +2442,104 @@ spec:
                })
 
        })
+
+       Context("Test Service With AppProtocol", func() {
+               var (
+                       httproute = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: nginx
+spec:
+  parentRefs:
+  - name: %s
+  hostnames:
+  - api6.com
+  rules:
+  - matches:
+    - path:
+        type: Exact
+        value: /get
+    backendRefs:
+    - name: nginx
+      port: 443
+ `
+                       httprouteWithWSS = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: nginx-wss
+spec:
+  parentRefs:
+  - name: %s
+  hostnames:
+  - api6.com
+  rules:
+  - matches:
+    - path:
+        type: Exact
+        value: /ws
+    backendRefs:
+    - name: nginx
+      port: 8443
+ `
+               )
+
+               BeforeEach(func() {
+                       beforeEachHTTPS()
+                       s.DeployNginx(framework.NginxOptions{
+                               Namespace: s.Namespace(),
+                               Replicas:  ptr.To(int32(1)),
+                       })
+               })
+               It("HTTPS backend", func() {
+                       s.ResourceApplied("HTTPRoute", "nginx", 
fmt.Sprintf(httproute, s.Namespace()), 1)
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Host:   "api6.com",
+                               Check:  
scaffold.WithExpectedStatus(http.StatusOK),
+                       })
+               })
+
+               It("WSS backend", func() {
+                       s.ResourceApplied("HTTPRoute", "nginx-wss", 
fmt.Sprintf(httprouteWithWSS, s.Namespace()), 1)
+                       time.Sleep(6 * time.Second)
+
+                       By("verify wss connection")
+                       u := url.URL{
+                               Scheme: "wss",
+                               Host:   s.GetAPISIXHTTPSEndpoint(),
+                               Path:   "/ws",
+                       }
+                       headers := http.Header{"Host": []string{"api6.com"}}
+
+                       hostname := "api6.com"
+
+                       dialer := websocket.Dialer{
+                               TLSClientConfig: &tls.Config{
+                                       InsecureSkipVerify: true,
+                                       ServerName:         hostname,
+                               },
+                       }
+
+                       conn, resp, err := dialer.Dial(u.String(), headers)
+                       Expect(err).ShouldNot(HaveOccurred(), "WebSocket 
handshake")
+                       
Expect(resp.StatusCode).Should(Equal(http.StatusSwitchingProtocols))
+
+                       defer func() {
+                               _ = conn.Close()
+                       }()
+
+                       By("send and receive message through WebSocket")
+                       testMessage := "hello, this is APISIX"
+                       err = conn.WriteMessage(websocket.TextMessage, 
[]byte(testMessage))
+                       Expect(err).ShouldNot(HaveOccurred(), "writing 
WebSocket message")
+
+                       // Then our echo
+                       _, msg, err := conn.ReadMessage()
+                       Expect(err).ShouldNot(HaveOccurred(), "reading echo 
message")
+                       Expect(string(msg)).To(Equal(testMessage), "message 
content verification")
+               })
+       })
 })
diff --git a/test/e2e/ingress/ingress.go b/test/e2e/ingress/ingress.go
index b61e3109..1e3e3a50 100644
--- a/test/e2e/ingress/ingress.go
+++ b/test/e2e/ingress/ingress.go
@@ -19,16 +19,20 @@ package ingress
 
 import (
        "context"
+       "crypto/tls"
        "encoding/base64"
        "fmt"
        "net/http"
+       "net/url"
        "strings"
        "time"
 
+       "github.com/gorilla/websocket"
        . "github.com/onsi/ginkgo/v2"
        . "github.com/onsi/gomega"
        "github.com/stretchr/testify/assert"
        "k8s.io/apimachinery/pkg/types"
+       "k8s.io/utils/ptr"
 
        "github.com/apache/apisix-ingress-controller/api/v1alpha1"
        "github.com/apache/apisix-ingress-controller/test/e2e/framework"
@@ -929,6 +933,129 @@ spec:
                })
        })
 
+       Context("Test Services With AppProtocol", func() {
+               var ingressClass = `
+apiVersion: networking.k8s.io/v1
+kind: IngressClass
+metadata:
+  name: %s
+spec:
+  controller: "%s"
+  parameters:
+    apiGroup: "apisix.apache.org"
+    kind: "GatewayProxy"
+    name: "apisix-proxy-config"
+    namespace: "%s"
+    scope: "Namespace"
+`
+               var ingress = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: apisix-ingress-tls
+spec:
+  ingressClassName: %s
+  rules:
+  - host: nginx.example
+    http:
+      paths:
+      - path: /get
+        pathType: Exact
+        backend:
+          service:
+            name: nginx
+            port:
+              number: 443
+`
+               var ingressWithWSS = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: apisix-ingress-wss
+spec:
+  ingressClassName: %s
+  tls:
+  - hosts:
+    - api6.com
+    secretName: test-ingress-tls
+  rules:
+  - host: api6.com
+    http:
+      paths:
+      - path: /ws
+        pathType: Exact
+        backend:
+          service:
+            name: nginx
+            port:
+              number: 8443
+`
+               BeforeEach(func() {
+                       s.DeployNginx(framework.NginxOptions{
+                               Namespace: s.Namespace(),
+                               Replicas:  ptr.To(int32(1)),
+                       })
+                       By("create GatewayProxy")
+                       
Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(),
 "creating GatewayProxy")
+
+                       By("create IngressClass")
+                       err := 
s.CreateResourceFromStringWithNamespace(fmt.Sprintf(ingressClass, 
s.Namespace(), s.GetControllerName(), s.Namespace()), s.Namespace())
+                       Expect(err).NotTo(HaveOccurred(), "creating 
IngressClass")
+                       time.Sleep(5 * time.Second)
+               })
+
+               It("Ingress With HTTPS Backend", func() {
+                       By("create Ingress")
+                       Expect(s.CreateResourceFromString(fmt.Sprintf(ingress, 
s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Host:   "nginx.example",
+                               Check:  
scaffold.WithExpectedStatus(http.StatusOK),
+                       })
+               })
+
+               It("Ingress With WSS Backend", func() {
+                       createSecret(s, _secretName)
+                       By("create Ingress")
+                       
Expect(s.CreateResourceFromString(fmt.Sprintf(ingressWithWSS, 
s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+                       time.Sleep(6 * time.Second)
+
+                       By("verify wss connection")
+                       u := url.URL{
+                               Scheme: "wss",
+                               Host:   s.GetAPISIXHTTPSEndpoint(),
+                               Path:   "/ws",
+                       }
+                       headers := http.Header{"Host": []string{"api6.com"}}
+                       dialer := websocket.Dialer{
+                               TLSClientConfig: &tls.Config{
+                                       InsecureSkipVerify: true,
+                                       ServerName:         "api6.com",
+                               },
+                       }
+
+                       conn, resp, err := dialer.Dial(u.String(), headers)
+                       Expect(err).ShouldNot(HaveOccurred(), "WebSocket 
handshake")
+                       
Expect(resp.StatusCode).Should(Equal(http.StatusSwitchingProtocols))
+
+                       defer func() {
+                               _ = conn.Close()
+                       }()
+
+                       By("send and receive message through WebSocket")
+                       testMessage := "hello, this is APISIX"
+                       err = conn.WriteMessage(websocket.TextMessage, 
[]byte(testMessage))
+                       Expect(err).ShouldNot(HaveOccurred(), "writing 
WebSocket message")
+
+                       // Then our echo
+                       _, msg, err := conn.ReadMessage()
+                       Expect(err).ShouldNot(HaveOccurred(), "reading echo 
message")
+                       Expect(string(msg)).To(Equal(testMessage), "message 
content verification")
+               })
+       })
+
        PContext("GatewayProxy reference Secret", func() {
                const secretSpec = `
 apiVersion: v1


Reply via email to