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 93458e99 fix: normalize hosts and SNIs so uppercase hostnames stay 
routable (#2837)
93458e99 is described below

commit 93458e991f0c31cdf9594f4df5fcf90d823bfdeb
Author: AlinsRan <[email protected]>
AuthorDate: Fri Aug 7 11:56:11 2026 +0800

    fix: normalize hosts and SNIs so uppercase hostnames stay routable (#2837)
---
 internal/adc/translator/apisixroute.go      |  3 ++-
 internal/adc/translator/apisixroute_test.go | 29 ++++++++++++++++++++++++
 internal/adc/translator/apisixtls.go        |  1 +
 internal/adc/translator/gateway.go          |  1 +
 internal/adc/translator/grpcroute.go        |  4 ++++
 internal/adc/translator/grpcroute_test.go   | 33 ++++++++++++++++++++++++++++
 internal/adc/translator/httproute.go        |  2 ++
 internal/adc/translator/ingress.go          |  2 ++
 test/e2e/crds/v2/route.go                   | 34 +++++++++++++++++++++++++++++
 9 files changed, 108 insertions(+), 1 deletion(-)

diff --git a/internal/adc/translator/apisixroute.go 
b/internal/adc/translator/apisixroute.go
index 9eb7cd5e..5fd15cbe 100644
--- a/internal/adc/translator/apisixroute.go
+++ b/internal/adc/translator/apisixroute.go
@@ -35,6 +35,7 @@ import (
        apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
        "github.com/apache/apisix-ingress-controller/internal/controller/label"
        "github.com/apache/apisix-ingress-controller/internal/provider"
+       sslutils "github.com/apache/apisix-ingress-controller/internal/ssl"
        internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
        "github.com/apache/apisix-ingress-controller/internal/utils"
        "github.com/apache/apisix-ingress-controller/pkg/id"
@@ -304,7 +305,7 @@ func (t *Translator) buildService(ar *apiv2.ApisixRoute, 
rule apiv2.ApisixRouteH
        service.Name = adc.ComposeServiceNameWithRule(ar.Namespace, ar.Name, 
fmt.Sprintf("%d", ruleIndex))
        service.ID = id.GenID(service.Name)
        service.Labels = label.GenLabel(ar)
-       service.Hosts = rule.Match.Hosts
+       service.Hosts = sslutils.NormalizeHosts(rule.Match.Hosts)
        service.Upstream = adc.NewDefaultUpstream()
        return service
 }
diff --git a/internal/adc/translator/apisixroute_test.go 
b/internal/adc/translator/apisixroute_test.go
index 1a900d3d..431e2d5b 100644
--- a/internal/adc/translator/apisixroute_test.go
+++ b/internal/adc/translator/apisixroute_test.go
@@ -91,6 +91,35 @@ func TestBuildService_HostsSet(t *testing.T) {
        assert.Equal(t, []string{"example.com", "foo.com"}, service.Hosts)
 }
 
+func TestBuildService_HostsLowercased(t *testing.T) {
+       translator := NewTranslator(logr.Discard(), "")
+
+       ar := &apiv2.ApisixRoute{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test-route",
+                       Namespace: "default",
+               },
+       }
+
+       rule := apiv2.ApisixRouteHTTP{
+               Name: "rule1",
+               Match: apiv2.ApisixRouteHTTPMatch{
+                       Hosts: []string{"MixedCase.example.com", 
"*.UPPER.example.com", "mixedcase.example.com"},
+                       Paths: []string{"/api/*"},
+               },
+       }
+
+       service := translator.buildService(ar, rule, 0)
+
+       // APISIX matches service hosts against nginx's $host, which is always 
lowercase,
+       // and does not normalize service-level hosts itself. Entries that 
collide once
+       // lowercased collapse: the APISIX service schema requires unique hosts.
+       assert.Equal(t, []string{"mixedcase.example.com", 
"*.upper.example.com"}, service.Hosts)
+
+       rule.Match.Hosts = nil
+       assert.Nil(t, translator.buildService(ar, rule, 0).Hosts)
+}
+
 func TestBuildRoute_MetadataLabelsDoNotOverwriteControllerLabels(t *testing.T) 
{
        translator := NewTranslator(logr.Discard(), "")
 
diff --git a/internal/adc/translator/apisixtls.go 
b/internal/adc/translator/apisixtls.go
index ea428711..a32995d3 100644
--- a/internal/adc/translator/apisixtls.go
+++ b/internal/adc/translator/apisixtls.go
@@ -55,6 +55,7 @@ func (t *Translator) TranslateApisixTls(tctx 
*provider.TranslateContext, tls *ap
        for i, host := range tls.Spec.Hosts {
                snis[i] = string(host)
        }
+       snis = sslutils.NormalizeHosts(snis)
 
        // Create SSL object
        ssl := &adctypes.SSL{
diff --git a/internal/adc/translator/gateway.go 
b/internal/adc/translator/gateway.go
index a7bd7c7d..e4288c79 100644
--- a/internal/adc/translator/gateway.go
+++ b/internal/adc/translator/gateway.go
@@ -146,6 +146,7 @@ func (t *Translator) translateSecret(tctx 
*provider.TranslateContext, listener g
                                        }
                                        sslObj.Snis = append(sslObj.Snis, 
hosts...)
                                }
+                               sslObj.Snis = 
sslutils.NormalizeHosts(sslObj.Snis)
                                sslObj.Client = client
                                sslObj.ID = id.GenID(fmt.Sprintf("%s_%s_%d", 
adctypes.ComposeSSLName(internaltypes.KindGateway, obj.Namespace, obj.Name), 
listener.Name, refIndex))
                                t.Log.V(1).Info("generated ssl id", "ssl id", 
sslObj.ID, "secret", secretNN.String())
diff --git a/internal/adc/translator/grpcroute.go 
b/internal/adc/translator/grpcroute.go
index 7a65c520..e3a1cad3 100644
--- a/internal/adc/translator/grpcroute.go
+++ b/internal/adc/translator/grpcroute.go
@@ -30,6 +30,7 @@ import (
        "github.com/apache/apisix-ingress-controller/internal/controller/label"
        "github.com/apache/apisix-ingress-controller/internal/id"
        "github.com/apache/apisix-ingress-controller/internal/provider"
+       sslutils "github.com/apache/apisix-ingress-controller/internal/ssl"
        internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
 )
 
@@ -158,6 +159,9 @@ func (t *Translator) TranslateGRPCRoute(tctx 
*provider.TranslateContext, grpcRou
                        hosts = append(hosts, string(*listener.Hostname))
                }
        }
+       // the listener hostnames can repeat what the route already declares, 
and the APISIX
+       // service schema requires unique hosts
+       hosts = sslutils.NormalizeHosts(hosts)
 
        rules := grpcRoute.Spec.Rules
 
diff --git a/internal/adc/translator/grpcroute_test.go 
b/internal/adc/translator/grpcroute_test.go
index f02e75a8..749ae19d 100644
--- a/internal/adc/translator/grpcroute_test.go
+++ b/internal/adc/translator/grpcroute_test.go
@@ -197,3 +197,36 @@ func TestTranslateGRPCRouteServerPortVarsByMode(t 
*testing.T) {
                })
        }
 }
