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 33d376f1 feat: support port-based routing for Gateway API routes 
(#2703)
33d376f1 is described below

commit 33d376f16453e33b8d269f232265c1199833a02d
Author: Johannes Engler <[email protected]>
AuthorDate: Wed May 27 02:25:29 2026 +0200

    feat: support port-based routing for Gateway API routes (#2703)
---
 .github/workflows/benchmark-test.yml           |   1 +
 Makefile                                       |   4 +-
 config/samples/config.yaml                     |   5 +
 docs/en/latest/concepts/gateway-api.md         |   2 +-
 docs/en/latest/reference/configuration-file.md |   6 +
 docs/en/latest/reference/example.md            |   2 +-
 internal/adc/translator/annotations_test.go    | 249 +++++++++++-
 internal/adc/translator/apisixconsumer_test.go |   2 +-
 internal/adc/translator/apisixroute_test.go    |   6 +-
 internal/adc/translator/consumer_test.go       |   2 +-
 internal/adc/translator/grpcroute.go           |  13 +
 internal/adc/translator/grpcroute_test.go      | 215 ++++++++++
 internal/adc/translator/httproute.go           | 305 ++++++++------
 internal/adc/translator/httproute_test.go      | 199 ++++++++-
 internal/adc/translator/translator.go          |  61 ++-
 internal/controller/config/config.go           |  12 +-
 internal/controller/config/config_test.go      |  78 ++++
 internal/controller/config/types.go            |  37 +-
 internal/controller/context.go                 |   1 +
 internal/controller/grpcroute_controller.go    |   9 +-
 internal/controller/httproute_controller.go    |   8 +
 internal/controller/utils.go                   |  94 +++--
 internal/controller/utils_hostname_test.go     | 251 ++++++++++++
 internal/manager/run.go                        |   7 +-
 internal/provider/apisix/provider.go           |   2 +-
 internal/provider/options.go                   |   6 +
 internal/webhook/v1/adc_validation.go          |   2 +-
 test/e2e/framework/manifests/apisix.yaml       |  12 +-
 test/e2e/framework/manifests/ingress.yaml      |   2 +-
 test/e2e/gatewayapi/grpcroute.go               | 157 ++++++++
 test/e2e/gatewayapi/httproute.go               | 536 +++++++++++++++++++++++++
 test/e2e/scaffold/grpc.go                      |  19 +-
 test/e2e/scaffold/k8s.go                       |  30 +-
 test/e2e/scaffold/scaffold.go                  |  41 ++
 34 files changed, 2177 insertions(+), 199 deletions(-)

diff --git a/.github/workflows/benchmark-test.yml 
b/.github/workflows/benchmark-test.yml
index 618ea96c..05834508 100644
--- a/.github/workflows/benchmark-test.yml
+++ b/.github/workflows/benchmark-test.yml
@@ -109,5 +109,6 @@ jobs:
           PROVIDER_TYPE: ${{ matrix.provider_type }}
           TEST_LABEL: ${{ matrix.cases_subset }}
           TEST_ENV: CI
+          E2E_EXEC_ADC_TIMEOUT: "30s"
         run: |
           make benchmark-test
diff --git a/Makefile b/Makefile
index 4249a3a4..4a617800 100644
--- a/Makefile
+++ b/Makefile
@@ -185,9 +185,10 @@ kind-down:
 
 .PHONY: kind-load-images
 kind-load-images: pull-infra-images kind-load-ingress-image kind-load-adc-image
-       @kind load docker-image kennethreitz/httpbin:latest --name $(KIND_NAME) 
+       @kind load docker-image kennethreitz/httpbin:latest --name $(KIND_NAME)
        @kind load docker-image jmalloc/echo-server:latest --name $(KIND_NAME)
        @kind load docker-image openresty/openresty:1.27.1.2-4-bullseye-fat 
--name $(KIND_NAME)
+       @kind load docker-image alpine/curl:8.17.0 --name $(KIND_NAME)
 
 .PHONY: kind-load-ingress-image
 kind-load-ingress-image:
@@ -204,6 +205,7 @@ pull-infra-images:
        @docker pull kennethreitz/httpbin:latest
        @docker pull jmalloc/echo-server:latest
        @docker pull openresty/openresty:1.27.1.2-4-bullseye-fat
+       @docker pull alpine/curl:8.17.0
 
 ##@ Build
 
diff --git a/config/samples/config.yaml b/config/samples/config.yaml
index 8e371922..b9b9384b 100644
--- a/config/samples/config.yaml
+++ b/config/samples/config.yaml
@@ -34,6 +34,11 @@ exec_adc_timeout: 15s                   # The timeout for 
the ADC to execute.
                                         # The default value is 15 seconds.
 disable_gateway_api: false              # Whether to disable the Gateway API 
support.
                                         # The default value is false.
+listener_port_match_mode: "auto"        # Mode for injecting server_port route 
vars from Gateway listener ports.
+                                        # - "auto": inject when parentRefs 
explicitly target listeners (sectionName/port) or when multiple listener ports 
are matched.
+                                        # - "explicit": inject only when 
parentRefs explicitly target listeners.
+                                        # - "off": never inject server_port 
vars.
+                                        # The default value is "auto".
 
 provider:
   type: "apisix"                        # Provider type.
diff --git a/docs/en/latest/concepts/gateway-api.md 
b/docs/en/latest/concepts/gateway-api.md
index cefdde19..9b8ad48d 100644
--- a/docs/en/latest/concepts/gateway-api.md
+++ b/docs/en/latest/concepts/gateway-api.md
@@ -78,7 +78,7 @@ The fields below are specified in the Gateway API 
specification but are either p
 
 | Fields                                               | Status               
| Notes                                                                         
                 |
 
|------------------------------------------------------|----------------------|------------------------------------------------------------------------------------------------|
-| `spec.listeners[].port`               | Not supported*  | The configuration 
is required but ignored. This is due to limitations in the data plane: it 
cannot dynamically open new ports. Since the Ingress Controller does not manage 
the data plane deployment, it cannot automatically update the configuration or 
restart the data plane to apply port changes.    |
+| `spec.listeners[].port`               | Partially supported | Controls 
`server_port` route-var injection; behaviour is configured via 
[`listener_port_match_mode`](../reference/configuration-file.md) (`auto` / 
`explicit` / `off`). The controller cannot dynamically open data plane ports, 
so APISIX must already listen on the specified port. |
 | `spec.listeners[].tls.certificateRefs[].group` | Partially supported | Only 
`""` is supported; other group values cause validation failure. |
 | `spec.listeners[].tls.certificateRefs[].kind`        | Partially supported  
| Only `Secret` is supported.                                                   
                 |
 | `spec.listeners[].tls.mode`                          | Partially supported  
| `Terminate` is implemented; `Passthrough` is effectively unsupported for 
Gateway listeners.    |
diff --git a/docs/en/latest/reference/configuration-file.md 
b/docs/en/latest/reference/configuration-file.md
index b01b0294..166570ab 100644
--- a/docs/en/latest/reference/configuration-file.md
+++ b/docs/en/latest/reference/configuration-file.md
@@ -63,6 +63,12 @@ secure_metrics: false                   # The secure metrics 
configuration.
 exec_adc_timeout: 15s                   # The timeout for the ADC to execute.
                                         # The default value is 15 seconds.
 
+listener_port_match_mode: "auto"        # Mode for injecting server_port route 
vars from Gateway listener ports.
+                                        # - "auto": inject when parentRefs 
explicitly target listeners (sectionName/port) or when multiple listener ports 
are matched.
+                                        # - "explicit": inject only when 
parentRefs explicitly target listeners.
+                                        # - "off": never inject server_port 
vars.
+                                        # The default value is "auto".
+
 provider:
   type: "apisix"                        # Provider type.
                                         # Value can be "apisix" or 
"apisix-standalone".
diff --git a/docs/en/latest/reference/example.md 
b/docs/en/latest/reference/example.md
index a9d7a9ae..ca796ee4 100644
--- a/docs/en/latest/reference/example.md
+++ b/docs/en/latest/reference/example.md
@@ -96,7 +96,7 @@ spec:
 
 ❶ The controller name should be customized if you are running multiple 
distinct instances of the APISIX Ingress Controller in the same cluster (not a 
single instance with multiple replicas). Each ingress controller instance must 
use a unique controllerName in its [configuration file](configuration-file.md), 
and the corresponding GatewayClass should reference that value.
 
-❷ The `port` in the Gateway listener is required but ignored. This is due to 
limitations in the data plane: it cannot dynamically open new ports. Since the 
Ingress Controller does not manage the data plane deployment, it cannot 
automatically update the configuration or restart the data plane to apply port 
changes.
+❷ The `port` in the Gateway listener is used for routing matching based on 
`listener_port_match_mode` in the controller configuration (`auto`, `explicit`, 
or `off`). The controller cannot dynamically open new ports on the data plane, 
so ensure APISIX is configured to listen on the port.
 
 ❸ API group of the referenced resource.
 
diff --git a/internal/adc/translator/annotations_test.go 
b/internal/adc/translator/annotations_test.go
index 8c8b1b96..7b4d1ec4 100644
--- a/internal/adc/translator/annotations_test.go
+++ b/internal/adc/translator/annotations_test.go
@@ -21,10 +21,13 @@ import (
 
        "github.com/incubator4/go-resty-expr/expr"
        "github.com/stretchr/testify/assert"
+       "k8s.io/utils/ptr"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 
        adctypes "github.com/apache/apisix-ingress-controller/api/adc"
        
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
        
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
 )
 
 type mockParser struct {
@@ -342,7 +345,7 @@ func TestTranslateIngressAnnotations(t *testing.T) {
 
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
-                       translator := &Translator{}
+                       translator := &Translator{ListenerPortMatchMode: 
config.ListenerPortMatchModeAuto}
                        result := 
translator.TranslateIngressAnnotations(tt.anno)
 
                        assert.NotNil(t, result)
@@ -350,3 +353,247 @@ func TestTranslateIngressAnnotations(t *testing.T) {
                })
        }
 }
+
+func TestAddServerPortVars(t *testing.T) {
+       tests := []struct {
+               name     string
+               route    *adctypes.Route
+               ports    map[int32]struct{}
+               expected adctypes.Vars
+       }{
+               {
+                       name:     "empty ports map - no vars added",
+                       route:    &adctypes.Route{},
+                       ports:    map[int32]struct{}{},
+                       expected: adctypes.Vars(nil),
+               },
+               {
+                       name:  "single port - uses == operator",
+                       route: &adctypes.Route{},
+                       ports: map[int32]struct{}{
+                               9080: {},
+                       },
+                       expected: adctypes.Vars{
+                               {
+                                       {StrVal: "server_port"},
+                                       {StrVal: "=="},
+                                       {StrVal: "9080"},
+                               },
+                       },
+               },
+               {
+                       name:  "two ports - uses 'in' operator",
+                       route: &adctypes.Route{},
+                       ports: map[int32]struct{}{
+                               9080: {},
+                               9081: {},
+                       },
+                       expected: adctypes.Vars{
+                               {
+                                       {StrVal: "server_port"},
+                                       {StrVal: "in"},
+                                       {SliceVal: []adctypes.StringOrSlice{
+                                               {StrVal: "9080"},
+                                               {StrVal: "9081"},
+                                       }},
+                               },
+                       },
+               },
+               {
+                       name:  "three ports - uses 'in' operator",
+                       route: &adctypes.Route{},
+                       ports: map[int32]struct{}{
+                               80:   {},
+                               443:  {},
+                               9080: {},
+                       },
+                       expected: adctypes.Vars{
+                               {
+                                       {StrVal: "server_port"},
+                                       {StrVal: "in"},
+                                       {SliceVal: []adctypes.StringOrSlice{
+                                               {StrVal: "80"},
+                                               {StrVal: "443"},
+                                               {StrVal: "9080"},
+                                       }},
+                               },
+                       },
+               },
+               {
+                       name: "vars are appended - preserves existing vars",
+                       route: &adctypes.Route{
+                               Vars: adctypes.Vars{
+                                       {
+                                               {StrVal: "uri"},
+                                               {StrVal: "~~"},
+                                               {StrVal: "^/api"},
+                                       },
+                               },
+                       },
+                       ports: map[int32]struct{}{
+                               9080: {},
+                       },
+                       expected: adctypes.Vars{
+                               {
+                                       {StrVal: "uri"},
+                                       {StrVal: "~~"},
+                                       {StrVal: "^/api"},
+                               },
+                               {
+                                       {StrVal: "server_port"},
+                                       {StrVal: "=="},
+                                       {StrVal: "9080"},
+                               },
+                       },
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       addServerPortVars(tt.route, tt.ports)
+                       assert.Equal(t, tt.expected, tt.route.Vars)
+               })
+       }
+}
+
+func TestShouldInjectServerPortVars(t *testing.T) {
+       sectionName := gatewayv1.SectionName("http-main")
+       port := gatewayv1.PortNumber(9080)
+
+       tests := []struct {
+               name       string
+               mode       config.ListenerPortMatchMode
+               parentRefs []gatewayv1.ParentReference
+               ports      map[int32]struct{}
+               expected   bool
+       }{
+               {
+                       name:     "empty listener ports",
+                       mode:     config.ListenerPortMatchModeAuto,
+                       ports:    map[int32]struct{}{},
+                       expected: false,
+               },
+               {
+                       name: "single port without sectionName",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                       },
+                       ports: map[int32]struct{}{
+                               9080: {},
+                       },
+                       expected: false,
+               },
+               {
+                       name: "single port with sectionName",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", SectionName: &sectionName},
+                       },
+                       ports: map[int32]struct{}{
+                               9080: {},
+                       },
+                       expected: true,
+               },
+               {
+                       name: "multiple ports without sectionName",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                       },
+                       ports: map[int32]struct{}{
+                               9080: {},
+                               9081: {},
+                       },
+                       expected: true,
+               },
+               {
+                       name: "explicit mode with multiple ports and no 
explicit target",
+                       mode: config.ListenerPortMatchModeExplicit,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                       },
+                       ports: map[int32]struct{}{
+                               9080: {},
+                               9081: {},
+                       },
+                       expected: false,
+               },
+               {
+                       name: "explicit mode with parentRef.port",
+                       mode: config.ListenerPortMatchModeExplicit,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", Port: &port},
+                       },
+                       ports: map[int32]struct{}{
+                               9080: {},
+                       },
+                       expected: true,
+               },
+               {
+                       name: "explicit mode with single port and no explicit 
target",
+                       mode: config.ListenerPortMatchModeExplicit,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                       },
+                       ports: map[int32]struct{}{
+                               9080: {},
+                       },
+                       expected: false,
+               },
+               {
+                       name: "off mode ignores explicit target",
+                       mode: config.ListenerPortMatchModeOff,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", SectionName: &sectionName},
+                       },
+                       ports: map[int32]struct{}{
+                               9080: {},
+                               9081: {},
+                       },
+                       expected: false,
+               },
+               {
+                       name: "off mode ignores explicit parentRef.port target",
+                       mode: config.ListenerPortMatchModeOff,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", Port: &port},
+                       },
+                       ports: map[int32]struct{}{
+                               9080: {},
+                       },
+                       expected: false,
+               },
+               {
+                       name: "explicit mode: non-Gateway parentRef with port 
is not treated as explicit target",
+                       mode: config.ListenerPortMatchModeExplicit,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                               {Name: "svc", Kind: 
ptr.To(gatewayv1.Kind("Service")), Port: &port},
+                       },
+                       ports: map[int32]struct{}{
+                               9080: {},
+                       },
+                       expected: false,
+               },
+               {
+                       name: "auto mode: non-Gateway parentRef with port does 
not trigger single-port injection",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                               {Name: "svc", Kind: 
ptr.To(gatewayv1.Kind("Service")), Port: &port},
+                       },
+                       ports: map[int32]struct{}{
+                               9080: {},
+                       },
+                       expected: false,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       translator := &Translator{ListenerPortMatchMode: 
tt.mode}
+                       assert.Equal(t, tt.expected, 
translator.shouldInjectServerPortVars(tt.parentRefs, tt.ports))
+               })
+       }
+}
diff --git a/internal/adc/translator/apisixconsumer_test.go 
b/internal/adc/translator/apisixconsumer_test.go
index fe56b1ec..29ec023b 100644
--- a/internal/adc/translator/apisixconsumer_test.go
+++ b/internal/adc/translator/apisixconsumer_test.go
@@ -31,7 +31,7 @@ import (
 )
 
 func 
