@@ -424,6 +424,40 @@ redirServersLoop:
424424 // we'll create a new server for all the listener addresses
425425 // that are unused and serve the remaining redirects from it
426426
427+ // Sort redirect routes by host specificity to ensure exact matches
428+ // take precedence over wildcards, preventing ambiguous routing.
429+ slices .SortFunc (routes , func (a , b Route ) int {
430+ hostA := getFirstHostFromRoute (a )
431+ hostB := getFirstHostFromRoute (b )
432+
433+ // Catch-all routes (empty host) have the lowest priority
434+ if hostA == "" && hostB != "" {
435+ return 1
436+ }
437+ if hostB == "" && hostA != "" {
438+ return - 1
439+ }
440+
441+ hasWildcardA := strings .Contains (hostA , "*" )
442+ hasWildcardB := strings .Contains (hostB , "*" )
443+
444+ // Exact domains take precedence over wildcards
445+ if ! hasWildcardA && hasWildcardB {
446+ return - 1
447+ }
448+ if hasWildcardA && ! hasWildcardB {
449+ return 1
450+ }
451+
452+ // If both are exact or both are wildcards, the longer one is more specific
453+ if len (hostA ) != len (hostB ) {
454+ return len (hostB ) - len (hostA )
455+ }
456+
457+ // Tie-breaker: alphabetical order to ensure determinism
458+ return strings .Compare (hostA , hostB )
459+ })
460+
427461 // Use the sorted srvNames to consistently find the target server
428462 for _ , srvName := range srvNames {
429463 srv := app .Servers [srvName ]
@@ -793,3 +827,26 @@ func isTailscaleDomain(name string) bool {
793827}
794828
795829type acmeCapable interface { GetACMEIssuer () * caddytls.ACMEIssuer }
830+
831+ // getFirstHostFromRoute traverses a route's matchers to find the Host rule.
832+ // Since we are dealing with internally generated redirect routes, the host
833+ // is typically the first string within the MatchHost.
834+ func getFirstHostFromRoute (r Route ) string {
835+ for _ , matcherSet := range r .MatcherSets {
836+ for _ , m := range matcherSet {
837+ // Check if the matcher is of type MatchHost (value or pointer)
838+ switch hm := m .(type ) {
839+ case MatchHost :
840+ if len (hm ) > 0 {
841+ return hm [0 ]
842+ }
843+ case * MatchHost :
844+ if len (* hm ) > 0 {
845+ return (* hm )[0 ]
846+ }
847+ }
848+ }
849+ }
850+ // Return an empty string if it's a catch-all route (no specific host)
851+ return ""
852+ }
0 commit comments