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() {