Random and Round-Robin strategies can exclude dead outbounds (#5926)

* Add policy group fallback health checks

* Refine policy group observatory defaults

* Simplify policy group fallback

* Localize policy group fallback settings

* Update CoreConfigContextBuilder.kt

* Update CoreConfigManager.kt

* Restore implicit policy group fallback

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
This commit is contained in:
eliotcougarand2dust authored and GitHub committed 2026-07-24 10:20:16 +08:00
1 parent ce75aad55e
commit 2be47f37fe
14 files changed
+119 -12

No files matched your search

@@ -4,6 +4,7 @@ import android.content.Context
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.CoreConfigContext import com.v2ray.ang.dto.CoreConfigContext
import com.v2ray.ang.dto.entities.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.BalancerStrategyType
import com.v2ray.ang.enums.CoreResolvedType import com.v2ray.ang.enums.CoreResolvedType
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isComplexType import com.v2ray.ang.extension.isComplexType
@@ -43,12 +44,14 @@ object CoreConfigContextBuilder {
// Step 2: Resolve all non-builtin routing outbound tags. // Step 2: Resolve all non-builtin routing outbound tags.
val routingResolvedOutbounds = resolveRoutingOutbounds() val routingResolvedOutbounds = resolveRoutingOutbounds()
val resolvedOutbounds = listOf(primaryResolvedOutbound) + routingResolvedOutbounds
val fallbackResolvedOutbounds = resolveFallbackOutbounds(resolvedOutbounds)
val routingDomainRules = collectRoutingDomainRulesForDns() val routingDomainRules = collectRoutingDomainRulesForDns()
return CoreConfigContext( return CoreConfigContext(
context = context, context = context,
guid = guid, guid = guid,
resolvedOutbounds = listOf(primaryResolvedOutbound) + routingResolvedOutbounds, resolvedOutbounds = resolvedOutbounds + fallbackResolvedOutbounds,
routingDomainRules = routingDomainRules, routingDomainRules = routingDomainRules,
) )
} }
@@ -246,4 +249,25 @@ object CoreConfigContextBuilder {
return result return result
} }
/**
* Resolve and collect fallback outbounds from all POLICYGROUP nodes.
*
* Fallback targets must not overlap with already resolved tags or builtin tags.
*/
private fun resolveFallbackOutbounds(resolvedOutbounds: List<CoreConfigContext.ResolvedOutbound>): List<CoreConfigContext.ResolvedOutbound> {
return resolvedOutbounds
.asSequence()
.filter { it.resolvedType == CoreResolvedType.POLICYGROUP }
.filter { BalancerStrategyType.from(it.profile.policyGroupType).supportsObservatory && it.profile.policyGroupTestOutbounds != false }
.mapNotNull { it.profile.policyGroupFallbackTag }
.filter { it !in AppConfig.BUILTIN_OUTBOUND_TAGS && resolvedOutbounds.none { outbound -> outbound.tag == it } }
.distinct()
.mapNotNull { tag ->
SettingsManager.getServerViaRemarks(tag)
?.takeUnless { it.configType == EConfigType.CUSTOM || it.configType == EConfigType.POLICYGROUP }
?.let { resolveOutbound(tag, it) }
}
.toList()
}
} }
@@ -374,10 +374,19 @@ object CoreConfigManager {
} else { } else {
"${AppConfig.TAG_BALANCER_PRE}-${resolvedOutbound.tag}" "${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 strategy = buildBalancerStrategy( val strategy = buildBalancerStrategy(
policyGroupType = resolvedOutbound.profile.policyGroupType, strategyType = strategyType,
selector = listOf(memberTagPrefix), selector = listOf(memberTagPrefix),
balancerTag = balancerTag, balancerTag = balancerTag,
fallbackTag = fallbackTag,
) )
val existingBalancers = v2rayConfig.routing.balancers?.toMutableList() ?: mutableListOf() val existingBalancers = v2rayConfig.routing.balancers?.toMutableList() ?: mutableListOf()
if (existingBalancers.none { it.tag == balancerTag }) { if (existingBalancers.none { it.tag == balancerTag }) {
@@ -1180,9 +1189,10 @@ object CoreConfigManager {
* Build balancer and probe settings from one policy-group strategy value. * Build balancer and probe settings from one policy-group strategy value.
*/ */
private fun buildBalancerStrategy( private fun buildBalancerStrategy(
policyGroupType: String?, strategyType: BalancerStrategyType,
selector: List<String>, selector: List<String>,
balancerTag: String = AppConfig.TAG_BALANCER, balancerTag: String = AppConfig.TAG_BALANCER,
fallbackTag: String? = null,
): BalancerStrategy { ): BalancerStrategy {
val probeUrl = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DELAY_TEST_URL val probeUrl = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DELAY_TEST_URL
val leastPingInterval = decodeObservatoryDuration(AppConfig.PREF_OBSERVATORY_LEAST_PING_INTERVAL, AppConfig.OBSERVATORY_LEAST_PING_INTERVAL) val leastPingInterval = decodeObservatoryDuration(AppConfig.PREF_OBSERVATORY_LEAST_PING_INTERVAL, AppConfig.OBSERVATORY_LEAST_PING_INTERVAL)
@@ -1190,13 +1200,13 @@ object CoreConfigManager {
val leastLoadMethod = MmkvManager.decodeSettingsString(AppConfig.PREF_OBSERVATORY_LEAST_LOAD_METHOD, AppConfig.OBSERVATORY_LEAST_LOAD_METHOD) val leastLoadMethod = MmkvManager.decodeSettingsString(AppConfig.PREF_OBSERVATORY_LEAST_LOAD_METHOD, AppConfig.OBSERVATORY_LEAST_LOAD_METHOD)
val leastLoadSampling = decodeObservatorySampling() val leastLoadSampling = decodeObservatorySampling()
val leastLoadTimeout = decodeObservatoryDuration(AppConfig.PREF_OBSERVATORY_LEAST_LOAD_TIMEOUT, AppConfig.OBSERVATORY_LEAST_LOAD_TIMEOUT) val leastLoadTimeout = decodeObservatoryDuration(AppConfig.PREF_OBSERVATORY_LEAST_LOAD_TIMEOUT, AppConfig.OBSERVATORY_LEAST_LOAD_TIMEOUT)
val strategyType = BalancerStrategyType.from(policyGroupType)
val balancer = V2rayConfig.RoutingBean.BalancerBean( val balancer = V2rayConfig.RoutingBean.BalancerBean(
tag = balancerTag, tag = balancerTag,
selector = selector, selector = selector,
fallbackTag = fallbackTag,
strategy = V2rayConfig.RoutingBean.StrategyObject(type = strategyType.policyGroupType) strategy = V2rayConfig.RoutingBean.StrategyObject(type = strategyType.policyGroupType)
) )
val observatory = if (strategyType.requiresObservatory) { val observatory = if (strategyType.requiresObservatory || fallbackTag != null) {
V2rayConfig.ObservatoryObject( V2rayConfig.ObservatoryObject(
subjectSelector = selector, subjectSelector = selector,
probeUrl = probeUrl, probeUrl = probeUrl,
@@ -67,6 +67,8 @@ data class ProfileItem(
var policyGroupType: String? = null, var policyGroupType: String? = null,
var policyGroupSubscriptionId: String? = null, var policyGroupSubscriptionId: String? = null,
var policyGroupFilter: String? = null, var policyGroupFilter: String? = null,
var policyGroupTestOutbounds: Boolean? = null,
var policyGroupFallbackTag: String? = null,
var proxyChainProfiles: String? = null, var proxyChainProfiles: String? = null,
var browserDialerMode: String? = null, var browserDialerMode: String? = null,
@@ -7,20 +7,22 @@ package com.v2ray.ang.enums
* @param policyGroupTypeValue The numeric string stored in [com.v2ray.ang.dto.entities.ProfileItem.policyGroupType]. * @param policyGroupTypeValue The numeric string stored in [com.v2ray.ang.dto.entities.ProfileItem.policyGroupType].
* @param requiresBurstObservatory Whether this strategy needs a burstObservatory (leastLoad). * @param requiresBurstObservatory Whether this strategy needs a burstObservatory (leastLoad).
* @param requiresObservatory Whether this strategy needs an observatory (leastPing). * @param requiresObservatory Whether this strategy needs an observatory (leastPing).
* @param supportsObservatory Whether an observatory can health-filter this strategy.
*/ */
enum class BalancerStrategyType( enum class BalancerStrategyType(
val policyGroupType: String, val policyGroupType: String,
val policyGroupTypeValue: String, val policyGroupTypeValue: String,
val requiresBurstObservatory: Boolean = false, val requiresBurstObservatory: Boolean = false,
val requiresObservatory: Boolean = false, val requiresObservatory: Boolean = false,
val supportsObservatory: Boolean = false,
) { ) {
LEAST_LOAD("leastLoad", "1", requiresBurstObservatory = true), LEAST_LOAD("leastLoad", "1", requiresBurstObservatory = true),
RANDOM("random", "2"), RANDOM("random", "2", supportsObservatory = true),
ROUND_ROBIN("roundRobin", "3"), ROUND_ROBIN("roundRobin", "3", supportsObservatory = true),
LEAST_PING("leastPing", "", requiresObservatory = true); // default / else LEAST_PING("leastPing", "", requiresObservatory = true); // default / else
companion object { companion object {
fun from(policyGroupType: String?): BalancerStrategyType = fun from(policyGroupType: String?): BalancerStrategyType =
entries.firstOrNull { it.policyGroupTypeValue == policyGroupType } ?: LEAST_PING entries.firstOrNull { it.policyGroupTypeValue == policyGroupType } ?: LEAST_PING
} }
} }
@@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -21,17 +23,22 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.compose.AppTopBar import com.v2ray.ang.compose.AppTopBar
import com.v2ray.ang.compose.ConfirmDialog import com.v2ray.ang.compose.ConfirmDialog
import com.v2ray.ang.compose.FormDropdownField import com.v2ray.ang.compose.FormDropdownField
import com.v2ray.ang.compose.FormTextField import com.v2ray.ang.compose.FormTextField
import com.v2ray.ang.compose.SettingsSwitchItem
import com.v2ray.ang.dto.entities.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.BalancerStrategyType
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.ui.base.BaseComponentActivity import com.v2ray.ang.ui.base.BaseComponentActivity
class ServerGroupActivity : BaseComponentActivity() { class ServerGroupActivity : BaseComponentActivity() {
@@ -45,21 +52,32 @@ class ServerGroupActivity : BaseComponentActivity() {
private val subscriptionId by lazy { intent.getStringExtra("subscriptionId") } private val subscriptionId by lazy { intent.getStringExtra("subscriptionId") }
private val subIds = mutableListOf<String>() private val subIds = mutableListOf<String>()
private val subDisplay = mutableListOf<String>() private val subDisplay = mutableListOf<String>()
private lateinit var fallbackSuggestions: List<String>
private lateinit var initialRemarks: String private lateinit var initialRemarks: String
private lateinit var initialFilter: String private lateinit var initialFilter: String
private var initialType: Int = 0 private var initialType: Int = 0
private var initialSubIndex: Int = 0 private var initialSubIndex: Int = 0
private var initialTestOutbounds: Boolean = false
private lateinit var initialFallbackTag: String
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val config = MmkvManager.decodeServerConfig(editGuid) val config = MmkvManager.decodeServerConfig(editGuid)
populateSubscriptionSpinner() populateSubscriptionSpinner()
fallbackSuggestions = (
BUILTIN_OUTBOUND_TAGS + SettingsManager.getProfileRemarks(
excludeConfigTypes = setOf(EConfigType.CUSTOM, EConfigType.POLICYGROUP)
)
).filter { it != TAG_PROXY }
initialRemarks = config?.remarks ?: "" initialRemarks = config?.remarks ?: ""
initialFilter = config?.policyGroupFilter ?: "" initialFilter = config?.policyGroupFilter ?: ""
initialType = config?.policyGroupType?.toIntOrNull() ?: 0 initialType = config?.policyGroupType?.toIntOrNull() ?: 0
initialTestOutbounds = config == null || config.policyGroupTestOutbounds != false ||
!BalancerStrategyType.from(config.policyGroupType).supportsObservatory
initialFallbackTag = config?.policyGroupFallbackTag.orEmpty()
initialSubIndex = if (config != null) { initialSubIndex = if (config != null) {
subIds.indexOf(config.policyGroupSubscriptionId ?: "").let { if (it >= 0) it else 0 } subIds.indexOf(config.policyGroupSubscriptionId ?: "").let { if (it >= 0) it else 0 }
} else if (subscriptionId.isNotNullEmpty()) { } else if (subscriptionId.isNotNullEmpty()) {
@@ -77,8 +95,13 @@ class ServerGroupActivity : BaseComponentActivity() {
initialFilter = initialFilter, initialFilter = initialFilter,
initialType = initialType, initialType = initialType,
initialSubIndex = initialSubIndex, initialSubIndex = initialSubIndex,
initialTestOutbounds = initialTestOutbounds,
initialFallbackTag = initialFallbackTag,
fallbackSuggestions = fallbackSuggestions,
onBackClick = { finish() }, onBackClick = { finish() },
onSave = { remarks, filter, typeIdx, subIdx -> saveServer(remarks, filter, typeIdx, subIdx) }, onSave = { remarks, filter, typeIdx, subIdx, testOutbounds, fallbackTag ->
saveServer(remarks, filter, typeIdx, subIdx, testOutbounds, fallbackTag)
},
onDelete = { deleteServer() } onDelete = { deleteServer() }
) )
} }
@@ -87,7 +110,9 @@ class ServerGroupActivity : BaseComponentActivity() {
remarks: String, remarks: String,
filter: String, filter: String,
typeIdx: Int, typeIdx: Int,
subIdx: Int subIdx: Int,
testOutbounds: Boolean,
fallbackTag: String,
): Boolean { ): Boolean {
if (remarks.isBlank()) { if (remarks.isBlank()) {
toast(R.string.server_lab_remarks) toast(R.string.server_lab_remarks)
@@ -103,6 +128,8 @@ class ServerGroupActivity : BaseComponentActivity() {
config.policyGroupType = typeIdx.toString() config.policyGroupType = typeIdx.toString()
config.policyGroupSubscriptionId = config.policyGroupSubscriptionId =
subIds.getOrNull(subIdx) subIds.getOrNull(subIdx)
config.policyGroupTestOutbounds = testOutbounds
config.policyGroupFallbackTag = fallbackTag.trim().takeIf { it.isNotEmpty() }
if ( if (
config.subscriptionId.isEmpty() && config.subscriptionId.isEmpty() &&
@@ -189,8 +216,11 @@ fun ServerGroupScreen(
initialFilter: String, initialFilter: String,
initialType: Int, initialType: Int,
initialSubIndex: Int, initialSubIndex: Int,
initialTestOutbounds: Boolean,
initialFallbackTag: String,
fallbackSuggestions: List<String>,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onSave: (String, String, Int, Int) -> Boolean, onSave: (String, String, Int, Int, Boolean, String) -> Boolean,
onDelete: () -> Unit onDelete: () -> Unit
) { ) {
val typeEntries = stringArrayResource(R.array.policy_group_type).toList() val typeEntries = stringArrayResource(R.array.policy_group_type).toList()
@@ -199,8 +229,12 @@ fun ServerGroupScreen(
var filter by rememberSaveable { mutableStateOf(initialFilter) } var filter by rememberSaveable { mutableStateOf(initialFilter) }
var typeValue by rememberSaveable { mutableStateOf(typeEntries.getOrNull(initialType).orEmpty()) } var typeValue by rememberSaveable { mutableStateOf(typeEntries.getOrNull(initialType).orEmpty()) }
var subValue by rememberSaveable { mutableStateOf(subDisplay.getOrNull(initialSubIndex).orEmpty()) } var subValue by rememberSaveable { mutableStateOf(subDisplay.getOrNull(initialSubIndex).orEmpty()) }
var testOutbounds by rememberSaveable { mutableStateOf(initialTestOutbounds) }
var fallbackTag by rememberSaveable { mutableStateOf(initialFallbackTag) }
var showDeleteConfirm by remember { mutableStateOf(false) } var showDeleteConfirm by remember { mutableStateOf(false) }
val showDelete = editGuid.isNotEmpty() && !isRunning val showDelete = editGuid.isNotEmpty() && !isRunning
val selectedType = typeEntries.indexOf(typeValue).coerceAtLeast(0).toString()
val supportsObservatory = BalancerStrategyType.from(selectedType).supportsObservatory
Scaffold( Scaffold(
contentWindowInsets = ScaffoldDefaults.contentWindowInsets, contentWindowInsets = ScaffoldDefaults.contentWindowInsets,
@@ -217,7 +251,7 @@ fun ServerGroupScreen(
IconButton(onClick = { IconButton(onClick = {
val typeIdx = typeEntries.indexOf(typeValue).coerceAtLeast(0) val typeIdx = typeEntries.indexOf(typeValue).coerceAtLeast(0)
val subIdx = subDisplay.indexOf(subValue).coerceAtLeast(0) val subIdx = subDisplay.indexOf(subValue).coerceAtLeast(0)
onSave(remarks, filter, typeIdx, subIdx) onSave(remarks, filter, typeIdx, subIdx, testOutbounds, fallbackTag)
}) { }) {
Icon(painterResource(R.drawable.ic_fab_check), contentDescription = stringResource(R.string.menu_item_save_config)) Icon(painterResource(R.drawable.ic_fab_check), contentDescription = stringResource(R.string.menu_item_save_config))
} }
@@ -232,6 +266,7 @@ fun ServerGroupScreen(
.consumeWindowInsets(innerPadding) .consumeWindowInsets(innerPadding)
.imePadding() .imePadding()
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.verticalScroll(rememberScrollState())
) { ) {
FormTextField(stringResource(R.string.server_lab_remarks), remarks, { remarks = it }) FormTextField(stringResource(R.string.server_lab_remarks), remarks, { remarks = it })
FormDropdownField( FormDropdownField(
@@ -247,6 +282,22 @@ fun ServerGroupScreen(
onValueChange = { subValue = it } onValueChange = { subValue = it }
) )
FormTextField(stringResource(R.string.title_policy_group_subscription_filter), filter, { filter = it }) FormTextField(stringResource(R.string.title_policy_group_subscription_filter), filter, { filter = it })
if (supportsObservatory) {
SettingsSwitchItem(
title = stringResource(R.string.title_policy_group_test_outbounds),
checked = testOutbounds,
onCheckedChange = { testOutbounds = it }
)
if (testOutbounds) {
FormDropdownField(
label = stringResource(R.string.title_policy_group_fallback),
value = fallbackTag,
options = fallbackSuggestions,
onValueChange = { fallbackTag = it },
editable = true
)
}
}
} }
} }
@@ -387,6 +387,8 @@
<string name="title_policy_group_type">Policy group type</string> <string name="title_policy_group_type">Policy group type</string>
<string name="title_policy_group_subscription_id">From subscription group</string> <string name="title_policy_group_subscription_id">From subscription group</string>
<string name="title_policy_group_subscription_filter">Remarks regular filter</string> <string name="title_policy_group_subscription_filter">Remarks regular filter</string>
<string name="title_policy_group_test_outbounds">اختبار المخارج</string>
<string name="title_policy_group_fallback">المخرج الاحتياطي (اختياري)</string>
<string name="server_proxy_chain_members">اعضاء سلسلة الوكيل</string> <string name="server_proxy_chain_members">اعضاء سلسلة الوكيل</string>
<string name="server_proxy_chain_pick_members">اضغط هنا لاختيار عضو</string> <string name="server_proxy_chain_pick_members">اضغط هنا لاختيار عضو</string>
<string name="server_proxy_chain_member_unselected">اختر عضوا</string> <string name="server_proxy_chain_member_unselected">اختر عضوا</string>
@@ -386,6 +386,8 @@
<string name="title_policy_group_type">Policy group type</string> <string name="title_policy_group_type">Policy group type</string>
<string name="title_policy_group_subscription_id">From subscription group</string> <string name="title_policy_group_subscription_id">From subscription group</string>
<string name="title_policy_group_subscription_filter">Remarks regular filter</string> <string name="title_policy_group_subscription_filter">Remarks regular filter</string>
<string name="title_policy_group_test_outbounds">আউটবাউন্ড পরীক্ষা করুন</string>
<string name="title_policy_group_fallback">ফলব্যাক আউটবাউন্ড (ঐচ্ছিক)</string>
<string name="server_proxy_chain_members">Proxy chain members</string> <string name="server_proxy_chain_members">Proxy chain members</string>
<string name="server_proxy_chain_pick_members">Tap here to pick member</string> <string name="server_proxy_chain_pick_members">Tap here to pick member</string>
<string name="server_proxy_chain_member_unselected">Select a member</string> <string name="server_proxy_chain_member_unselected">Select a member</string>
@@ -392,6 +392,8 @@
<string name="title_policy_group_type">نوع بونکۊ سیاست</string> <string name="title_policy_group_type">نوع بونکۊ سیاست</string>
<string name="title_policy_group_subscription_id">ز بونکۊ اشتراک</string> <string name="title_policy_group_subscription_id">ز بونکۊ اشتراک</string>
<string name="title_policy_group_subscription_filter">توزیهات فیلتر معمۊلی</string> <string name="title_policy_group_subscription_filter">توزیهات فیلتر معمۊلی</string>
<string name="title_policy_group_test_outbounds">آزمایش بارتا</string>
<string name="title_policy_group_fallback">بارت جایگزین (اختیاری)</string>
<string name="server_proxy_chain_members">ٱئزا زنجیره پروکسی</string> <string name="server_proxy_chain_members">ٱئزا زنجیره پروکسی</string>
<string name="server_proxy_chain_pick_members">سی پسند عوزو ایچو بزنین</string> <string name="server_proxy_chain_pick_members">سی پسند عوزو ایچو بزنین</string>
<string name="server_proxy_chain_member_unselected">ی عوزو پسند کۊنین</string> <string name="server_proxy_chain_member_unselected">ی عوزو پسند کۊنین</string>
@@ -383,6 +383,8 @@
<string name="title_policy_group_type">نوع گروه خط مشی</string> <string name="title_policy_group_type">نوع گروه خط مشی</string>
<string name="title_policy_group_subscription_id">از گروه اشتراک</string> <string name="title_policy_group_subscription_id">از گروه اشتراک</string>
<string name="title_policy_group_subscription_filter">توضیحات فیلتر معمولی</string> <string name="title_policy_group_subscription_filter">توضیحات فیلتر معمولی</string>
<string name="title_policy_group_test_outbounds">آزمایش خروجی‌ها</string>
<string name="title_policy_group_fallback">خروجی جایگزین (اختیاری)</string>
<string name="server_proxy_chain_members">اعضای زنجیره پروکسی</string> <string name="server_proxy_chain_members">اعضای زنجیره پروکسی</string>
<string name="server_proxy_chain_pick_members">برای انتخاب عضو اینجا بزنید</string> <string name="server_proxy_chain_pick_members">برای انتخاب عضو اینجا بزنید</string>
<string name="server_proxy_chain_member_unselected">یک عضو را انتخاب کنید</string> <string name="server_proxy_chain_member_unselected">یک عضو را انتخاب کنید</string>
@@ -398,6 +398,8 @@
<string name="title_policy_group_type">Тип политики группы</string> <string name="title_policy_group_type">Тип политики группы</string>
<string name="title_policy_group_subscription_id">Из группы подписки</string> <string name="title_policy_group_subscription_id">Из группы подписки</string>
<string name="title_policy_group_subscription_filter">Название фильтра</string> <string name="title_policy_group_subscription_filter">Название фильтра</string>
<string name="title_policy_group_test_outbounds">Проверять исходящие соединения</string>
<string name="title_policy_group_fallback">Резервное исходящее соединение (необязательно)</string>
<string name="server_proxy_chain_members">Участники цепочки прокси</string> <string name="server_proxy_chain_members">Участники цепочки прокси</string>
<string name="server_proxy_chain_pick_members">Нажмите здесь, чтобы выбрать участника</string> <string name="server_proxy_chain_pick_members">Нажмите здесь, чтобы выбрать участника</string>
@@ -388,6 +388,8 @@
<string name="title_policy_group_type">Policy group type</string> <string name="title_policy_group_type">Policy group type</string>
<string name="title_policy_group_subscription_id">From subscription group</string> <string name="title_policy_group_subscription_id">From subscription group</string>
<string name="title_policy_group_subscription_filter">Remarks regular filter</string> <string name="title_policy_group_subscription_filter">Remarks regular filter</string>
<string name="title_policy_group_test_outbounds">Kiểm tra outbound</string>
<string name="title_policy_group_fallback">Outbound dự phòng (tùy chọn)</string>
<string name="server_proxy_chain_members">Proxy chain members</string> <string name="server_proxy_chain_members">Proxy chain members</string>
<string name="server_proxy_chain_pick_members">Tap here to pick member</string> <string name="server_proxy_chain_pick_members">Tap here to pick member</string>
<string name="server_proxy_chain_member_unselected">Select a member</string> <string name="server_proxy_chain_member_unselected">Select a member</string>
@@ -402,6 +402,8 @@
<string name="title_policy_group_type">策略组类型</string> <string name="title_policy_group_type">策略组类型</string>
<string name="title_policy_group_subscription_id">来自订阅分组</string> <string name="title_policy_group_subscription_id">来自订阅分组</string>
<string name="title_policy_group_subscription_filter">别名正则过滤</string> <string name="title_policy_group_subscription_filter">别名正则过滤</string>
<string name="title_policy_group_test_outbounds">测试出站</string>
<string name="title_policy_group_fallback">备用出站(可选)</string>
<string name="server_proxy_chain_members">代理链成员</string> <string name="server_proxy_chain_members">代理链成员</string>
<string name="server_proxy_chain_pick_members">点此选择成员</string> <string name="server_proxy_chain_pick_members">点此选择成员</string>
<string name="server_proxy_chain_member_unselected">请选择成员</string> <string name="server_proxy_chain_member_unselected">请选择成员</string>
@@ -400,6 +400,8 @@
<string name="title_policy_group_type">策略群組類型</string> <string name="title_policy_group_type">策略群組類型</string>
<string name="title_policy_group_subscription_id">來自訂閱分組</string> <string name="title_policy_group_subscription_id">來自訂閱分組</string>
<string name="title_policy_group_subscription_filter">別名正規過濾</string> <string name="title_policy_group_subscription_filter">別名正規過濾</string>
<string name="title_policy_group_test_outbounds">測試出口</string>
<string name="title_policy_group_fallback">備用出口(選填)</string>
<string name="server_proxy_chain_members">代理鏈成員</string> <string name="server_proxy_chain_members">代理鏈成員</string>
<string name="server_proxy_chain_pick_members">點此選擇成員</string> <string name="server_proxy_chain_pick_members">點此選擇成員</string>
<string name="server_proxy_chain_member_unselected">請選擇成員</string> <string name="server_proxy_chain_member_unselected">請選擇成員</string>
@@ -407,6 +407,8 @@
<string name="title_policy_group_type">Policy group type</string> <string name="title_policy_group_type">Policy group type</string>
<string name="title_policy_group_subscription_id">From subscription group</string> <string name="title_policy_group_subscription_id">From subscription group</string>
<string name="title_policy_group_subscription_filter">Remarks regular filter</string> <string name="title_policy_group_subscription_filter">Remarks regular filter</string>
<string name="title_policy_group_test_outbounds">Test outbounds</string>
<string name="title_policy_group_fallback">Fallback outbound (optional)</string>
<string name="server_proxy_chain_members">Proxy chain members</string> <string name="server_proxy_chain_members">Proxy chain members</string>
<string name="server_proxy_chain_pick_members">Tap here to pick member</string> <string name="server_proxy_chain_pick_members">Tap here to pick member</string>