+
+func TestTranslateGRPCRoute_HostsDeduplicated(t *testing.T) {
+       tctx := provider.NewDefaultTranslateContext(context.Background())
+       tctx.Listeners = []gatewayv1.Listener{
+               {
+                       Name:     "grpc",
+                       Protocol: gatewayv1.HTTPProtocolType,
+                       Port:     gatewayv1.PortNumber(80),
+                       Hostname: ptr.To(gatewayv1.Hostname("example.com")),
+               },
+       }
+
+       grpcRoute := &gatewayv1.GRPCRoute{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "route",
+                       Namespace: "default",
+               },
+               Spec: gatewayv1.GRPCRouteSpec{
+                       Hostnames: []gatewayv1.Hostname{"example.com"},
+                       Rules:     []gatewayv1.GRPCRouteRule{{}},
+               },
+       }
+
+       translator := NewTranslator(logr.Discard(), 
config.ListenerPortMatchModeOff)
+       got, err := translator.TranslateGRPCRoute(tctx, grpcRoute)
+       assert.NoError(t, err)
+
+       // The listener hostname repeats what the route already declares. The 
APISIX
+       // service schema requires unique hosts, so the duplicate has to 
collapse.
+       if assert.Len(t, got.Services, 1) {
+               assert.Equal(t, []string{"example.com"}, got.Services[0].Hosts)
+       }
+}
diff --git a/internal/adc/translator/httproute.go 
b/internal/adc/translator/httproute.go
index f129d799..cba58c21 100644
--- a/internal/adc/translator/httproute.go
+++ b/internal/adc/translator/httproute.go
@@ -36,6 +36,7 @@ import (
        "github.com/apache/apisix-ingress-controller/internal/controller/label"
        "github.com/apache/apisix-ingress-controller/internal/id"
        "github.com/apache/apisix-ingress-controller/internal/provider"
+       sslutils "github.com/apache/apisix-ingress-controller/internal/ssl"
        internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
 )
 