TestTranslateApisixConsumer_UsesMetadataLabelsWithoutOverwritingControllerLabels(t
 *testing.T) {
-       translator := NewTranslator(logr.Discard())
+       translator := NewTranslator(logr.Discard(), "")
        tctx := provider.NewDefaultTranslateContext(context.Background())
 
        consumer := &apiv2.ApisixConsumer{
diff --git a/internal/adc/translator/apisixroute_test.go 
b/internal/adc/translator/apisixroute_test.go
index 9feda221..df4c9c07 100644
--- a/internal/adc/translator/apisixroute_test.go
+++ b/internal/adc/translator/apisixroute_test.go
@@ -29,7 +29,7 @@ import (
 )
 
 func TestBuildRoute_HostsNotSet(t *testing.T) {
-       translator := NewTranslator(logr.Discard())
+       translator := NewTranslator(logr.Discard(), "")
 
        ar := &apiv2.ApisixRoute{
                ObjectMeta: metav1.ObjectMeta{
@@ -60,7 +60,7 @@ func TestBuildRoute_HostsNotSet(t *testing.T) {
 }
 
 func TestBuildService_HostsSet(t *testing.T) {
-       translator := NewTranslator(logr.Discard())
+       translator := NewTranslator(logr.Discard(), "")
 
        ar := &apiv2.ApisixRoute{
                ObjectMeta: metav1.ObjectMeta{
@@ -84,7 +84,7 @@ func TestBuildService_HostsSet(t *testing.T) {
 }
 
 func TestBuildRoute_MetadataLabelsDoNotOverwriteControllerLabels(t *testing.T) 
{
-       translator := NewTranslator(logr.Discard())
+       translator := NewTranslator(logr.Discard(), "")
 
        ar := &apiv2.ApisixRoute{
                TypeMeta: metav1.TypeMeta{
diff --git a/internal/adc/translator/consumer_test.go 
b/internal/adc/translator/consumer_test.go
index cd572d87..6e8af842 100644
--- a/internal/adc/translator/consumer_test.go
+++ b/internal/adc/translator/consumer_test.go
@@ -31,7 +31,7 @@ import (
 )
 
 func 
TestTranslateConsumerV1alpha1_UsesMetadataLabelsWithoutOverwritingControllerLabels(t
 *testing.T) {
-       translator := NewTranslator(logr.Discard())
+       translator := NewTranslator(logr.Discard(), "")
        tctx := provider.NewDefaultTranslateContext(context.Background())
 
        consumer := &v1alpha1.Consumer{
diff --git a/internal/adc/translator/grpcroute.go 
b/internal/adc/translator/grpcroute.go
index abe6dfab..631b34d5 100644
--- a/internal/adc/translator/grpcroute.go
+++ b/internal/adc/translator/grpcroute.go
@@ -308,6 +308,19 @@ func (t *Translator) TranslateGRPCRoute(tctx 
*provider.TranslateContext, grpcRou
 
                        routes = append(routes, route)
                }
+
+               // Collect unique listener ports for port-based routing.
+               listenerPorts := make(map[int32]struct{})
+               for _, listener := range tctx.Listeners {
+                       listenerPorts[int32(listener.Port)] = struct{}{}
+               }
+
+               if t.shouldInjectServerPortVars(tctx.RouteParentRefs, 
listenerPorts) {
+                       for _, route := range routes {
+                               addServerPortVars(route, listenerPorts)
+                       }
+               }
+
                service.Routes = routes
 
                result.Services = append(result.Services, service)
diff --git a/internal/adc/translator/grpcroute_test.go 
b/internal/adc/translator/grpcroute_test.go
new file mode 100644
index 00000000..df95a35a
--- /dev/null
+++ b/internal/adc/translator/grpcroute_test.go
@@ -0,0 +1,215 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 translator
+
+import (
+       "context"
+       "testing"
+
+       "github.com/go-logr/logr"
+       "github.com/stretchr/testify/assert"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+       adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
+)
+
+func TestTranslateGRPCRouteServerPortVarsByMode(t *testing.T) {
+       sectionName := gatewayv1.SectionName("grpc-main")
+       parentPort := gatewayv1.PortNumber(9080)
+
+       singlePortVars := adctypes.Vars{
+               {
+                       {StrVal: "server_port"},
+                       {StrVal: "=="},
+                       {StrVal: "9080"},
+               },
+       }
+       multiPortVars := adctypes.Vars{
+               {
+                       {StrVal: "server_port"},
+                       {StrVal: "in"},
+                       {SliceVal: []adctypes.StringOrSlice{
+                               {StrVal: "9080"},
+                               {StrVal: "9081"},
+                       }},
+               },
+       }
+
+       tests := []struct {
+               name       string
+               mode       config.ListenerPortMatchMode
+               parentRefs []gatewayv1.ParentReference
+               listeners  []gatewayv1.Listener
+               expected   adctypes.Vars
+       }{
+               {
+                       name: "auto mode: no injection for single listener 
without explicit target",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "grpc-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: nil,
+               },
+               {
+                       name: "auto mode: inject for sectionName target",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", SectionName: &sectionName},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "grpc-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: singlePortVars,
+               },
+               {
+                       name: "auto mode: inject for port target",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", Port: &parentPort},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "grpc-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: singlePortVars,
+               },
+               {
+                       name: "auto mode: inject for multiple listener ports",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "grpc-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
+                               {Name: "grpc-alt", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: multiPortVars,
+               },
+               {
+                       name: "auto mode: inject for multiple listener ports 
when listener names collide across gateways",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw-a"},
+                               {Name: "gw-b"},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "http", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
+                               {Name: "http", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: multiPortVars,
+               },
+               {
+                       name: "explicit mode: inject for sectionName target",
+                       mode: config.ListenerPortMatchModeExplicit,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", SectionName: &sectionName},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "grpc-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: singlePortVars,
+               },
+               {
+                       name: "explicit mode: inject for port target",
+                       mode: config.ListenerPortMatchModeExplicit,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", Port: &parentPort},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "grpc-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: singlePortVars,
+               },
+               {
+                       name: "explicit mode: no injection for multiple 
listener ports without explicit target",
+                       mode: config.ListenerPortMatchModeExplicit,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "grpc-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
+                               {Name: "grpc-alt", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: nil,
+               },
+               {
+                       name: "off mode: no injection even with sectionName 
target",
+                       mode: config.ListenerPortMatchModeOff,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", SectionName: &sectionName},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "grpc-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: nil,
+               },
+               {
+                       name: "off mode: no injection for multiple listener 
ports",
+                       mode: config.ListenerPortMatchModeOff,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "grpc-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
+                               {Name: "grpc-alt", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: nil,
+               },
+               {
+                       name: "empty mode normalizes to auto",
+                       mode: "",
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", Port: &parentPort},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "grpc-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: singlePortVars,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       tctx := 
provider.NewDefaultTranslateContext(context.Background())
+                       tctx.RouteParentRefs = tt.parentRefs
+                       tctx.Listeners = tt.listeners
+
+                       grpcRoute := &gatewayv1.GRPCRoute{
+                               ObjectMeta: metav1.ObjectMeta{
+                                       Name:      "route",
+                                       Namespace: "default",
+                               },
+                               Spec: gatewayv1.GRPCRouteSpec{
+                                       Rules: []gatewayv1.GRPCRouteRule{
+                                               {},
+                                       },
+                               },
+                       }
+
+                       translator := NewTranslator(logr.Discard(), tt.mode)
+                       got, err := translator.TranslateGRPCRoute(tctx, 
grpcRoute)
+                       assert.NoError(t, err)
+                       if assert.Len(t, got.Services, 1) && assert.Len(t, 
got.Services[0].Routes, 1) {
+                               assert.Equal(t, tt.expected, 
got.Services[0].Routes[0].Vars)
+                       }
+               })
+       }
+}
diff --git a/internal/adc/translator/httproute.go 
b/internal/adc/translator/httproute.go
index fb7f23bf..83c728b0 100644
--- a/internal/adc/translator/httproute.go
+++ b/internal/adc/translator/httproute.go
@@ -20,6 +20,7 @@ package translator
 import (
        "encoding/json"
        "fmt"
+       "sort"
        "strings"
 
        "github.com/pkg/errors"
@@ -524,6 +525,135 @@ func calculateHTTPRoutePriority(match 
*gatewayv1.HTTPRouteMatch, ruleIndex int,
        return priority
 }
 
+// translateBackendsToUpstreams processes the BackendRefs of an HTTPRouteRule,
+// builds upstreams, assigns them to the service (single upstream or 
traffic-split
+// plugin for multiple), and injects fault-injection on backend errors.
+func (t *Translator) translateBackendsToUpstreams(
+       tctx *provider.TranslateContext,
+       rule gatewayv1.HTTPRouteRule,
+       httpRoute *gatewayv1.HTTPRoute,
+       service *adctypes.Service,
+) (enableWebsocket *bool, backendErr error) {
+       upstreams := make([]*adctypes.Upstream, 0)
+       weightedUpstreams := 
make([]adctypes.TrafficSplitConfigRuleWeightedUpstream, 0)
+
+       for _, backend := range rule.BackendRefs {
+               if backend.Namespace == nil {
+                       namespace := gatewayv1.Namespace(httpRoute.Namespace)
+                       backend.Namespace = &namespace
+               }
+               upstream := adctypes.NewDefaultUpstream()
+               upNodes, protocol, err := t.translateBackendRef(tctx, 
backend.BackendRef, DefaultEndpointFilter)
+               if err != nil {
+                       backendErr = err
+                       continue
+               }
+               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
+               if upstream.Scheme == "" {
+                       upstream.Scheme = appProtocolToUpstreamScheme(protocol)
+               }
+               var (
+                       kind string
+                       port int32
+               )
+               if backend.Kind == nil {
+                       kind = internaltypes.KindService
+               } else {
+                       kind = string(*backend.Kind)
+               }
+               if backend.Port != nil {
+                       port = int32(*backend.Port)
+               }
+               namespace := string(*backend.Namespace)
+               name := string(backend.Name)
+               upstreamName := adctypes.ComposeUpstreamNameForBackendRef(kind, 
namespace, name, port)
+               upstream.Name = upstreamName
+               upstream.ID = id.GenID(upstreamName)
+               upstreams = append(upstreams, upstream)
+       }
+
+       // Handle multiple backends with traffic-split plugin
+       if len(upstreams) == 0 {
+               // Create a default upstream if no valid backends
+               service.Upstream = adctypes.NewDefaultUpstream()
+       } else if len(upstreams) == 1 {
+               // Single backend - use directly as service upstream
+               service.Upstream = upstreams[0]
+               // remove the id and name of the service.upstream, adc schema 
does not need id and name for it
+               service.Upstream.ID = ""
+               service.Upstream.Name = ""
+       } else {
+               // Multiple backends - use traffic-split plugin
+               service.Upstream = upstreams[0]
+               // remove the id and name of the service.upstream, adc schema 
does not need id and name for it
+               service.Upstream.ID = ""
+               service.Upstream.Name = ""
+
+               upstreams = upstreams[1:]
+
+               if len(upstreams) > 0 {
+                       service.Upstreams = upstreams
+               }
+
+               // Set weight in traffic-split for the default upstream
+               weight := apiv2.DefaultWeight
+               if rule.BackendRefs[0].Weight != nil {
+                       weight = int(*rule.BackendRefs[0].Weight)
+               }
+               weightedUpstreams = append(weightedUpstreams, 
adctypes.TrafficSplitConfigRuleWeightedUpstream{
+                       Weight: weight,
+               })
+
+               // Set other upstreams in traffic-split using upstream_id
+               for i, upstream := range upstreams {
+                       weight := apiv2.DefaultWeight
+                       // get weight from the backend refs starting from the 
second backend
+                       if i+1 < len(rule.BackendRefs) && 
rule.BackendRefs[i+1].Weight != nil {
+                               weight = int(*rule.BackendRefs[i+1].Weight)
+                       }
+                       weightedUpstreams = append(weightedUpstreams, 
adctypes.TrafficSplitConfigRuleWeightedUpstream{
+                               UpstreamID: upstream.ID,
+                               Weight:     weight,
+                       })
+               }
+
+               if len(weightedUpstreams) > 0 {
+                       if service.Plugins == nil {
+                               service.Plugins = make(map[string]any)
+                       }
+                       service.Plugins["traffic-split"] = 
&adctypes.TrafficSplitConfig{
+                               Rules: []adctypes.TrafficSplitConfigRule{
+                                       {
+                                               WeightedUpstreams: 
weightedUpstreams,
+                                       },
+                               },
+                       }
+               }
+       }
+
+       if backendErr != nil && (service.Upstream == nil || 
len(service.Upstream.Nodes) == 0) {
+               if service.Plugins == nil {
+                       service.Plugins = make(map[string]any)
+               }
+               service.Plugins["fault-injection"] = map[string]any{
+                       "abort": map[string]any{
+                               "http_status": 500,
+                               "body":        "No existing backendRef 
provided",
+                       },
+               }
+       }
+
+       return enableWebsocket, backendErr
+}
+
 func (t *Translator) TranslateHTTPRoute(tctx *provider.TranslateContext, 
httpRoute *gatewayv1.HTTPRoute) (*TranslateResult, error) {
        result := &TranslateResult{}
 
@@ -544,127 +674,7 @@ func (t *Translator) TranslateHTTPRoute(tctx 
*provider.TranslateContext, httpRou
                service.ID = id.GenID(service.Name)
                service.Hosts = hosts
 
-               var (
-                       upstreams         = make([]*adctypes.Upstream, 0)
-                       weightedUpstreams = 
make([]adctypes.TrafficSplitConfigRuleWeightedUpstream, 0)
-                       backendErr        error
-                       enableWebsocket   *bool
-               )
-
-               for _, backend := range rule.BackendRefs {
-                       if backend.Namespace == nil {
-                               namespace := 
gatewayv1.Namespace(httpRoute.Namespace)
-                               backend.Namespace = &namespace
-                       }
-                       upstream := adctypes.NewDefaultUpstream()
-                       upNodes, protocol, err := t.translateBackendRef(tctx, 
backend.BackendRef, DefaultEndpointFilter)
-                       if err != nil {
-                               backendErr = err
-                               continue
-                       }
-                       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
-                       if upstream.Scheme == "" {
-                               upstream.Scheme = 
appProtocolToUpstreamScheme(protocol)
-                       }
-                       var (
-                               kind string
-                               port int32
-                       )
-                       if backend.Kind == nil {
-                               kind = internaltypes.KindService
-                       } else {
-                               kind = string(*backend.Kind)
-                       }
-                       if backend.Port != nil {
-                               port = int32(*backend.Port)
-                       }
-                       namespace := string(*backend.Namespace)
-                       name := string(backend.Name)
-                       upstreamName := 
adctypes.ComposeUpstreamNameForBackendRef(kind, namespace, name, port)
-                       upstream.Name = upstreamName
-                       upstream.ID = id.GenID(upstreamName)
-                       upstreams = append(upstreams, upstream)
-               }
-
-               // Handle multiple backends with traffic-split plugin
-               if len(upstreams) == 0 {
-                       // Create a default upstream if no valid backends
-                       upstream := adctypes.NewDefaultUpstream()
-                       service.Upstream = upstream
-               } else if len(upstreams) == 1 {
-                       // Single backend - use directly as service upstream
-                       service.Upstream = upstreams[0]
-                       // remove the id and name of the service.upstream, adc 
schema does not need id and name for it
-                       service.Upstream.ID = ""
-                       service.Upstream.Name = ""
-               } else {
-                       // Multiple backends - use traffic-split plugin
-                       service.Upstream = upstreams[0]
-                       // remove the id and name of the service.upstream, adc 
schema does not need id and name for it
-                       service.Upstream.ID = ""
-                       service.Upstream.Name = ""
-
-                       upstreams = upstreams[1:]
-
-                       if len(upstreams) > 0 {
-                               service.Upstreams = upstreams
-                       }
-
-                       // Set weight in traffic-split for the default upstream
-                       weight := apiv2.DefaultWeight
-                       if rule.BackendRefs[0].Weight != nil {
-                               weight = int(*rule.BackendRefs[0].Weight)
-                       }
-                       weightedUpstreams = append(weightedUpstreams, 
adctypes.TrafficSplitConfigRuleWeightedUpstream{
-                               Weight: weight,
-                       })
-
-                       // Set other upstreams in traffic-split using 
upstream_id
-                       for i, upstream := range upstreams {
-                               weight := apiv2.DefaultWeight
-                               // get weight from the backend refs starting 
from the second backend
-                               if i+1 < len(rule.BackendRefs) && 
rule.BackendRefs[i+1].Weight != nil {
-                                       weight = 
int(*rule.BackendRefs[i+1].Weight)
-                               }
-                               weightedUpstreams = append(weightedUpstreams, 
adctypes.TrafficSplitConfigRuleWeightedUpstream{
-                                       UpstreamID: upstream.ID,
-                                       Weight:     weight,
-                               })
-                       }
-
-                       if len(weightedUpstreams) > 0 {
-                               if service.Plugins == nil {
-                                       service.Plugins = make(map[string]any)
-                               }
-                               service.Plugins["traffic-split"] = 
&adctypes.TrafficSplitConfig{
-                                       Rules: 
[]adctypes.TrafficSplitConfigRule{
-                                               {
-                                                       WeightedUpstreams: 
weightedUpstreams,
-                                               },
-                                       },
-                               }
-                       }
-               }
-
-               if backendErr != nil && (service.Upstream == nil || 
len(service.Upstream.Nodes) == 0) {
-                       if service.Plugins == nil {
-                               service.Plugins = make(map[string]any)
-                       }
-                       service.Plugins["fault-injection"] = map[string]any{
-                               "abort": map[string]any{
-                                       "http_status": 500,
-                                       "body":        "No existing backendRef 
provided",
-                               },
-                       }
-               }
+               enableWebsocket, _ := t.translateBackendsToUpstreams(tctx, 
rule, httpRoute, service)
 
                t.fillPluginsFromHTTPRouteFilters(service.Plugins, 
httpRoute.GetNamespace(), rule.Filters, rule.Matches, tctx)
 
@@ -701,6 +711,21 @@ func (t *Translator) TranslateHTTPRoute(tctx 
*provider.TranslateContext, httpRou
 
                        routes = append(routes, route)
                }
+
+               // Collect unique listener ports for port-based routing
+               listenerPorts := make(map[int32]struct{})
+               for _, listener := range tctx.Listeners {
+                       listenerPorts[int32(listener.Port)] = struct{}{}
+               }
+
+               // Add server_port matching only when a route explicitly 
targets a listener
+               // or when multiple listener ports need to be disambiguated.
+               if t.shouldInjectServerPortVars(tctx.RouteParentRefs, 
listenerPorts) {
+                       for _, route := range routes {
+                               addServerPortVars(route, listenerPorts)
+                       }
+               }
+
                t.fillHTTPRoutePoliciesForHTTPRoute(tctx, routes, rule)
                service.Routes = routes
 
@@ -854,3 +879,41 @@ func appProtocolToUpstreamScheme(appProtocol string) 
string {
                return ""
        }
 }
+
+func addServerPortVars(route *adctypes.Route, ports map[int32]struct{}) {
+       if len(ports) == 0 {
+               return
+       }
+
+       // For single port, use exact match
+       if len(ports) == 1 {
+               for port := range ports {
+                       portVar := []adctypes.StringOrSlice{
+                               {StrVal: "server_port"},
+                               {StrVal: "=="},
+                               {StrVal: fmt.Sprintf("%d", port)},
+                       }
+                       route.Vars = append(route.Vars, portVar)
+                       return
+               }
+       }
+
+       // For multiple ports, use "in" operator
+       // Sort ports for deterministic output
+       sortedPorts := make([]int, 0, len(ports))
+       for port := range ports {
+               sortedPorts = append(sortedPorts, int(port))
+       }
+       sort.Ints(sortedPorts)
+
+       portList := make([]adctypes.StringOrSlice, 0, len(ports))
+       for _, port := range sortedPorts {
+               portList = append(portList, adctypes.StringOrSlice{StrVal: 
fmt.Sprintf("%d", port)})
+       }
+       portVar := []adctypes.StringOrSlice{
+               {StrVal: "server_port"},
+               {StrVal: "in"},
+               {SliceVal: portList},
+       }
+       route.Vars = append(route.Vars, portVar)
+}
diff --git a/internal/adc/translator/httproute_test.go 
b/internal/adc/translator/httproute_test.go
index 7b11e129..f26831b0 100644
--- a/internal/adc/translator/httproute_test.go
+++ b/internal/adc/translator/httproute_test.go
@@ -36,10 +36,207 @@ import (
        adctypes "github.com/apache/apisix-ingress-controller/api/adc"
        "github.com/apache/apisix-ingress-controller/api/v1alpha1"
        apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
        "github.com/apache/apisix-ingress-controller/internal/provider"
        internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
 )
 
+func TestTranslateHTTPRouteServerPortVarsByMode(t *testing.T) {
+       sectionName := gatewayv1.SectionName("http-main")
+       parentPort := gatewayv1.PortNumber(9080)
+       pathMatchType := gatewayv1.PathMatchPathPrefix
+       pathValue := "/"
+
+       singlePortVars := adctypes.Vars{
+               {
+                       {StrVal: "server_port"},
+                       {StrVal: "=="},
+                       {StrVal: "9080"},
+               },
+       }
+       multiPortVars := adctypes.Vars{
+               {
+                       {StrVal: "server_port"},
+                       {StrVal: "in"},
+                       {SliceVal: []adctypes.StringOrSlice{
+                               {StrVal: "9080"},
+                               {StrVal: "9081"},
+                       }},
+               },
+       }
+
+       tests := []struct {
+               name       string
+               mode       config.ListenerPortMatchMode
+               parentRefs []gatewayv1.ParentReference
+               listeners  []gatewayv1.Listener
+               expected   adctypes.Vars
+       }{
+               {
+                       name: "auto mode: no injection for single listener 
without explicit target",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "http-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: nil,
+               },
+               {
+                       name: "auto mode: inject for sectionName target",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", SectionName: &sectionName},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "http-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: singlePortVars,
+               },
+               {
+                       name: "auto mode: inject for port target",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", Port: &parentPort},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "http-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: singlePortVars,
+               },
+               {
+                       name: "auto mode: inject for multiple listener ports",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "http-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
+                               {Name: "http-alt", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: multiPortVars,
+               },
+               {
+                       name: "auto mode: inject for multiple listener ports 
when listener names collide across gateways",
+                       mode: config.ListenerPortMatchModeAuto,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw-a"},
+                               {Name: "gw-b"},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "http", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
+                               {Name: "http", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: multiPortVars,
+               },
+               {
+                       name: "explicit mode: inject for sectionName target",
+                       mode: config.ListenerPortMatchModeExplicit,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", SectionName: &sectionName},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "http-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: singlePortVars,
+               },
+               {
+                       name: "explicit mode: inject for port target",
+                       mode: config.ListenerPortMatchModeExplicit,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", Port: &parentPort},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "http-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: singlePortVars,
+               },
+               {
+                       name: "explicit mode: no injection for multiple 
listener ports without explicit target",
+                       mode: config.ListenerPortMatchModeExplicit,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "http-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
+                               {Name: "http-alt", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: nil,
+               },
+               {
+                       name: "off mode: no injection even with sectionName 
target",
+                       mode: config.ListenerPortMatchModeOff,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", SectionName: &sectionName},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "http-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: nil,
+               },
+               {
+                       name: "off mode: no injection for multiple listener 
ports",
+                       mode: config.ListenerPortMatchModeOff,
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw"},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "http-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
+                               {Name: "http-alt", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: nil,
+               },
+               {
+                       name: "empty mode normalizes to auto",
+                       mode: "",
+                       parentRefs: []gatewayv1.ParentReference{
+                               {Name: "gw", Port: &parentPort},
+                       },
+                       listeners: []gatewayv1.Listener{
+                               {Name: "http-main", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
+                       },
+                       expected: singlePortVars,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       tctx := 
provider.NewDefaultTranslateContext(context.Background())
+                       tctx.RouteParentRefs = tt.parentRefs
+                       tctx.Listeners = tt.listeners
+
+                       httpRoute := &gatewayv1.HTTPRoute{
+                               ObjectMeta: metav1.ObjectMeta{
+                                       Name:      "route",
+                                       Namespace: "default",
+                               },
+                               Spec: gatewayv1.HTTPRouteSpec{
+                                       Rules: []gatewayv1.HTTPRouteRule{
+                                               {
+                                                       Matches: 
[]gatewayv1.HTTPRouteMatch{
+                                                               {
+                                                                       Path: 
&gatewayv1.HTTPPathMatch{
+                                                                               
Type:  &pathMatchType,
+                                                                               
Value: &pathValue,
+                                                                       },
+                                                               },
+                                                       },
+                                               },
+                                       },
+                               },
+                       }
+
+                       translator := NewTranslator(logr.Discard(), tt.mode)
+                       got, err := translator.TranslateHTTPRoute(tctx, 
httpRoute)
+                       assert.NoError(t, err)
+                       if assert.Len(t, got.Services, 1) && assert.Len(t, 
got.Services[0].Routes, 1) {
+                               assert.Equal(t, tt.expected, 
got.Services[0].Routes[0].Vars)
+                       }
+               })
+       }
+}
+
 func TestTranslateHTTPRouteUpstreamScheme(t *testing.T) {
        tests := []struct {
                name         string
@@ -62,7 +259,7 @@ func TestTranslateHTTPRouteUpstreamScheme(t *testing.T) {
 
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
-                       translator := NewTranslator(logr.Discard())
+                       translator := NewTranslator(logr.Discard(), "")
                        tctx := 
provider.NewDefaultTranslateContext(context.Background())
 
                        const (
diff --git a/internal/adc/translator/translator.go 
b/internal/adc/translator/translator.go
index aeaef250..e294c7da 100644
--- a/internal/adc/translator/translator.go
+++ b/internal/adc/translator/translator.go
@@ -19,17 +19,72 @@ package translator
 
 import (
        "github.com/go-logr/logr"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 
        adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
 )
 
 type Translator struct {
-       Log logr.Logger
+       Log                   logr.Logger
+       ListenerPortMatchMode config.ListenerPortMatchMode
 }
 
-func NewTranslator(log logr.Logger) *Translator {
+func normalizeMode(mode config.ListenerPortMatchMode) 
config.ListenerPortMatchMode {
+       switch mode {
+       case "", config.ListenerPortMatchModeAuto:
+               return config.ListenerPortMatchModeAuto
+       case config.ListenerPortMatchModeExplicit, 
config.ListenerPortMatchModeOff:
+               return mode
+       default:
+               return config.ListenerPortMatchModeAuto
+       }
+}
+
+func NewTranslator(log logr.Logger, mode config.ListenerPortMatchMode) 
*Translator {
        return &Translator{
-               Log: log.WithName("translator"),
+               Log:                   log.WithName("translator"),
+               ListenerPortMatchMode: normalizeMode(mode),
+       }
+}
+
+func hasExplicitListenerTarget(parentRefs []gatewayv1.ParentReference) bool {
+       for _, parentRef := range parentRefs {
+               // Skip non-Gateway parentRefs (e.g. GAMMA Service mesh refs) — 
they
+               // are not relevant to listener port injection.
+               if parentRef.Kind != nil && *parentRef.Kind != "Gateway" {
+                       continue
+               }
+               if parentRef.SectionName != nil && *parentRef.SectionName != "" 
{
+                       return true
+               }
+               if parentRef.Port != nil {
+                       return true
+               }
+       }
+
+       return false
+}
+
+func (t *Translator) shouldInjectServerPortVars(parentRefs 
[]gatewayv1.ParentReference, ports map[int32]struct{}) bool {
+       if len(ports) == 0 {
+               return false
+       }
+
+       explicit := hasExplicitListenerTarget(parentRefs)
+
+       switch t.ListenerPortMatchMode {
+       case config.ListenerPortMatchModeOff:
+               if explicit {
+                       t.Log.V(1).Info("listener_port_match_mode is 'off'; 
ignoring explicit listener targeting", "parent_refs", len(parentRefs))
+               }
+               return false
+       case config.ListenerPortMatchModeExplicit:
+               return explicit
+       case config.ListenerPortMatchModeAuto:
+               return explicit || len(ports) > 1
+       default:
+               return explicit || len(ports) > 1
        }
 }
 
diff --git a/internal/controller/config/config.go 
b/internal/controller/config/config.go
index 31ec1783..2f885e02 100644
--- a/internal/controller/config/config.go
+++ b/internal/controller/config/config.go
@@ -56,7 +56,8 @@ func NewDefaultConfig() *Config {
                        SyncPeriod:    types.TimeDuration{Duration: 1 * 
time.Hour},
                        InitSyncDelay: types.TimeDuration{Duration: 20 * 
time.Minute},
                },
-               Webhook: NewWebhookConfig(),
+               Webhook:               NewWebhookConfig(),
+               ListenerPortMatchMode: ListenerPortMatchModeAuto,
        }
 }
 
@@ -122,6 +123,15 @@ func (c *Config) Validate() error {
        if c.ControllerName == "" {
                return fmt.Errorf("controller_name is required")
        }
+
+       if c.ListenerPortMatchMode != "" {
+               switch c.ListenerPortMatchMode {
+               case ListenerPortMatchModeAuto, ListenerPortMatchModeExplicit, 
ListenerPortMatchModeOff:
+               default:
+                       return fmt.Errorf("invalid listener_port_match_mode: %q 
(must be auto, explicit, or off)", c.ListenerPortMatchMode)
+               }
+       }
+
        if err := validateProvider(c.ProviderConfig); err != nil {
                return err
        }
diff --git a/internal/controller/config/config_test.go 
b/internal/controller/config/config_test.go
new file mode 100644
index 00000000..97224dbf
--- /dev/null
+++ b/internal/controller/config/config_test.go
@@ -0,0 +1,78 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 config
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestNewDefaultConfigListenerPortMatchMode(t *testing.T) {
+       cfg := NewDefaultConfig()
+       assert.Equal(t, ListenerPortMatchModeAuto, cfg.ListenerPortMatchMode)
+}
+
+func TestConfigValidateListenerPortMatchMode(t *testing.T) {
+       tests := []struct {
+               name      string
+               mode      ListenerPortMatchMode
+               expectErr bool
+       }{
+               {
+                       name:      "default auto",
+                       mode:      ListenerPortMatchModeAuto,
+                       expectErr: false,
+               },
+               {
+                       name:      "explicit",
+                       mode:      ListenerPortMatchModeExplicit,
+                       expectErr: false,
+               },
+               {
+                       name:      "off",
+                       mode:      ListenerPortMatchModeOff,
+                       expectErr: false,
+               },
+               {
+                       name:      "empty mode is allowed",
+                       mode:      "",
+                       expectErr: false,
+               },
+               {
+                       name:      "invalid mode",
+                       mode:      "invalid",
+                       expectErr: true,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       cfg := NewDefaultConfig()
+                       cfg.ListenerPortMatchMode = tt.mode
+
+                       err := cfg.Validate()
+                       if tt.expectErr {
+                               assert.Error(t, err)
+                               assert.ErrorContains(t, err, "invalid 
listener_port_match_mode")
+                       } else {
+                               assert.NoError(t, err)
+                       }
+               })
+       }
+}
diff --git a/internal/controller/config/types.go 
b/internal/controller/config/types.go
index e00ef1f4..7e470d1c 100644
--- a/internal/controller/config/types.go
+++ b/internal/controller/config/types.go
@@ -28,6 +28,14 @@ const (
        ProviderTypeAPISIX     ProviderType = "apisix"
 )
 
+type ListenerPortMatchMode string
+
+const (
+       ListenerPortMatchModeAuto     ListenerPortMatchMode = "auto"
+       ListenerPortMatchModeExplicit ListenerPortMatchMode = "explicit"
+       ListenerPortMatchModeOff      ListenerPortMatchMode = "off"
+)
+
 const (
        // IngressAPISIXLeader is the default election id for the controller
        // leader election.
@@ -55,20 +63,21 @@ const (
 // Config contains all config items which are necessary for
 // apisix-ingress-controller's running.
 type Config struct {
-       LogLevel          string             `json:"log_level" yaml:"log_level"`
-       ControllerName    string             `json:"controller_name" 
yaml:"controller_name"`
-       LeaderElectionID  string             `json:"leader_election_id" 
yaml:"leader_election_id"`
-       MetricsAddr       string             `json:"metrics_addr" 
yaml:"metrics_addr"`
-       ServerAddr        string             `json:"server_addr" 
yaml:"server_addr"`
-       EnableServer      bool               `json:"enable_server" 
yaml:"enable_server"`
-       EnableHTTP2       bool               `json:"enable_http2" 
yaml:"enable_http2"`
-       ProbeAddr         string             `json:"probe_addr" 
yaml:"probe_addr"`
-       SecureMetrics     bool               `json:"secure_metrics" 
yaml:"secure_metrics"`
-       LeaderElection    *LeaderElection    `json:"leader_election" 
yaml:"leader_election"`
-       ExecADCTimeout    types.TimeDuration `json:"exec_adc_timeout" 
yaml:"exec_adc_timeout"`
-       ProviderConfig    ProviderConfig     `json:"provider" yaml:"provider"`
-       Webhook           *WebhookConfig     `json:"webhook" yaml:"webhook"`
-       DisableGatewayAPI bool               `json:"disable_gateway_api" 
yaml:"disable_gateway_api"`
+       LogLevel              string                `json:"log_level" 
yaml:"log_level"`
+       ControllerName        string                `json:"controller_name" 
yaml:"controller_name"`
+       LeaderElectionID      string                `json:"leader_election_id" 
yaml:"leader_election_id"`
+       MetricsAddr           string                `json:"metrics_addr" 
yaml:"metrics_addr"`
+       ServerAddr            string                `json:"server_addr" 
yaml:"server_addr"`
+       EnableServer          bool                  `json:"enable_server" 
yaml:"enable_server"`
+       EnableHTTP2           bool                  `json:"enable_http2" 
yaml:"enable_http2"`
+       ProbeAddr             string                `json:"probe_addr" 
yaml:"probe_addr"`
+       SecureMetrics         bool                  `json:"secure_metrics" 
yaml:"secure_metrics"`
+       LeaderElection        *LeaderElection       `json:"leader_election" 
yaml:"leader_election"`
+       ExecADCTimeout        types.TimeDuration    `json:"exec_adc_timeout" 
yaml:"exec_adc_timeout"`
+       ProviderConfig        ProviderConfig        `json:"provider" 
yaml:"provider"`
+       Webhook               *WebhookConfig        `json:"webhook" 
yaml:"webhook"`
+       DisableGatewayAPI     bool                  `json:"disable_gateway_api" 
yaml:"disable_gateway_api"`
+       ListenerPortMatchMode ListenerPortMatchMode 
`json:"listener_port_match_mode" yaml:"listener_port_match_mode"`
 }
 
 type GatewayConfig struct {
diff --git a/internal/controller/context.go b/internal/controller/context.go
index 5398f044..686dd046 100644
--- a/internal/controller/context.go
+++ b/internal/controller/context.go
@@ -27,6 +27,7 @@ type RouteParentRefContext struct {
 
        ListenerName string
        Listener     *gatewayv1.Listener
+       Listeners    []gatewayv1.Listener
 
        Conditions []metav1.Condition
 }
diff --git a/internal/controller/grpcroute_controller.go 
b/internal/controller/grpcroute_controller.go
index 3b423417..e15e0986 100644
--- a/internal/controller/grpcroute_controller.go
+++ b/internal/controller/grpcroute_controller.go
@@ -200,8 +200,13 @@ func (r *GRPCRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (
                        acceptStatus.status = false
                        acceptStatus.msg = err.Error()
                }
-               if gateway.Listener != nil {
-                       tctx.Listeners = append(tctx.Listeners, 
*gateway.Listener)
+               // Populate listeners for port-based routing
+               // Use Listeners slice if available (multiple listener support)
+               if len(gateway.Listeners) > 0 {
+                       tctx.Listeners = appendListeners(tctx.Listeners, 
gateway.Listeners...)
+               } else if gateway.Listener != nil {
+                       // Fallback for backward compatibility
+                       tctx.Listeners = appendListeners(tctx.Listeners, 
*gateway.Listener)
                }
        }
 
diff --git a/internal/controller/httproute_controller.go 
b/internal/controller/httproute_controller.go
index 34615b9f..c183587e 100644
--- a/internal/controller/httproute_controller.go
+++ b/internal/controller/httproute_controller.go
@@ -183,6 +183,14 @@ func (r *HTTPRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (
                        acceptStatus.status = false
                        acceptStatus.msg = err.Error()
                }
+               // Populate listeners for port-based routing
+               // Use Listeners slice if available (multiple listener support)
+               if len(gateway.Listeners) > 0 {
+                       tctx.Listeners = appendListeners(tctx.Listeners, 
gateway.Listeners...)
+               } else if gateway.Listener != nil {
+                       // Fallback for backward compatibility
+                       tctx.Listeners = appendListeners(tctx.Listeners, 
*gateway.Listener)
+               }
        }
 
        var backendRefErr error
diff --git a/internal/controller/utils.go b/internal/controller/utils.go
index e8a31ab6..9007dea7 100644
--- a/internal/controller/utils.go
+++ b/internal/controller/utils.go
@@ -362,6 +362,10 @@ func ParseRouteParentRefs(
                reason := gatewayv1.RouteReasonNoMatchingParent
                var listenerName string
                var matchedListener gatewayv1.Listener
+               var matchedListeners []gatewayv1.Listener
+
+               // Track if sectionName was explicitly specified
+               sectionNameSpecified := parentRef.SectionName != nil && 
*parentRef.SectionName != ""
 
                for _, listener := range gateway.Spec.Listeners {
                        if parentRef.SectionName != nil {
@@ -385,7 +389,6 @@ func ParseRouteParentRefs(
                                continue
                        }
 
-                       listenerName = string(listener.Name)
                        ok, err := routeMatchesListenerAllowedRoutes(ctx, mgrc, 
route, listener.AllowedRoutes, gateway.Namespace, parentRef.Namespace)
                        if err != nil {
                                log.Error(err, "failed matching listener to a 
route for gateway",
@@ -400,9 +403,23 @@ func ParseRouteParentRefs(
 
                        // TODO: check if the listener status is programmed
 
+                       if sectionNameSpecified {
+                               listenerName = string(listener.Name)
+                       }
+
+                       if !matched {
+                               // First match - store for backward 
compatibility
+                               matchedListener = listener
+                       }
+
+                       // Always add to the list of matched listeners
+                       matchedListeners = append(matchedListeners, listener)
                        matched = true
-                       matchedListener = listener
-                       break
+
+                       // Only break if sectionName was explicitly specified
+                       if sectionNameSpecified {
+                               break
+                       }
                }
 
                if matched {
@@ -410,6 +427,7 @@ func ParseRouteParentRefs(
                                Gateway:      &gateway,
                                ListenerName: listenerName,
                                Listener:     &matchedListener,
+                               Listeners:    matchedListeners,
                                Conditions: []metav1.Condition{{
                                        Type:               
string(gatewayv1.RouteConditionAccepted),
                                        Status:             
metav1.ConditionTrue,
@@ -421,7 +439,8 @@ func ParseRouteParentRefs(
                        gateways = append(gateways, RouteParentRefContext{
                                Gateway:      &gateway,
                                ListenerName: listenerName,
-                               Listener:     &matchedListener,
+                               Listener:     nil,
+                               Listeners:    matchedListeners,
                                Conditions: []metav1.Condition{{
                                        Type:               
string(gatewayv1.RouteConditionAccepted),
                                        Status:             
metav1.ConditionFalse,
@@ -1103,29 +1122,15 @@ func getUnionOfGatewayHostnames(gateways 
[]RouteParentRefContext) ([]gatewayv1.H
        hostnames := make([]gatewayv1.Hostname, 0)
 
        for _, gateway := range gateways {
-               if gateway.ListenerName != "" {
-                       // If a listener name is specified, only check that 
listener
-                       for _, listener := range gateway.Gateway.Spec.Listeners 
{
-                               if string(listener.Name) == 
gateway.ListenerName {
-                                       // If a listener does not specify a 
hostname, it can match any hostname
-                                       if listener.Hostname == nil {
-                                               return nil, true
-                                       }
-                                       hostnames = append(hostnames, 
*listener.Hostname)
-                                       break
-                               }
+               for _, listener := range listenersForGatewayContext(gateway) {
+                       // Only consider listeners that can effectively 
configure hostnames (HTTP, HTTPS, or TLS).
+                       if !isListenerHostnameEffective(listener) {
+                               continue
                        }
-               } else {
-                       // Otherwise, check all listeners
-                       for _, listener := range gateway.Gateway.Spec.Listeners 
{
-                               // Only consider listeners that can effectively 
configure hostnames (HTTP, HTTPS, or TLS)
-                               if isListenerHostnameEffective(listener) {
-                                       if listener.Hostname == nil {
-                                               return nil, true
-                                       }
-                                       hostnames = append(hostnames, 
*listener.Hostname)
-                               }
+                       if listener.Hostname == nil {
+                               return nil, true
                        }
+                       hostnames = append(hostnames, *listener.Hostname)
                }
        }
 
@@ -1140,19 +1145,15 @@ func getUnionOfGatewayHostnames(gateways 
[]RouteParentRefContext) ([]gatewayv1.H
 // - If none of the above, return an empty string
 func getMinimumHostnameIntersection(gateways []RouteParentRefContext, hostname 
gatewayv1.Hostname) gatewayv1.Hostname {
        for _, gateway := range gateways {
-               for _, listener := range gateway.Gateway.Spec.Listeners {
-                       // If a listener name is specified, only check that 
listener
-                       // If the listener name is not specified, check all 
listeners
-                       if gateway.ListenerName == "" || gateway.ListenerName 
== string(listener.Name) {
-                               if listener.Hostname == nil || 
*listener.Hostname == "" {
-                                       return hostname
-                               }
-                               if HostnamesMatch(string(*listener.Hostname), 
string(hostname)) {
-                                       return hostname
-                               }
-                               if HostnamesMatch(string(hostname), 
string(*listener.Hostname)) {
-                                       return *listener.Hostname
-                               }
+               for _, listener := range listenersForGatewayContext(gateway) {
+                       if listener.Hostname == nil || *listener.Hostname == "" 
{
+                               return hostname
+                       }
+                       if HostnamesMatch(string(*listener.Hostname), 
string(hostname)) {
+                               return hostname
+                       }
+                       if HostnamesMatch(string(hostname), 
string(*listener.Hostname)) {
+                               return *listener.Hostname
                        }
                }
        }
@@ -1160,6 +1161,16 @@ func getMinimumHostnameIntersection(gateways 
[]RouteParentRefContext, hostname g
        return ""
 }
 
+func listenersForGatewayContext(gw RouteParentRefContext) []gatewayv1.Listener 
{
+       if len(gw.Listeners) > 0 {
+               return gw.Listeners
+       }
+       if gw.Listener != nil {
+               return []gatewayv1.Listener{*gw.Listener}
+       }
+       return nil
+}
+
 // isListenerHostnameEffective checks if a listener can specify a hostname to 
match the hostname in the request
 // Basically, check if the listener uses HTTP, HTTPS, or TLS protocol
 func isListenerHostnameEffective(listener gatewayv1.Listener) bool {
@@ -1168,6 +1179,13 @@ func isListenerHostnameEffective(listener 
gatewayv1.Listener) bool {
                listener.Protocol == gatewayv1.TLSProtocolType
 }
 
+// appendListeners appends listeners without de-duplication.
+// Route translation aggregates listeners across multiple Gateways, and 
listener
+// names are only unique within a single Gateway.
+func appendListeners(target []gatewayv1.Listener, source 
...gatewayv1.Listener) []gatewayv1.Listener {
+       return append(target, source...)
+}
+
 func isRouteAccepted(gateways []RouteParentRefContext) bool {
        for _, gateway := range gateways {
                for _, condition := range gateway.Conditions {
diff --git a/internal/controller/utils_hostname_test.go 
b/internal/controller/utils_hostname_test.go
new file mode 100644
index 00000000..031b0fe5
--- /dev/null
+++ b/internal/controller/utils_hostname_test.go
@@ -0,0 +1,251 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 controller
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+)
+
+func TestListenersForGatewayContext(t *testing.T) {
+       hostname := gatewayv1.Hostname("example.com")
+       listener := gatewayv1.Listener{
+               Name:     "http",
+               Protocol: gatewayv1.HTTPProtocolType,
+               Port:     gatewayv1.PortNumber(80),
+               Hostname: &hostname,
+       }
+
+       tests := []struct {
+               name     string
+               context  RouteParentRefContext
+               expected []gatewayv1.Listener
+       }{
+               {
+                       name: "prefer listeners slice when present",
+                       context: RouteParentRefContext{
+                               Listeners: []gatewayv1.Listener{
+                                       listener,
+                                       {
+                                               Name:     "https",
+                                               Protocol: 
gatewayv1.HTTPSProtocolType,
+                                               Port:     
gatewayv1.PortNumber(443),
+                                       },
+                               },
+                               Listener: &gatewayv1.Listener{
+                                       Name: "ignored",
+                               },
+                       },
+                       expected: []gatewayv1.Listener{
+                               listener,
+                               {
+                                       Name:     "https",
+                                       Protocol: gatewayv1.HTTPSProtocolType,
+                                       Port:     gatewayv1.PortNumber(443),
+                               },
+                       },
+               },
+               {
+                       name: "fallback to single listener pointer",
+                       context: RouteParentRefContext{
+                               Listener: &listener,
+                       },
+                       expected: []gatewayv1.Listener{
+                               listener,
+                       },
+               },
+               {
+                       name:     "no matched listeners",
+                       context:  RouteParentRefContext{},
+                       expected: nil,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       assert.Equal(t, tt.expected, 
listenersForGatewayContext(tt.context))
+               })
+       }
+}
+
+func TestGetUnionOfGatewayHostnames(t *testing.T) {
+       fooHostname := gatewayv1.Hostname("foo.example.com")
+       barHostname := gatewayv1.Hostname("bar.example.com")
+
+       t.Run("uses all matched listeners and ignores non-hostname-effective 
listeners", func(t *testing.T) {
+               gateways := []RouteParentRefContext{
+                       {
+                               Listeners: []gatewayv1.Listener{
+                                       {Name: "foo", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname: 
&fooHostname},
+                                       {Name: "bar", Protocol: 
gatewayv1.HTTPSProtocolType, Port: gatewayv1.PortNumber(443), Hostname: 
&barHostname},
+                                       {Name: "tcp", Protocol: 
gatewayv1.TCPProtocolType, Port: gatewayv1.PortNumber(9100)},
+                               },
+                       },
+               }
+
+               hostnames, matchAny := getUnionOfGatewayHostnames(gateways)
+               assert.False(t, matchAny)
+               assert.Equal(t, []gatewayv1.Hostname{fooHostname, barHostname}, 
hostnames)
+       })
+
+       t.Run("listener without hostname matches any hostname", func(t 
*testing.T) {
+               gateways := []RouteParentRefContext{
+                       {
+                               Listeners: []gatewayv1.Listener{
+                                       {Name: "http", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80)},
+                               },
+                       },
+               }
+
+               hostnames, matchAny := getUnionOfGatewayHostnames(gateways)
+               assert.True(t, matchAny)
+               assert.Nil(t, hostnames)
+       })
+}
+
+func TestGetMinimumHostnameIntersection(t *testing.T) {
+       fooHostname := gatewayv1.Hostname("foo.example.com")
+       routeHostname := gatewayv1.Hostname("foo.example.com")
+       wildcardHostname := gatewayv1.Hostname("*.example.com")
+
+       t.Run("matches across multiple listeners without sectionName", func(t 
*testing.T) {
+               gateways := []RouteParentRefContext{
+                       {
+                               Listeners: []gatewayv1.Listener{
+                                       {Name: "foo", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname: 
&fooHostname},
+                                       {Name: "wildcard", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname: 
&wildcardHostname},
+                               },
+                       },
+               }
+
+               assert.Equal(t, routeHostname, 
getMinimumHostnameIntersection(gateways, routeHostname))
+       })
+
+       t.Run("returns empty when there is no listener match", func(t 
*testing.T) {
+               gateways := []RouteParentRefContext{
+                       {
+                               Listeners: []gatewayv1.Listener{
+                                       {Name: "foo", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname: 
&fooHostname},
+                               },
+                       },
+               }
+               unmatched := gatewayv1.Hostname("bar.example.com")
+               assert.Equal(t, gatewayv1.Hostname(""), 
getMinimumHostnameIntersection(gateways, unmatched))
+       })
+}
+
+func TestFilterHostnamesWithMatchedListeners(t *testing.T) {
+       fooHostname := gatewayv1.Hostname("foo.example.com")
+       barHostname := gatewayv1.Hostname("bar.example.com")
+
+       route := &gatewayv1.HTTPRoute{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test",
+                       Namespace: "default",
+               },
+               Spec: gatewayv1.HTTPRouteSpec{
+                       Hostnames: []gatewayv1.Hostname{
+                               fooHostname,
+                               barHostname,
+                       },
+               },
+       }
+
+       gateways := []RouteParentRefContext{
+               {
+                       Listeners: []gatewayv1.Listener{
+                               {Name: "foo", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname: 
&fooHostname},
+                               {Name: "bar", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname: 
&barHostname},
+                       },
+               },
+       }
+
+       filtered, err := filterHostnames(gateways, route.DeepCopy())
+       assert.NoError(t, err)
+       assert.Equal(t, []gatewayv1.Hostname{fooHostname, barHostname}, 
filtered.Spec.Hostnames)
+}
+
+func TestFilterHostnamesNoMatchedListeners(t *testing.T) {
+       fooHostname := gatewayv1.Hostname("foo.example.com")
+       barHostname := gatewayv1.Hostname("bar.example.com")
+
+       route := &gatewayv1.HTTPRoute{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test",
+                       Namespace: "default",
+               },
+               Spec: gatewayv1.HTTPRouteSpec{
+                       Hostnames: []gatewayv1.Hostname{
+                               barHostname,
+                       },
+               },
+       }
+
+       gateways := []RouteParentRefContext{
+               {
+                       Listeners: []gatewayv1.Listener{
+                               {Name: "foo", Protocol: 
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname: 
&fooHostname},
+                       },
+               },
+       }
+
+       _, err := filterHostnames(gateways, route.DeepCopy())
+       assert.ErrorIs(t, err, ErrNoMatchingListenerHostname)
+}
+
+func TestAppendListeners(t *testing.T) {
+       listenerA := gatewayv1.Listener{Name: "a", Port: 80}
+       listenerB := gatewayv1.Listener{Name: "b", Port: 81}
+       listenerA2 := gatewayv1.Listener{Name: "a", Port: 82}
+       listenerA3 := gatewayv1.Listener{Name: "a", Port: 80}
+
+       tests := []struct {
+               name     string
+               target   []gatewayv1.Listener
+               source   []gatewayv1.Listener
+               expected []gatewayv1.Listener
+       }{
+               {
+                       name:     "empty target, add listeners",
+                       target:   nil,
+                       source:   []gatewayv1.Listener{listenerA, listenerB},
+                       expected: []gatewayv1.Listener{listenerA, listenerB},
+               },
+               {
+                       name:     "preserves same listener names from different 
gateways",
+                       target:   []gatewayv1.Listener{listenerA},
+                       source:   []gatewayv1.Listener{listenerA, listenerB},
+                       expected: []gatewayv1.Listener{listenerA, listenerA, 
listenerB},
+               },
+               {
+                       name:     "preserves all listeners when names collide",
+                       target:   []gatewayv1.Listener{listenerA},
+                       source:   []gatewayv1.Listener{listenerB, listenerA2, 
listenerA3},
+                       expected: []gatewayv1.Listener{listenerA, listenerB, 
listenerA2, listenerA3},
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       assert.Equal(t, tt.expected, appendListeners(tt.target, 
tt.source...))
+               })
+       }
+}
diff --git a/internal/manager/run.go b/internal/manager/run.go
index 78bc30e5..315644da 100644
--- a/internal/manager/run.go
+++ b/internal/manager/run.go
@@ -190,9 +190,10 @@ func Run(ctx context.Context, logger logr.Logger) error {
        providerType := string(config.ControllerConfig.ProviderConfig.Type)
 
        providerOptions := &provider.Options{
-               SyncTimeout:   config.ControllerConfig.ExecADCTimeout.Duration,
-               SyncPeriod:    
config.ControllerConfig.ProviderConfig.SyncPeriod.Duration,
-               InitSyncDelay: 
config.ControllerConfig.ProviderConfig.InitSyncDelay.Duration,
+               SyncTimeout:           
config.ControllerConfig.ExecADCTimeout.Duration,
+               SyncPeriod:            
config.ControllerConfig.ProviderConfig.SyncPeriod.Duration,
+               InitSyncDelay:         
config.ControllerConfig.ProviderConfig.InitSyncDelay.Duration,
+               ListenerPortMatchMode: 
config.ControllerConfig.ListenerPortMatchMode,
        }
        provider, err := provider.New(providerType, logger, updater.Writer(), 
readier, providerOptions)
        if err != nil {
diff --git a/internal/provider/apisix/provider.go 
b/internal/provider/apisix/provider.go
index 029675e2..ef6e3fc8 100644
--- a/internal/provider/apisix/provider.go
+++ b/internal/provider/apisix/provider.go
@@ -86,7 +86,7 @@ func New(log logr.Logger, updater status.Updater, readier 
readiness.ReadinessMan
        return &apisixProvider{
                client:     cli,
                Options:    o,
-               translator: translator.NewTranslator(log),
+               translator: translator.NewTranslator(log, 
o.ListenerPortMatchMode),
                updater:    updater,
                readier:    readier,
                syncCh:     make(chan struct{}, 1),
diff --git a/internal/provider/options.go b/internal/provider/options.go
index dbb0760b..c47e7ce9 100644
--- a/internal/provider/options.go
+++ b/internal/provider/options.go
@@ -19,6 +19,8 @@ package provider
 
 import (
        "time"
+
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
 )
 
 type Option interface {
@@ -31,6 +33,7 @@ type Options struct {
        InitSyncDelay           time.Duration
        DefaultBackendMode      string
        DefaultResolveEndpoints bool
+       ListenerPortMatchMode   config.ListenerPortMatchMode
 }
 
 func (o *Options) ApplyToList(lo *Options) {
@@ -49,6 +52,9 @@ func (o *Options) ApplyToList(lo *Options) {
        if o.DefaultResolveEndpoints {
                lo.DefaultResolveEndpoints = o.DefaultResolveEndpoints
        }
+       if o.ListenerPortMatchMode != "" {
+               lo.ListenerPortMatchMode = o.ListenerPortMatchMode
+       }
 }
 
 func (o *Options) ApplyOptions(opts []Option) *Options {
diff --git a/internal/webhook/v1/adc_validation.go 
b/internal/webhook/v1/adc_validation.go
index 668a65ab..2c980c0f 100644
--- a/internal/webhook/v1/adc_validation.go
+++ b/internal/webhook/v1/adc_validation.go
@@ -55,7 +55,7 @@ func newADCAdmissionValidator(kubeClient client.Client, log 
logr.Logger) (*adcAd
        return &adcAdmissionValidator{
                kubeClient:             kubeClient,
                client:                 cli,
-               translator:             adctranslator.NewTranslator(log),
+               translator:             adctranslator.NewTranslator(log, 
config.ControllerConfig.ListenerPortMatchMode),
                log:                    log.WithName("adc-validation"),
                defaultResolveEndpoint: 
config.ControllerConfig.ProviderConfig.Type == config.ProviderTypeStandalone,
        }, nil
diff --git a/test/e2e/framework/manifests/apisix.yaml 
b/test/e2e/framework/manifests/apisix.yaml
index 310398e1..90f8845a 100644
--- a/test/e2e/framework/manifests/apisix.yaml
+++ b/test/e2e/framework/manifests/apisix.yaml
@@ -46,7 +46,10 @@ data:
         lua_shared_dict:
           standalone-config: 50m
     apisix:
-      proxy_mode: http&stream      
+      proxy_mode: http&stream
+      node_listen:
+        - port: 9080
+        - port: 9081
       stream_proxy:                 # TCP/UDP proxy
         tcp:                        # TCP proxy port list
           - 9100
@@ -104,6 +107,9 @@ spec:
             - name: http
               containerPort: 9080
               protocol: TCP
+            - name: http-alt
+              containerPort: 9081
+              protocol: TCP
             - name: https
               containerPort: 9443
               protocol: TCP
@@ -151,6 +157,10 @@ spec:
       name: http
       protocol: TCP
       targetPort: 9080
+    - port: 9081
+      name: http-alt
+      protocol: TCP
+      targetPort: 9081
     - port: {{ .ServiceHTTPSPort }}
       name: https
       protocol: TCP
diff --git a/test/e2e/framework/manifests/ingress.yaml 
b/test/e2e/framework/manifests/ingress.yaml
index 2c240d7f..15fdf630 100644
--- a/test/e2e/framework/manifests/ingress.yaml
+++ b/test/e2e/framework/manifests/ingress.yaml
@@ -291,7 +291,7 @@ data:
       retry_period: 2s                      # retry_period is the time in 
seconds that the acting controller
                                             # will wait between tries of 
actions with the controller.
       disable: false                        # Whether to disable leader 
election.
-    exec_adc_timeout: 5s
+    exec_adc_timeout: {{ env "E2E_EXEC_ADC_TIMEOUT" | default "5s" }}
     provider:
       type: {{ .ProviderType | default "apisix" }}
       sync_period: {{ .ProviderSyncPeriod | default "0s" }}
diff --git a/test/e2e/gatewayapi/grpcroute.go b/test/e2e/gatewayapi/grpcroute.go
index e4485fe0..01aab5e8 100644
--- a/test/e2e/gatewayapi/grpcroute.go
+++ b/test/e2e/gatewayapi/grpcroute.go
@@ -19,6 +19,7 @@ package gatewayapi
 
 import (
        "fmt"
+       "time"
 
        . "github.com/onsi/ginkgo/v2"
        . "github.com/onsi/gomega"
@@ -282,6 +283,162 @@ spec:
                */
        })
 
+       Context("GRPCRoute with sectionName targeting different listeners", 
func() {
+               var multiListenerGateway = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+    - name: http-main
+      protocol: HTTP
+      port: 9080
+    - name: http-alt
+      protocol: HTTP
+      port: 9081
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`
+
+               var routeForMainListener = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: GRPCRoute
+metadata:
+  name: grpc-route-main
+spec:
+  parentRefs:
+  - name: %s
+    sectionName: http-main
+  rules:
+  - backendRefs:
+    - name: grpc-infra-backend-v1
+      port: 8080
+`
+
+               var routeForAltListener = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: GRPCRoute
+metadata:
+  name: grpc-route-alt
+spec:
+  parentRefs:
+  - name: %s
+    sectionName: http-alt
+  rules:
+  - backendRefs:
+    - name: grpc-infra-backend-v1
+      port: 8080
+`
+
+               var routeForMainListenerByPort = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: GRPCRoute
+metadata:
+  name: grpc-route-port-main
+spec:
+  parentRefs:
+  - name: %s
+    port: 9080
+  rules:
+  - backendRefs:
+    - name: grpc-infra-backend-v1
+      port: 8080
+`
+
+               var routeForAltListenerByPort = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: GRPCRoute
+metadata:
+  name: grpc-route-port-alt
+spec:
+  parentRefs:
+  - name: %s
+    port: 9081
+  rules:
+  - backendRefs:
+    - name: grpc-infra-backend-v1
+      port: 8080
+`
+
+               assertRouteReachabilityOnPort := func(port int, shouldSucceed 
bool) {
+                       check := Eventually(func() error {
+                               return 
s.RequestEchoBackendOnPort(scaffold.ExpectedResponse{
+                                       EchoRequest: &pb.EchoRequest{},
+                               }, port)
+                       }).WithTimeout(30 * time.Second).ProbeEvery(time.Second)
+                       if shouldSucceed {
+                               check.ShouldNot(HaveOccurred())
+                               return
+                       }
+                       check.Should(HaveOccurred())
+               }
+
+               runMultiListenerRouteTest := func(
+                       gatewayName string,
+                       routeMainTemplate, routeMainName, routeMainBy string,
+                       routeAltTemplate, routeAltName, routeAltBy string,
+                       deleteMainRouteBy string,
+               ) {
+                       By("create Gateway with listeners on ports 9080 and 
9081")
+                       gateway := fmt.Sprintf(multiListenerGateway, 
gatewayName, s.Namespace())
+                       
Expect(s.CreateResourceFromString(gateway)).NotTo(HaveOccurred())
+
+                       s.RetryAssertion(func() string {
+                               yaml, _ := s.GetResourceYaml("Gateway", 
gatewayName)
+                               return yaml
+                       }).Should(ContainSubstring(`status: "True"`))
+
+                       By(routeMainBy)
+                       routeMain := fmt.Sprintf(routeMainTemplate, gatewayName)
+                       s.ResourceApplied("GRPCRoute", routeMainName, 
routeMain, 1)
+
+                       By(routeAltBy)
+                       routeAlt := fmt.Sprintf(routeAltTemplate, gatewayName)
+                       s.ResourceApplied("GRPCRoute", routeAltName, routeAlt, 
1)
+
+                       By("verify both ports serve traffic before deletion")
+                       assertRouteReachabilityOnPort(9080, true)
+                       assertRouteReachabilityOnPort(9081, true)
+
+                       By(deleteMainRouteBy)
+                       
Expect(s.DeleteResourceFromString(routeMain)).NotTo(HaveOccurred())
+
+                       assertRouteReachabilityOnPort(9080, false)
+                       assertRouteReachabilityOnPort(9081, true)
+               }
+
+               It("routes to the configured listener ports when sectionName is 
set", func() {
+                       runMultiListenerRouteTest(
+                               "grpc-multi-listener",
+                               routeForMainListener,
+                               "grpc-route-main",
+                               "create GRPCRoute targeting listener http-main",
+                               routeForAltListener,
+                               "grpc-route-alt",
+                               "create GRPCRoute targeting listener http-alt",
+                               "delete route for 9080 and verify only 9081 
keeps serving traffic",
+                       )
+               })
+
+               It("routes to the configured listener ports when parentRef.port 
is set", func() {
+                       runMultiListenerRouteTest(
+                               "grpc-multi-listener-by-port",
+                               routeForMainListenerByPort,
+                               "grpc-route-port-main",
+                               "create GRPCRoute targeting port 9080 via 
parentRef.port",
+                               routeForAltListenerByPort,
+                               "grpc-route-port-alt",
+                               "create GRPCRoute targeting port 9081 via 
parentRef.port",
+                               "delete route for port 9080 and verify only 
port 9081 keeps serving traffic",
+                       )
+               })
+       })
+
        // TODO: add BackendTrafficPolicy test
        /*
                Context("GRPCRoute With BackendTrafficPolicy", func() {})
diff --git a/test/e2e/gatewayapi/httproute.go b/test/e2e/gatewayapi/httproute.go
index bb106c9d..d9c5de5e 100644
--- a/test/e2e/gatewayapi/httproute.go
+++ b/test/e2e/gatewayapi/httproute.go
@@ -22,6 +22,8 @@ import (
        "crypto/tls"
        "fmt"
        "net/http"
+       "regexp"
+       "strconv"
        "strings"
        "time"
 
@@ -2580,4 +2582,538 @@ spec:
                        Expect(string(msg)).To(Equal(testMessage), "message 
content verification")
                })
        })
+
+       Context("HTTPRoute with sectionName targeting different listeners", 
func() {
+               // Uses port 9080 (HTTP) and port 9081 (HTTP)
+               // Both ports are already exposed by the APISIX service
+               // Uses in-cluster curl to test server_port vars correctly
+
+               var multiListenerGateway = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+    - name: http-main
+      protocol: HTTP
+      port: 9080
+    - name: http-alt
+      protocol: HTTP
+      port: 9081
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`
+
+               var routeForMainListener = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: route-main
+spec:
+  parentRefs:
+  - name: %s
+    sectionName: http-main
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /get
+    backendRefs:
+    - name: httpbin-service-e2e-test
+      port: 80
+`
+
+               var routeForAltListener = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: route-alt
+spec:
+  parentRefs:
+  - name: %s
+    sectionName: http-alt
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /get
+    backendRefs:
+    - name: httpbin-service-e2e-test
+      port: 80
+`
+
+               var routeNoSectionName = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: route-no-section
+spec:
+  parentRefs:
+  - name: %s
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /get
+    backendRefs:
+    - name: httpbin-service-e2e-test
+      port: 80
+`
+
+               var routeInvalidSectionName = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: route-invalid-section
+spec:
+  parentRefs:
+  - name: %s
+    sectionName: non-existent-listener
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /get
+    backendRefs:
+    - name: httpbin-service-e2e-test
+      port: 80
+`
+
+               var routeMultiParentRef = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: route-multi-parent
+spec:
+  parentRefs:
+  - name: %s
+    sectionName: http-main
+  - name: %s
+    sectionName: http-alt
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /get
+    backendRefs:
+    - name: httpbin-service-e2e-test
+      port: 80
+`
+
+               var routeForMainListenerByPort = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: route-port-main
+spec:
+  parentRefs:
+  - name: %s
+    port: 9080
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /get
+    backendRefs:
+    - name: httpbin-service-e2e-test
+      port: 80
+`
+
+               var routeForAltListenerByPort = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: route-port-alt
+spec:
+  parentRefs:
+  - name: %s
+    port: 9081
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /get
+    backendRefs:
+    - name: httpbin-service-e2e-test
+      port: 80
+`
+
+               var multiListenerGatewayWithHostnames = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+    - name: http-main
+      protocol: HTTP
+      port: 9080
+      hostname: api-main.example.com
+    - name: http-alt
+      protocol: HTTP
+      port: 9081
+      hostname: api-alt.example.com
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`
+
+               var routeNoSectionNameWithHostnames = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: route-no-section-hostnames
+spec:
+  parentRefs:
+  - name: %s
+  hostnames:
+  - api-main.example.com
+  - api-alt.example.com
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /get
+    backendRefs:
+    - name: httpbin-service-e2e-test
+      port: 80
+`
+
+               // Get the APISIX service name from the deployer
+               getApisixServiceName := func() string {
+                       return framework.ProviderType
+               }
+
+               statusCodePattern := regexp.MustCompile(`\b([1-5][0-9]{2})\b`)
+               parseHTTPStatusCode := func(output string) (int, error) {
+                       matches := 
statusCodePattern.FindAllString(strings.TrimSpace(output), -1)
+                       if len(matches) == 0 {
+                               return 0, fmt.Errorf("failed to parse HTTP 
status code from output: %q", output)
+                       }
+
+                       code, err := strconv.Atoi(matches[len(matches)-1])
+                       if err != nil {
+                               return 0, fmt.Errorf("failed converting status 
code from output %q: %w", output, err)
+                       }
+                       return code, nil
+               }
+
+               // Run curl with explicit Host header from within the cluster.
+               curlInClusterWithHost := func(port int, path, host string) 
(int, string, error) {
+                       url := 
fmt.Sprintf("http://%s.%s.svc.cluster.local:%d%s";,
+                               getApisixServiceName(), s.Namespace(), port, 
path)
+
+                       args := []string{"-s", "-o", "/dev/null", "-w", 
"%{http_code}"}
+                       if host != "" {
+                               args = append(args, "-H", fmt.Sprintf("Host: 
%s", host))
+                       }
+                       args = append(args, url)
+
+                       output, err := s.RunCurlFromK8s(args...)
+                       if err != nil {
+                               return 0, "", err
+                       }
+                       statusCode, err := parseHTTPStatusCode(output)
+                       if err != nil {
+                               return 0, output, err
+                       }
+                       return statusCode, output, nil
+               }
+
+               // Run curl from within the cluster to the specified port.
+               curlInCluster := func(port int, path string) (int, string, 
error) {
+                       return curlInClusterWithHost(port, path, "")
+               }
+
+               BeforeEach(func() {
+                       By("create GatewayProxy")
+                       
Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred())
+
+                       By("create GatewayClass")
+                       
Expect(s.CreateResourceFromString(s.GetGatewayClassYaml())).NotTo(HaveOccurred())
+
+                       s.RetryAssertion(func() string {
+                               yaml, _ := s.GetResourceYaml("GatewayClass", 
s.Namespace())
+                               return yaml
+                       }).Should(ContainSubstring(`status: "True"`))
+               })
+
+               It("routes traffic to correct backend based on sectionName 
(using server_port vars)", func() {
+                       gatewayName := s.Namespace()
+
+                       By("create Gateway with two listeners on different 
ports")
+                       gateway := fmt.Sprintf(multiListenerGateway, 
gatewayName, s.Namespace())
+                       
Expect(s.CreateResourceFromString(gateway)).NotTo(HaveOccurred())
+
+                       s.RetryAssertion(func() string {
+                               yaml, _ := s.GetResourceYaml("Gateway", 
gatewayName)
+                               return yaml
+                       }).Should(ContainSubstring(`status: "True"`))
+
+                       By("create HTTPRoute targeting http-main listener (port 
9080)")
+                       routeMain := fmt.Sprintf(routeForMainListener, 
gatewayName)
+                       s.ResourceApplied("HTTPRoute", "route-main", routeMain, 
1)
+
+                       By("create HTTPRoute targeting http-alt listener (port 
9081)")
+                       routeAlt := fmt.Sprintf(routeForAltListener, 
gatewayName)
+                       s.ResourceApplied("HTTPRoute", "route-alt", routeAlt, 1)
+
+                       By("wait for routes to be synced")
+                       time.Sleep(5 * time.Second)
+
+                       By("verify route-main is accessible on port 9080 (via 
in-cluster curl)")
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9080, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "route should be accessible on port 9080")
+
+                       By("verify route-alt is accessible on port 9081 (via 
in-cluster curl)")
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9081, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "route should be accessible on port 9081")
+
+                       By("delete route-main and verify route-alt still works")
+                       err := s.DeleteResourceFromString(routeMain)
+                       Expect(err).NotTo(HaveOccurred())
+
+                       // Port 9080 should now return 404 (route deleted)
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9080, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound),
+                               "route should return 404 on port 9080 after 
deletion")
+
+                       // Port 9081 should still return 200
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9081, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "route should still return 200 on port 9081")
+               })
+
+               It("route with sectionName should be blocked on non-target port 
(server_port isolation)", func() { //nolint:dupl
+                       gatewayName := s.Namespace()
+
+                       By("create Gateway with two listeners on different 
ports")
+                       gateway := fmt.Sprintf(multiListenerGateway, 
gatewayName, s.Namespace())
+                       
Expect(s.CreateResourceFromString(gateway)).NotTo(HaveOccurred())
+
+                       s.RetryAssertion(func() string {
+                               yaml, _ := s.GetResourceYaml("Gateway", 
gatewayName)
+                               return yaml
+                       }).Should(ContainSubstring(`status: "True"`))
+
+                       By("create HTTPRoute targeting ONLY http-main listener 
(port 9080)")
+                       routeMain := fmt.Sprintf(routeForMainListener, 
gatewayName)
+                       s.ResourceApplied("HTTPRoute", "route-main", routeMain, 
1)
+
+                       By("wait for route sync")
+                       time.Sleep(5 * time.Second)
+
+                       By("verify route-main is accessible on its target port 
9080")
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9080, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "route should be accessible on target port 
9080")
+
+                       By("verify route-main is NOT accessible on non-target 
port 9081 (server_port var must block it)")
+                       // This is the key assertion: with server_port == 9080 
injected on route-main,
+                       // port 9081 must return 404. If server_port vars were 
missing, APISIX would match
+                       // route-main on all ports and this assertion would 
fail with 200.
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9081, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound),
+                               "route should NOT be accessible on non-target 
port 9081 — server_port var must enforce port isolation")
+               })
+
+               It("should match all listeners when sectionName is omitted", 
func() { //nolint:dupl
+                       gatewayName := s.Namespace()
+
+                       By("create Gateway with two listeners")
+                       gateway := fmt.Sprintf(multiListenerGateway, 
gatewayName, s.Namespace())
+                       
Expect(s.CreateResourceFromString(gateway)).NotTo(HaveOccurred())
+
+                       s.RetryAssertion(func() string {
+                               yaml, _ := s.GetResourceYaml("Gateway", 
gatewayName)
+                               return yaml
+                       }).Should(ContainSubstring(`status: "True"`))
+
+                       By("create HTTPRoute WITHOUT sectionName")
+                       route := fmt.Sprintf(routeNoSectionName, gatewayName)
+                       s.ResourceApplied("HTTPRoute", "route-no-section", 
route, 1)
+
+                       By("wait for route sync")
+                       time.Sleep(5 * time.Second)
+
+                       By("verify route is accessible on port 9080")
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9080, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "route should be accessible on port 9080")
+
+                       By("verify route is accessible on port 9081")
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9081, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "route should be accessible on port 9081")
+               })
+
+               It("should keep all matched hostnames when sectionName is 
omitted", func() {
+                       gatewayName := s.Namespace()
+
+                       By("create Gateway with two listeners and distinct 
hostnames")
+                       gateway := 
fmt.Sprintf(multiListenerGatewayWithHostnames, gatewayName, s.Namespace())
+                       
Expect(s.CreateResourceFromString(gateway)).NotTo(HaveOccurred())
+
+                       s.RetryAssertion(func() string {
+                               yaml, _ := s.GetResourceYaml("Gateway", 
gatewayName)
+                               return yaml
+                       }).Should(ContainSubstring(`status: "True"`))
+
+                       By("create HTTPRoute WITHOUT sectionName and with both 
hostnames")
+                       route := fmt.Sprintf(routeNoSectionNameWithHostnames, 
gatewayName)
+                       s.ResourceApplied("HTTPRoute", 
"route-no-section-hostnames", route, 1)
+
+                       By("verify first hostname is routable")
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := 
curlInClusterWithHost(9080, "/get", "api-main.example.com")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "api-main.example.com should be routable")
+
+                       By("verify second hostname is routable")
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := 
curlInClusterWithHost(9081, "/get", "api-alt.example.com")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "api-alt.example.com should be routable")
+               })
+
+               It("should not route traffic when sectionName references 
non-existent listener", func() {
+                       gatewayName := s.Namespace()
+
+                       By("create Gateway with two listeners")
+                       gateway := fmt.Sprintf(multiListenerGateway, 
gatewayName, s.Namespace())
+                       
Expect(s.CreateResourceFromString(gateway)).NotTo(HaveOccurred())
+
+                       s.RetryAssertion(func() string {
+                               yaml, _ := s.GetResourceYaml("Gateway", 
gatewayName)
+                               return yaml
+                       }).Should(ContainSubstring(`status: "True"`))
+
+                       By("create HTTPRoute with invalid sectionName")
+                       route := fmt.Sprintf(routeInvalidSectionName, 
gatewayName)
+                       
Expect(s.CreateResourceFromString(route)).NotTo(HaveOccurred())
+
+                       By("wait for reconciliation")
+                       time.Sleep(5 * time.Second)
+
+                       By("verify route is NOT accessible on any port (no 
matching listener)")
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9080, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound),
+                               "route should not be accessible when 
sectionName is invalid")
+               })
+
+               It("routes traffic to correct backend based on parentRef.port 
(using server_port vars)", func() {
+                       gatewayName := s.Namespace()
+
+                       By("create Gateway with two listeners on different 
ports")
+                       gateway := fmt.Sprintf(multiListenerGateway, 
gatewayName, s.Namespace())
+                       
Expect(s.CreateResourceFromString(gateway)).NotTo(HaveOccurred())
+
+                       s.RetryAssertion(func() string {
+                               yaml, _ := s.GetResourceYaml("Gateway", 
gatewayName)
+                               return yaml
+                       }).Should(ContainSubstring(`status: "True"`))
+
+                       By("create HTTPRoute targeting port 9080 via 
parentRef.port")
+                       routeMain := fmt.Sprintf(routeForMainListenerByPort, 
gatewayName)
+                       s.ResourceApplied("HTTPRoute", "route-port-main", 
routeMain, 1)
+
+                       By("create HTTPRoute targeting port 9081 via 
parentRef.port")
+                       routeAlt := fmt.Sprintf(routeForAltListenerByPort, 
gatewayName)
+                       s.ResourceApplied("HTTPRoute", "route-port-alt", 
routeAlt, 1)
+
+                       By("verify route-port-main is accessible on port 9080")
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9080, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "route should be accessible on port 9080")
+
+                       By("verify route-port-alt is accessible on port 9081")
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9081, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "route should be accessible on port 9081")
+
+                       By("delete route-port-main and verify route-port-alt 
still works")
+                       err := s.DeleteResourceFromString(routeMain)
+                       Expect(err).NotTo(HaveOccurred())
+
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9080, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound),
+                               "route should return 404 on port 9080 after 
deletion")
+
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9081, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "route should still return 200 on port 9081")
+               })
+
+               It("should route to multiple listeners via multiple parentRefs 
with sectionName", func() {
+                       gatewayName := s.Namespace()
+
+                       By("create Gateway with two listeners")
+                       gateway := fmt.Sprintf(multiListenerGateway, 
gatewayName, s.Namespace())
+                       
Expect(s.CreateResourceFromString(gateway)).NotTo(HaveOccurred())
+
+                       s.RetryAssertion(func() string {
+                               yaml, _ := s.GetResourceYaml("Gateway", 
gatewayName)
+                               return yaml
+                       }).Should(ContainSubstring(`status: "True"`))
+
+                       By("create HTTPRoute with multiple parentRefs targeting 
different listeners")
+                       route := fmt.Sprintf(routeMultiParentRef, gatewayName, 
gatewayName)
+                       s.ResourceApplied("HTTPRoute", "route-multi-parent", 
route, 1)
+
+                       By("wait for route sync")
+                       time.Sleep(5 * time.Second)
+
+                       By("verify route is accessible on port 9080")
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9080, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "route should be accessible on port 9080")
+
+                       By("verify route is accessible on port 9081")
+                       Eventually(func() (int, error) {
+                               statusCode, _, err := curlInCluster(9081, 
"/get")
+                               return statusCode, err
+                       
}).WithTimeout(30*time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK),
+                               "route should be accessible on port 9081")
+               })
+       })
 })
diff --git a/test/e2e/scaffold/grpc.go b/test/e2e/scaffold/grpc.go
index cf79c513..66babeda 100644
--- a/test/e2e/scaffold/grpc.go
+++ b/test/e2e/scaffold/grpc.go
@@ -21,6 +21,7 @@ import (
        "strings"
        "time"
 
+       "github.com/gruntwork-io/terratest/modules/k8s"
        "google.golang.org/grpc"
        "google.golang.org/grpc/codes"
        "google.golang.org/grpc/credentials/insecure"
@@ -65,8 +66,24 @@ func (s *Scaffold) DeployGRPCBackend() {
 }
 
 func (s *Scaffold) RequestEchoBackend(exp ExpectedResponse) error {
-       endpoint := s.apisixTunnels.HTTP.Endpoint()
+       return s.requestEchoBackendWithEndpoint(exp, 
s.apisixTunnels.HTTP.Endpoint())
+}
+
+func (s *Scaffold) RequestEchoBackendOnPort(exp ExpectedResponse, port int) 
error {
+       if port == 80 {
+               return s.RequestEchoBackend(exp)
+       }
+
+       tunnel := k8s.NewTunnel(s.kubectlOptions, k8s.ResourceTypeService, 
s.dataplaneService.Name, 0, port)
+       if err := tunnel.ForwardPortE(s.t); err != nil {
+               return err
+       }
+       defer tunnel.Close()
+
+       return s.requestEchoBackendWithEndpoint(exp, tunnel.Endpoint())
+}
 
+func (s *Scaffold) requestEchoBackendWithEndpoint(exp ExpectedResponse, 
endpoint string) error {
        endpoint = strings.Replace(endpoint, "localhost", "127.0.0.1", 1)
 
        dialOpts := 
[]grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
diff --git a/test/e2e/scaffold/k8s.go b/test/e2e/scaffold/k8s.go
index 99fa57db..30fab2fd 100644
--- a/test/e2e/scaffold/k8s.go
+++ b/test/e2e/scaffold/k8s.go
@@ -254,15 +254,41 @@ func (s *Scaffold) WaitUntilDeploymentAvailable(name 
string) {
 }
 
 func (s *Scaffold) RunDigDNSClientFromK8s(args ...string) (string, error) {
+       podName := fmt.Sprintf("dig-test-%d", time.Now().UnixNano())
        kubectlArgs := []string{
                "run",
-               "dig",
-               "-i",
+               podName,
+               "--attach=true",
                "--rm",
                "--restart=Never",
                "--image-pull-policy=IfNotPresent",
                "--image=toolbelt/dig",
+               "--quiet",
+               "--command",
+               "--",
+               "dig",
+       }
+       kubectlArgs = append(kubectlArgs, args...)
+       return s.RunKubectlAndGetOutput(kubectlArgs...)
+}
+
+// RunCurlFromK8s runs a curl command from a temporary pod inside the cluster.
+// This is useful for making HTTP requests from within the cluster, avoiding
+// port-forward limitations where server_port variables may not work correctly.
+func (s *Scaffold) RunCurlFromK8s(args ...string) (string, error) {
+       podName := fmt.Sprintf("curl-test-%d", time.Now().UnixNano())
+       kubectlArgs := []string{
+               "run",
+               podName,
+               "--attach=true",
+               "--rm",
+               "--restart=Never",
+               "--image-pull-policy=IfNotPresent",
+               "--image=alpine/curl:8.17.0",
+               "--quiet",
+               "--command",
                "--",
+               "curl",
        }
        kubectlArgs = append(kubectlArgs, args...)
        return s.RunKubectlAndGetOutput(kubectlArgs...)
diff --git a/test/e2e/scaffold/scaffold.go b/test/e2e/scaffold/scaffold.go
index af6d8289..d4498d02 100644
--- a/test/e2e/scaffold/scaffold.go
+++ b/test/e2e/scaffold/scaffold.go
@@ -313,6 +313,47 @@ func (s *Scaffold) NewAPISIXClientWithTLSProxy(host 
string) *httpexpect.Expect {
        })
 }
 
+// NewAPISIXClientForPort creates an HTTP client for a specific APISIX port.
+// For built-in ports (80, 443, 9100), it reuses the existing helpers/tunnels.
+// For any other port, it creates a new tunnel for that call.
+func (s *Scaffold) NewAPISIXClientForPort(port int) (*httpexpect.Expect, 
error) {
+       // Check if we can reuse existing tunnels
+       switch port {
+       case 80:
+               return s.NewAPISIXClient(), nil
+       case 443:
+               return s.NewAPISIXHttpsClient(""), nil
+       case 9100:
+               return s.NewAPISIXClientOnTCPPort(), nil
+       }
+
+       // Create new tunnel for custom port
+       serviceName := s.dataplaneService.Name
+       tunnel := k8s.NewTunnel(s.kubectlOptions, k8s.ResourceTypeService, 
serviceName, 0, port)
+       if err := tunnel.ForwardPortE(s.t); err != nil {
+               return nil, fmt.Errorf("failed to create tunnel for port %d: 
%w", port, err)
+       }
+       s.addFinalizers(tunnel.Close)
+
+       u := url.URL{
+               Scheme: "http",
+               Host:   tunnel.Endpoint(),
+       }
+       return httpexpect.WithConfig(httpexpect.Config{
+               BaseURL: u.String(),
+               Client: &http.Client{
+                       Transport: &http.Transport{TLSClientConfig: 
&tls.Config{InsecureSkipVerify: true}},
+                       Timeout:   3 * time.Second,
+                       CheckRedirect: func(req *http.Request, via 
[]*http.Request) error {
+                               return http.ErrUseLastResponse
+                       },
+               },
+               Reporter: httpexpect.NewAssertReporter(
+                       httpexpect.NewAssertReporter(GinkgoT()),
+               ),
+       }), nil
+}
+
 func (s *Scaffold) DefaultDataplaneResource() DataplaneResource {
        return s.Deployer.DefaultDataplaneResource()
 }

Reply via email to