From 6fe3893bfd37865dc677111fa48aef14c0a41efd Mon Sep 17 00:00:00 2001 From: Eliot the Cougar Date: Tue, 6 Oct 2026 10:32:19 +0300 Subject: [PATCH] Fix policy group fallback isolation (#6283) Give least-ping and least-load groups their own first member as fallback so missing or failed probes cannot send a matched routing rule through the main profile. Keep least-load on its burst observatory and preserve random and round-robin fallback behavior. Cover group isolation and observatory selection with regression tests. --- .../com/v2ray/ang/core/CoreConfigManager.kt | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/core/CoreConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/core/CoreConfigManager.kt index b2e08d316..3c822db01 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/core/CoreConfigManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/core/CoreConfigManager.kt @@ -413,13 +413,7 @@ object CoreConfigManager { "${AppConfig.TAG_BALANCER_PRE}-${resolvedOutbound.tag}" } val strategyType = BalancerStrategyType.from(resolvedOutbound.profile.policyGroupType) - val fallbackTag = if (strategyType.supportsObservatory && resolvedOutbound.profile.policyGroupTestOutbounds != false) { - resolvedOutbound.profile.policyGroupFallbackTag - ?.takeIf { it.isNotEmpty() && it != AppConfig.TAG_PROXY } - // Xray excludes dead random/roundRobin candidates only when fallbackTag is set; - // without this default, an enabled empty field creates no observatory. - ?: membersToAdd.first().tag - } else null + val fallbackTag = resolvePolicyGroupFallbackTag(strategyType, resolvedOutbound.profile, membersToAdd.first().tag) val strategy = buildBalancerStrategy( strategyType = strategyType, selector = listOf(memberTagPrefix), @@ -435,6 +429,27 @@ object CoreConfigManager { policyGroupBalancerTags[resolvedOutbound.tag] = balancerTag } + internal fun resolvePolicyGroupFallbackTag( + strategyType: BalancerStrategyType, + profile: ProfileItem, + firstMemberTag: String, + ): String? { + if (strategyType.requiresObservatory || strategyType.requiresBurstObservatory) { + // Missing or failed probes must not let a matched rule use Xray's default outbound. + return firstMemberTag + } + return if (strategyType.supportsObservatory && profile.policyGroupTestOutbounds != false) { + profile.policyGroupFallbackTag + ?.takeIf { it.isNotEmpty() && it != AppConfig.TAG_PROXY } + // Xray excludes dead random/roundRobin candidates only when fallbackTag is set; + // without this default, an enabled empty field creates no observatory. + ?: firstMemberTag + } else null + } + + internal fun shouldUseStandardObservatory(strategyType: BalancerStrategyType, fallbackTag: String?): Boolean = + strategyType.requiresObservatory || (strategyType.supportsObservatory && fallbackTag != null) + /** * Trim runtime sections that are not needed for latency testing. */ @@ -1249,7 +1264,7 @@ object CoreConfigManager { fallbackTag = fallbackTag, strategy = V2rayConfig.RoutingBean.StrategyObject(type = strategyType.policyGroupType) ) - val observatory = if (strategyType.requiresObservatory || fallbackTag != null) { + val observatory = if (shouldUseStandardObservatory(strategyType, fallbackTag)) { V2rayConfig.ObservatoryObject( subjectSelector = selector, probeUrl = probeUrl,