@@ -689,6 +690,7 @@ func (t *Translator) TranslateHTTPRoute(tctx 
*provider.TranslateContext, httpRou
        for _, hostname := range httpRoute.Spec.Hostnames {
                hosts = append(hosts, string(hostname))
        }
+       hosts = sslutils.NormalizeHosts(hosts)
 
        rules := httpRoute.Spec.Rules
 
diff --git a/internal/adc/translator/ingress.go 
b/internal/adc/translator/ingress.go
index aca8aed4..ae820330 100644
--- a/internal/adc/translator/ingress.go
+++ b/internal/adc/translator/ingress.go
@@ -53,6 +53,7 @@ func (t *Translator) translateIngressTLS(namespace, name 
string, tlsIndex int, i
                }
                hosts = append(hosts, certHosts...)
        }
+       hosts = sslutils.NormalizeHosts(hosts)
        if len(hosts) == 0 {
                return nil, fmt.Errorf("no hosts found in ingress TLS")
        }
@@ -101,6 +102,7 @@ func (t *Translator) TranslateIngress(
                if rule.Host != "" {
                        hosts = append(hosts, rule.Host)
                }
+               hosts = sslutils.NormalizeHosts(hosts)
 
                for j, path := range rule.HTTP.Paths {
                        index := fmt.Sprintf("%d-%d", i, j)
diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go
index f684db08..bf7aa967 100644
--- a/test/e2e/crds/v2/route.go
+++ b/test/e2e/crds/v2/route.go
@@ -181,6 +181,40 @@ spec:
                        It("Basic: with named service port and granularity 
service", func() {
                                
test(apisixRouteSpecWithNameServiceAndGranularity)
                        })
+                       It("Basic: with an uppercase host", func() {
+                               const apisixRouteSpecWithUppercaseHost = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: default
+  namespace: %s
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule0
+    match:
+      hosts:
+      - HTTPBIN.Example.com
+      paths:
+      - /get
+    backends:
+    - serviceName: httpbin-service-e2e-test
+      servicePort: 80
+`
+                               By("apply ApisixRoute")
+                               var apisixRoute apiv2.ApisixRoute
+                               
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: 
"default"},
+                                       &apisixRoute, 
fmt.Sprintf(apisixRouteSpecWithUppercaseHost, s.Namespace(), s.Namespace()))
+
+                               // APISIX matches hosts against nginx's $host, 
which is always lowercase,
+                               // so the host has to be stored lowercase to be 
reachable at all.
+                               request := func(host string) int {
+                                       return 
s.NewAPISIXClient().GET("/get").WithHost(host).Expect().Raw().StatusCode
+                               }
+                               By("verify the route is reachable regardless of 
the Host header case")
+                               
Eventually(request).WithArguments("HTTPBIN.Example.com").WithTimeout(20 * 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
+                               
Expect(request("httpbin.example.com")).Should(Equal(http.StatusOK))
+                       })
                })
 
                It("Test plugins in ApisixRoute", func() {

Reply via email to