@@ -221,11 +221,21 @@ enum HappRoutingApplier {
221221 ] )
222222
223223 // Merge with existing routing instead of replacing everything.
224- // Keep non-Happ rules so provider-specific routing logic survives .
224+ // Important: keep provider rules untouched; append Happ catch-all only at the very end .
225225 var routing = ( root [ " routing " ] as? [ String : Any ] ) ?? [ : ]
226226 let existingRules = ( routing [ " rules " ] as? [ [ String : Any ] ] ) ?? [ ]
227- let keptExisting = existingRules . filter { ! looksLikeManagedHappRule ( $0 , proxyTag : proxyTag , directTag : directTag , blockTag : blockTag ) }
228- routing [ " rules " ] = happRules + keptExisting
227+
228+ // Keep only specific Happ rules before provider rules.
229+ // Global catch-all is appended last so it doesn't shadow provider logic.
230+ let happSpecificRules = Array ( happRules . dropLast ( ) )
231+ let happCatchAll = happRules . last
232+
233+ var mergedRules = happSpecificRules + existingRules
234+ if let catchAll = happCatchAll , ! existingRules . contains ( where : looksLikeCatchAllRule ) {
235+ mergedRules . append ( catchAll )
236+ }
237+ routing [ " rules " ] = mergedRules
238+
229239 if let domainStrategy , ! domainStrategy . isEmpty {
230240 routing [ " domainStrategy " ] = domainStrategy
231241 } else if routing [ " domainStrategy " ] == nil {
@@ -276,13 +286,21 @@ enum HappRoutingApplier {
276286 return nil
277287 }
278288
279- private static func looksLikeManagedHappRule ( _ rule : [ String : Any ] , proxyTag : String , directTag : String , blockTag : String ) -> Bool {
289+ private static func looksLikeCatchAllRule ( _ rule : [ String : Any ] ) -> Bool {
280290 guard let type = rule [ " type " ] as? String , type == " field " else { return false }
281- guard let outbound = rule [ " outboundTag " ] as? String else { return false }
282- if outbound == proxyTag || outbound == directTag || outbound == blockTag {
283- return true
284- }
285- return false
291+
292+ // Typical Xray catch-all patterns used in subscription configs.
293+ let hasDomain = ( rule [ " domain " ] as? [ Any ] ) ? . isEmpty == false
294+ let hasIP = ( rule [ " ip " ] as? [ Any ] ) ? . isEmpty == false
295+ let hasPort = rule [ " port " ] != nil
296+ let hasProtocol = rule [ " protocol " ] != nil
297+ let hasInboundTag = rule [ " inboundTag " ] != nil
298+ let hasProcess = rule [ " process " ] != nil
299+
300+ // If rule has no specific selectors and forwards by outbound/balancer,
301+ // it is effectively a catch-all.
302+ let hasForward = rule [ " outboundTag " ] != nil || rule [ " balancerTag " ] != nil
303+ return hasForward && ! hasDomain && ! hasIP && ! hasPort && ! hasProtocol && ! hasInboundTag && ! hasProcess
286304 }
287305
288306 private static func stringArray ( _ any : Any ? ) -> [ String ] {
0 commit comments