feat: Enhance SSL passthrough support (#3943)

Signed-off-by: zijiren233 <pyh1670605849@gmail.com>
This commit is contained in:
zijiren
2026-06-22 21:06:42 +08:00
committed by GitHub
parent f060c9f51d
commit 9c13b6418c
14 changed files with 3178 additions and 46 deletions

View File

@@ -46,6 +46,8 @@ import (
"istio.io/istio/pkg/log"
"istio.io/istio/pkg/util/sets"
v1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
listersv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
@@ -438,6 +440,7 @@ func (m *IngressConfig) convertGateways(configs []common.WrapperConfig) []config
if err != nil {
IngressLog.Errorf("Get higress https configmap err %v", err)
}
m.preparePassthroughTLSHostOwners(&convertOptions, configs)
for idx := range configs {
cfg := configs[idx]
clusterId := common.GetClusterId(cfg.Config.Annotations)
@@ -504,6 +507,8 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
}
}
m.preparePassthroughTLSHostOwners(&convertOptions, configs)
// convert http route
for idx := range configs {
cfg := configs[idx]
@@ -570,13 +575,8 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
m.ingressRouteCache = convertOptions.IngressRouteCache.Extract()
m.mutex.Unlock()
// Convert http route to virtual service
out := make([]config.Config, 0, len(convertOptions.HTTPRoutes))
for host, routes := range convertOptions.HTTPRoutes {
if len(routes) == 0 {
continue
}
out := make([]config.Config, 0, len(convertOptions.VirtualServices))
for host, wrapperVS := range convertOptions.VirtualServices {
cleanHost := common.CleanHost(host)
// namespace/name, name format: (istio cluster id)-host
gateways := []string{
@@ -585,13 +585,10 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
common.CreateConvertedName(constants.IstioIngressGatewayName, cleanHost),
}
wrapperVS, exist := convertOptions.VirtualServices[host]
if !exist {
IngressLog.Warnf("virtual service for host %s does not exist.", host)
}
vs := wrapperVS.VirtualService
vs.Gateways = gateways
routes := convertOptions.HTTPRoutes[host]
// Sort, exact -> prefix -> regex
common.SortHTTPRoutes(routes)
@@ -599,14 +596,18 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
vs.Http = append(vs.Http, route.HTTPRoute)
}
firstRoute := routes[0]
if len(vs.Http) == 0 && len(vs.Tls) == 0 {
continue
}
vsName, clusterId := virtualServiceNameAndClusterID(cleanHost, wrapperVS, routes)
out = append(out, config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.VirtualService,
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, firstRoute.WrapperConfig.Config.Namespace, firstRoute.WrapperConfig.Config.Name, cleanHost),
Name: vsName,
Namespace: m.namespace,
Annotations: map[string]string{
common.ClusterIdAnnotation: firstRoute.ClusterId.String(),
common.ClusterIdAnnotation: clusterId.String(),
},
},
Spec: vs,
@@ -625,6 +626,129 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
return out
}
func virtualServiceNameAndClusterID(cleanHost string, wrapperVS *common.WrapperVirtualService, routes []*common.WrapperHTTPRoute) (string, cluster.ID) {
if len(routes) > 0 {
firstRoute := routes[0]
return common.CreateConvertedName(constants.IstioIngressGatewayName, firstRoute.WrapperConfig.Config.Namespace, firstRoute.WrapperConfig.Config.Name, cleanHost), firstRoute.ClusterId
}
cfg := wrapperVS.WrapperConfig.Config
return common.CreateConvertedName(constants.IstioIngressGatewayName, cfg.Namespace, cfg.Name, cleanHost), common.GetClusterId(cfg.Annotations)
}
func (m *IngressConfig) preparePassthroughTLSHostOwners(convertOptions *common.ConvertOptions, configs []common.WrapperConfig) {
if convertOptions.PassthroughTLSHostOwners == nil {
convertOptions.PassthroughTLSHostOwners = map[string]*config.Config{}
}
// ingress-nginx enables SSL passthrough at host level when any ingress for the host has the
// annotation, then uses the first root path as the passthrough backend.
passthroughHosts := map[string]struct{}{}
firstRootPathHostOwners := map[string]*config.Config{}
for idx := range configs {
cfg := configs[idx]
if cfg.AnnotationsConfig.IsCanary() {
continue
}
if cfg.AnnotationsConfig.IsSSLPassthrough() {
for _, host := range ingressRuleHosts(cfg.Config.Spec) {
passthroughHosts[host] = struct{}{}
}
}
for _, host := range ingressRootPathHosts(cfg.Config.Spec) {
if _, exist := firstRootPathHostOwners[host]; exist {
continue
}
firstRootPathHostOwners[host] = cfg.Config
}
}
for host := range passthroughHosts {
if owner := firstRootPathHostOwners[host]; owner != nil {
convertOptions.PassthroughTLSHostOwners[host] = owner
}
}
}
func ingressRuleHosts(spec config.Spec) []string {
switch ingressSpec := spec.(type) {
case networkingv1.IngressSpec:
return ingressV1RuleHosts(ingressSpec.Rules)
case networkingv1beta1.IngressSpec:
return ingressV1Beta1RuleHosts(ingressSpec.Rules)
default:
return nil
}
}
func ingressRootPathHosts(spec config.Spec) []string {
switch ingressSpec := spec.(type) {
case networkingv1.IngressSpec:
return ingressV1RootPathHosts(ingressSpec.Rules)
case networkingv1beta1.IngressSpec:
return ingressV1Beta1RootPathHosts(ingressSpec.Rules)
default:
return nil
}
}
func ingressV1RuleHosts(rules []networkingv1.IngressRule) []string {
out := make([]string, 0, len(rules))
for _, rule := range rules {
out = append(out, rule.Host)
}
return out
}
func ingressV1Beta1RuleHosts(rules []networkingv1beta1.IngressRule) []string {
out := make([]string, 0, len(rules))
for _, rule := range rules {
out = append(out, rule.Host)
}
return out
}
func ingressV1RootPathHosts(rules []networkingv1.IngressRule) []string {
out := make([]string, 0, len(rules))
for _, rule := range rules {
if rule.HTTP == nil || !hasV1RootHTTPIngressPath(rule.HTTP.Paths) {
continue
}
out = append(out, rule.Host)
}
return out
}
func ingressV1Beta1RootPathHosts(rules []networkingv1beta1.IngressRule) []string {
out := make([]string, 0, len(rules))
for _, rule := range rules {
if rule.HTTP == nil || !hasV1Beta1RootHTTPIngressPath(rule.HTTP.Paths) {
continue
}
out = append(out, rule.Host)
}
return out
}
func hasV1RootHTTPIngressPath(paths []networkingv1.HTTPIngressPath) bool {
for _, path := range paths {
if path.Path == "" || path.Path == "/" {
return true
}
}
return false
}
func hasV1Beta1RootHTTPIngressPath(paths []networkingv1beta1.HTTPIngressPath) bool {
for _, path := range paths {
if path.Path == "" || path.Path == "/" {
return true
}
}
return false
}
func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions) {
var envoyFilters []config.Config
mappings := map[string]*common.Rule{}

View File

@@ -23,6 +23,7 @@ import (
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pkg/cluster"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/constants"
"istio.io/istio/pkg/config/schema/gvk"
"istio.io/istio/pkg/config/xds"
ingress "k8s.io/api/networking/v1"
@@ -109,6 +110,405 @@ func TestNormalizeWeightedCluster(t *testing.T) {
}
}
func TestVirtualServiceNameAndClusterID(t *testing.T) {
cleanHost := common.CleanHost("example.com")
wrapperVS := &common.WrapperVirtualService{
WrapperConfig: &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "tls-ns",
Name: "tls-ingress",
Annotations: map[string]string{
common.ClusterIdAnnotation: "tls-cluster",
},
},
},
},
}
routes := []*common.WrapperHTTPRoute{
{
WrapperConfig: &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "http-ns",
Name: "http-ingress",
},
},
},
ClusterId: "http-cluster",
},
}
name, clusterID := virtualServiceNameAndClusterID(cleanHost, wrapperVS, routes)
if name != common.CreateConvertedName(constants.IstioIngressGatewayName, "http-ns", "http-ingress", cleanHost) {
t.Fatalf("http-backed virtual service name mismatch: %s", name)
}
if clusterID != "http-cluster" {
t.Fatalf("http-backed cluster id mismatch: %s", clusterID)
}
name, clusterID = virtualServiceNameAndClusterID(cleanHost, wrapperVS, nil)
if name != common.CreateConvertedName(constants.IstioIngressGatewayName, "tls-ns", "tls-ingress", cleanHost) {
t.Fatalf("tls-only virtual service name mismatch: %s", name)
}
if clusterID != "tls-cluster" {
t.Fatalf("tls-only cluster id mismatch: %s", clusterID)
}
}
func TestPreparePassthroughTLSHostOwnersRequiresPassthroughHost(t *testing.T) {
m := &IngressConfig{}
configs := []common.WrapperConfig{
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "plain-root",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
},
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "plain-root-duplicate",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
},
}
options := &common.ConvertOptions{}
m.preparePassthroughTLSHostOwners(options, configs)
if len(options.PassthroughTLSHostOwners) != 0 {
t.Fatalf("unexpected ssl passthrough owners: %+v", options.PassthroughTLSHostOwners)
}
}
func TestPreparePassthroughTLSHostOwnersUsesFirstRootPathOwner(t *testing.T) {
m := &IngressConfig{}
configs := []common.WrapperConfig{
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "plain-root",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
},
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "passthrough-non-root",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/api"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
},
}
options := &common.ConvertOptions{}
m.preparePassthroughTLSHostOwners(options, configs)
if !common.IsPassthroughTLSHostOwner(options, configs[0].Config, "example.com") {
t.Fatal("first root ingress was not recorded as passthrough owner")
}
if !common.HasPassthroughTLSHostOwner(options, configs[0].Config) {
t.Fatal("first root ingress was not found as passthrough owner")
}
}
func TestPreparePassthroughTLSHostOwnersIgnoresHTTPOnlyIngressForHTTPSFallback(t *testing.T) {
m := &IngressConfig{}
configs := []common.WrapperConfig{
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "http-only",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/api"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
},
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-ingress",
},
Spec: ingress.IngressSpec{
TLS: []ingress.IngressTLS{
{
Hosts: []string{"example.com"},
SecretName: "example-com",
},
},
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/app"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
},
}
options := &common.ConvertOptions{}
m.preparePassthroughTLSHostOwners(options, configs)
if len(options.PassthroughTLSHostOwners) != 0 {
t.Fatalf("unexpected ssl passthrough owners: %+v", options.PassthroughTLSHostOwners)
}
}
func TestConvertGatewaysHonorsFirstRootPathSSLPassthroughOwner(t *testing.T) {
fake := kube.NewFakeClient()
options := common.Options{
Enable: true,
ClusterId: "ingress-v1",
RawClusterId: "ingress-v1__",
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
}
ingressController := controllerv1.NewController(fake, fake, options, nil)
m := NewIngressConfig(fake, nil, "wakanda", options)
m.remoteIngressControllers = map[cluster.ID]common.IngressController{
"ingress-v1": ingressController,
}
configs := []common.WrapperConfig{
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-non-root",
Annotations: map[string]string{
common.ClusterIdAnnotation: "ingress-v1",
},
},
Spec: ingress.IngressSpec{
TLS: []ingress.IngressTLS{
{
Hosts: []string{"example.com"},
SecretName: "example-com",
},
},
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/api"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
},
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "passthrough-root",
Annotations: map[string]string{
common.ClusterIdAnnotation: "ingress-v1",
},
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
},
}
result := m.convertGateways(configs)
if len(result) != 1 {
t.Fatalf("gateway count mismatch, want 1, got %d", len(result))
}
gateway := result[0].Spec.(*networking.Gateway)
if len(gateway.Servers) != 2 {
t.Fatalf("server count mismatch, want 2, got %d", len(gateway.Servers))
}
tlsServer := gateway.Servers[1]
if tlsServer.Port.Protocol != "TLS" {
t.Fatalf("tls server protocol mismatch, want TLS, got %s", tlsServer.Port.Protocol)
}
if tlsServer.Tls.GetMode() != networking.ServerTLSSettings_PASSTHROUGH {
t.Fatalf("tls mode mismatch, want PASSTHROUGH, got %s", tlsServer.Tls.GetMode())
}
}
func TestConvertGatewaysUsesFirstRootOwnerWhenLaterIngressEnablesSSLPassthrough(t *testing.T) {
fake := kube.NewFakeClient()
options := common.Options{
Enable: true,
ClusterId: "ingress-v1",
RawClusterId: "ingress-v1__",
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
}
ingressController := controllerv1.NewController(fake, fake, options, nil)
m := NewIngressConfig(fake, nil, "wakanda", options)
m.remoteIngressControllers = map[cluster.ID]common.IngressController{
"ingress-v1": ingressController,
}
configs := []common.WrapperConfig{
ingressV1Wrapper("root", "example.com", "/", false),
ingressV1Wrapper("passthrough", "example.com", "/passthrough", true),
}
result := m.convertGateways(configs)
if len(result) != 1 {
t.Fatalf("gateway count mismatch, want 1, got %d", len(result))
}
gateway := result[0].Spec.(*networking.Gateway)
if len(gateway.Servers) != 2 {
t.Fatalf("server count mismatch, want 2, got %d", len(gateway.Servers))
}
tlsServer := gateway.Servers[1]
if tlsServer.Port.Protocol != "TLS" {
t.Fatalf("tls server protocol mismatch, want TLS, got %s", tlsServer.Port.Protocol)
}
if tlsServer.Tls.GetMode() != networking.ServerTLSSettings_PASSTHROUGH {
t.Fatalf("tls mode mismatch, want PASSTHROUGH, got %s", tlsServer.Tls.GetMode())
}
}
func TestConvertVirtualServiceUsesFirstRootOwnerWhenLaterIngressEnablesSSLPassthrough(t *testing.T) {
fake := kube.NewFakeClient()
options := common.Options{
Enable: true,
ClusterId: "ingress-v1",
RawClusterId: "ingress-v1__",
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
}
ingressController := controllerv1.NewController(fake, fake, options, nil)
m := NewIngressConfig(fake, nil, "wakanda", options)
m.remoteIngressControllers = map[cluster.ID]common.IngressController{
"ingress-v1": ingressController,
}
configs := []common.WrapperConfig{
ingressV1Wrapper("root", "example.com", "/", false),
ingressV1Wrapper("passthrough", "example.com", "/passthrough", true),
}
result := m.convertVirtualService(configs)
if len(result) != 1 {
t.Fatalf("virtual service count mismatch, want 1, got %d", len(result))
}
vs := result[0].Spec.(*networking.VirtualService)
if len(vs.Tls) != 1 {
t.Fatalf("tls route count mismatch, want 1, got %d", len(vs.Tls))
}
if got := vs.Tls[0].Route[0].Destination.Host; got != "root.default.svc.cluster.local" {
t.Fatalf("destination host mismatch, want root.default.svc.cluster.local, got %s", got)
}
}
func TestConvertGatewaysForIngress(t *testing.T) {
fake := kube.NewFakeClient()
v1Beta1Options := common.Options{
@@ -616,3 +1016,46 @@ func TestConstructBasicAuthEnvoyFilter(t *testing.T) {
target := proto.Clone(pb).(*httppb.HttpFilter)
t.Log(target)
}
func ingressV1Wrapper(name, host, path string, sslPassthrough bool) common.WrapperConfig {
wrapper := common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: name,
Annotations: map[string]string{
common.ClusterIdAnnotation: "ingress-v1",
},
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: host,
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: path,
Backend: ingress.IngressBackend{
Service: &ingress.IngressServiceBackend{
Name: name,
Port: ingress.ServiceBackendPort{Number: 443},
},
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
Match: &annotations.MatchConfig{},
},
}
if sslPassthrough {
wrapper.AnnotationsConfig.SSLPassthrough = &annotations.SSLPassthroughConfig{Enabled: true}
}
return wrapper
}