detached 01ebc5c

fix(traffic): reject overlapping wildcard sni

commit
01ebc5c9e80d2526511dda8f2e8fd5ab602b5643
author
brice · · 2026-06-29 22:31 UTC
parent
39cada7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
437437 if s.wildSNI[entryPoint][sni] != "" {
438438 return fmt.Errorf("ambiguous wildcard tls sni %q on entrypoint %q", sni, entryPoint)
439439 }
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+ }
440443 if host := matchingHTTPHost(sni, s.httpHosts[entryPoint]); host != "" {
441444 return fmt.Errorf("http host conflicts with wildcard tls passthrough sni %q on entrypoint %q for host %q", sni, entryPoint, host)
442445 }
@@ -625,6 +628,24 @@ func matchingHTTPHost(wildcard string, hosts map[string]string) string {
625628 return ""
626629 }
627630
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+
628649 func hostMatchesWildcard(host string, wildcard string) bool {
629650 suffix, ok := strings.CutPrefix(wildcard, "*.")
630651 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) {
264264 },
265265 },
266266 {
267- name: "ambiguous wildcard sni overlaps rejected",
267+ name: "duplicate wildcard sni rejected",
268268 routers: []TrafficRouter{
269269 {Name: "wild1", EntryPoint: "tls", Protocol: RouterProtocolTLSPassthrough, Rule: TrafficRule{SNI: "*.example.com"}, Service: "network_service:app:https"},
270270 {Name: "wild2", EntryPoint: "tls", Protocol: RouterProtocolTLSPassthrough, Rule: TrafficRule{SNI: "*.example.com"}, Service: "network_service:app:https"},
271271 },
272272 wantErr: "ambiguous wildcard tls sni",
273273 },
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+ },
274282 {
275283 name: "http host and tls passthrough sni conflict rejected",
276284 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{

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close