fix(traffic): reject overlapping wildcard sni
- commit
- 01ebc5c9e80d2526511dda8f2e8fd5ab602b5643
- author
- brice · · 2026-06-29 22:31 UTC
- parent
- 39cada7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +30 -1
M internal/domain/traffic.go +21 -0
| @@ -437,6 +437,9 @@ func (s *routerValidationState) validateWildcardSNI(entryPoint string, sni strin | |||
| 437 | 437 | if s.wildSNI[entryPoint][sni] != "" { | |
| 438 | 438 | return fmt.Errorf("ambiguous wildcard tls sni %q on entrypoint %q", sni, entryPoint) | |
| 439 | 439 | } | |
| 440 | + | if wildcard := overlappingWildcardSNI(sni, s.wildSNI[entryPoint]); wildcard != "" { | |
| 441 | + | return fmt.Errorf("ambiguous wildcard tls sni %q overlaps %q on entrypoint %q", sni, wildcard, entryPoint) | |
| 442 | + | } | |
| 440 | 443 | if host := matchingHTTPHost(sni, s.httpHosts[entryPoint]); host != "" { | |
| 441 | 444 | return fmt.Errorf("http host conflicts with wildcard tls passthrough sni %q on entrypoint %q for host %q", sni, entryPoint, host) | |
| 442 | 445 | } | |
| @@ -625,6 +628,24 @@ func matchingHTTPHost(wildcard string, hosts map[string]string) string { | |||
| 625 | 628 | return "" | |
| 626 | 629 | } | |
| 627 | 630 | ||
| 631 | + | func overlappingWildcardSNI(wildcard string, wildcards map[string]string) string { | |
| 632 | + | for existing := range wildcards { | |
| 633 | + | if wildcardSNIOverlap(wildcard, existing) { | |
| 634 | + | return existing | |
| 635 | + | } | |
| 636 | + | } | |
| 637 | + | return "" | |
| 638 | + | } | |
| 639 | + | ||
| 640 | + | func wildcardSNIOverlap(left string, right string) bool { | |
| 641 | + | leftSuffix, leftOK := strings.CutPrefix(left, "*.") | |
| 642 | + | rightSuffix, rightOK := strings.CutPrefix(right, "*.") | |
| 643 | + | if !leftOK || !rightOK { | |
| 644 | + | return false | |
| 645 | + | } | |
| 646 | + | return leftSuffix == rightSuffix || strings.HasSuffix(leftSuffix, "."+rightSuffix) || strings.HasSuffix(rightSuffix, "."+leftSuffix) | |
| 647 | + | } | |
| 648 | + | ||
| 628 | 649 | func hostMatchesWildcard(host string, wildcard string) bool { | |
| 629 | 650 | suffix, ok := strings.CutPrefix(wildcard, "*.") | |
| 630 | 651 | if !ok || host == suffix { | |
| @@ -437,6 +437,9 @@ func (s *routerValidationState) validateWildcardSNI(entryPoint string, sni strin | |||||
| 437 | if s.wildSNI[entryPoint][sni] != "" { | 437 | if s.wildSNI[entryPoint][sni] != "" { | ||
| 438 | return fmt.Errorf("ambiguous wildcard tls sni %q on entrypoint %q", sni, entryPoint) | 438 | return fmt.Errorf("ambiguous wildcard tls sni %q on entrypoint %q", sni, entryPoint) | ||
| 439 | } | 439 | } | ||
| 440 | + | if wildcard := overlappingWildcardSNI(sni, s.wildSNI[entryPoint]); wildcard != "" { | |||
| 441 | + | return fmt.Errorf("ambiguous wildcard tls sni %q overlaps %q on entrypoint %q", sni, wildcard, entryPoint) | |||
| 442 | + | } | |||
| 440 | if host := matchingHTTPHost(sni, s.httpHosts[entryPoint]); host != "" { | 443 | if host := matchingHTTPHost(sni, s.httpHosts[entryPoint]); host != "" { | ||
| 441 | return fmt.Errorf("http host conflicts with wildcard tls passthrough sni %q on entrypoint %q for host %q", sni, entryPoint, host) | 444 | return fmt.Errorf("http host conflicts with wildcard tls passthrough sni %q on entrypoint %q for host %q", sni, entryPoint, host) | ||
| 442 | } | 445 | } | ||
| @@ -625,6 +628,24 @@ func matchingHTTPHost(wildcard string, hosts map[string]string) string { | |||||
| 625 | return "" | 628 | return "" | ||
| 626 | } | 629 | } | ||
| 627 | 630 | ||||
| 631 | + | func overlappingWildcardSNI(wildcard string, wildcards map[string]string) string { | |||
| 632 | + | for existing := range wildcards { | |||
| 633 | + | if wildcardSNIOverlap(wildcard, existing) { | |||
| 634 | + | return existing | |||
| 635 | + | } | |||
| 636 | + | } | |||
| 637 | + | return "" | |||
| 638 | + | } | |||
| 639 | + | ||||
| 640 | + | func wildcardSNIOverlap(left string, right string) bool { | |||
| 641 | + | leftSuffix, leftOK := strings.CutPrefix(left, "*.") | |||
| 642 | + | rightSuffix, rightOK := strings.CutPrefix(right, "*.") | |||
| 643 | + | if !leftOK || !rightOK { | |||
| 644 | + | return false | |||
| 645 | + | } | |||
| 646 | + | return leftSuffix == rightSuffix || strings.HasSuffix(leftSuffix, "."+rightSuffix) || strings.HasSuffix(rightSuffix, "."+leftSuffix) | |||
| 647 | + | } | |||
| 648 | + | ||||
| 628 | func hostMatchesWildcard(host string, wildcard string) bool { | 649 | func hostMatchesWildcard(host string, wildcard string) bool { | ||
| 629 | suffix, ok := strings.CutPrefix(wildcard, "*.") | 650 | suffix, ok := strings.CutPrefix(wildcard, "*.") | ||
| 630 | if !ok || host == suffix { | 651 | if !ok || host == suffix { | ||
M internal/domain/traffic_test.go +9 -1
| @@ -264,13 +264,21 @@ func TestTrafficGraphValidateTLSRules(t *testing.T) { | |||
| 264 | 264 | }, | |
| 265 | 265 | }, | |
| 266 | 266 | { | |
| 267 | - | name: "ambiguous wildcard sni overlaps rejected", | |
| 267 | + | name: "duplicate wildcard sni rejected", | |
| 268 | 268 | routers: []TrafficRouter{ | |
| 269 | 269 | {Name: "wild1", EntryPoint: "tls", Protocol: RouterProtocolTLSPassthrough, Rule: TrafficRule{SNI: "*.example.com"}, Service: "network_service:app:https"}, | |
| 270 | 270 | {Name: "wild2", EntryPoint: "tls", Protocol: RouterProtocolTLSPassthrough, Rule: TrafficRule{SNI: "*.example.com"}, Service: "network_service:app:https"}, | |
| 271 | 271 | }, | |
| 272 | 272 | wantErr: "ambiguous wildcard tls sni", | |
| 273 | 273 | }, | |
| 274 | + | { | |
| 275 | + | name: "nested wildcard sni overlap rejected", | |
| 276 | + | routers: []TrafficRouter{ | |
| 277 | + | {Name: "wild1", EntryPoint: "tls", Protocol: RouterProtocolTLSPassthrough, Rule: TrafficRule{SNI: "*.example.com"}, Service: "network_service:app:https"}, | |
| 278 | + | {Name: "wild2", EntryPoint: "tls", Protocol: RouterProtocolTLSPassthrough, Rule: TrafficRule{SNI: "*.sub.example.com"}, Service: "network_service:app:https"}, | |
| 279 | + | }, | |
| 280 | + | wantErr: "ambiguous wildcard tls sni", | |
| 281 | + | }, | |
| 274 | 282 | { | |
| 275 | 283 | name: "http host and tls passthrough sni conflict rejected", | |
| 276 | 284 | routers: []TrafficRouter{ | |
| @@ -264,13 +264,21 @@ func TestTrafficGraphValidateTLSRules(t *testing.T) { | |||||
| 264 | }, | 264 | }, | ||
| 265 | }, | 265 | }, | ||
| 266 | { | 266 | { | ||
| 267 | - | name: "ambiguous wildcard sni overlaps rejected", | 267 | + | name: "duplicate wildcard sni rejected", |
| 268 | routers: []TrafficRouter{ | 268 | routers: []TrafficRouter{ | ||
| 269 | {Name: "wild1", EntryPoint: "tls", Protocol: RouterProtocolTLSPassthrough, Rule: TrafficRule{SNI: "*.example.com"}, Service: "network_service:app:https"}, | 269 | {Name: "wild1", EntryPoint: "tls", Protocol: RouterProtocolTLSPassthrough, Rule: TrafficRule{SNI: "*.example.com"}, Service: "network_service:app:https"}, | ||
| 270 | {Name: "wild2", EntryPoint: "tls", Protocol: RouterProtocolTLSPassthrough, Rule: TrafficRule{SNI: "*.example.com"}, Service: "network_service:app:https"}, | 270 | {Name: "wild2", EntryPoint: "tls", Protocol: RouterProtocolTLSPassthrough, Rule: TrafficRule{SNI: "*.example.com"}, Service: "network_service:app:https"}, | ||
| 271 | }, | 271 | }, | ||
| 272 | wantErr: "ambiguous wildcard tls sni", | 272 | wantErr: "ambiguous wildcard tls sni", | ||
| 273 | }, | 273 | }, | ||
| 274 | + | { | |||
| 275 | + | name: "nested wildcard sni overlap rejected", | |||
| 276 | + | routers: []TrafficRouter{ | |||
| 277 | + | {Name: "wild1", EntryPoint: "tls", Protocol: RouterProtocolTLSPassthrough, Rule: TrafficRule{SNI: "*.example.com"}, Service: "network_service:app:https"}, | |||
| 278 | + | {Name: "wild2", EntryPoint: "tls", Protocol: RouterProtocolTLSPassthrough, Rule: TrafficRule{SNI: "*.sub.example.com"}, Service: "network_service:app:https"}, | |||
| 279 | + | }, | |||
| 280 | + | wantErr: "ambiguous wildcard tls sni", | |||
| 281 | + | }, | |||
| 274 | { | 282 | { | ||
| 275 | name: "http host and tls passthrough sni conflict rejected", | 283 | name: "http host and tls passthrough sni conflict rejected", | ||
| 276 | routers: []TrafficRouter{ | 284 | routers: []TrafficRouter{ | ||