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:
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.dto.CoreConfigContext
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.BalancerStrategyType
|
||||
import com.v2ray.ang.enums.CoreResolvedType
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.isComplexType
|
||||
@@ -43,12 +44,14 @@ object CoreConfigContextBuilder {
|
||||
|
||||
// Step 2: Resolve all non-builtin routing outbound tags.
|
||||
val routingResolvedOutbounds = resolveRoutingOutbounds()
|
||||
val resolvedOutbounds = listOf(primaryResolvedOutbound) + routingResolvedOutbounds
|
||||
val fallbackResolvedOutbounds = resolveFallbackOutbounds(resolvedOutbounds)
|
||||
val routingDomainRules = collectRoutingDomainRulesForDns()
|
||||
|
||||
return CoreConfigContext(
|
||||
context = context,
|
||||
guid = guid,
|
||||
resolvedOutbounds = listOf(primaryResolvedOutbound) + routingResolvedOutbounds,
|
||||
resolvedOutbounds = resolvedOutbounds + fallbackResolvedOutbounds,
|
||||
routingDomainRules = routingDomainRules,
|
||||
)
|
||||
}
|
||||
@@ -246,4 +249,25 @@ object CoreConfigContextBuilder {
|
||||
|
||||
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 {
|
||||
"${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(
|
||||
policyGroupType = resolvedOutbound.profile.policyGroupType,
|
||||
strategyType = strategyType,
|
||||
selector = listOf(memberTagPrefix),
|
||||
balancerTag = balancerTag,
|
||||
fallbackTag = fallbackTag,
|
||||
)
|
||||
val existingBalancers = v2rayConfig.routing.balancers?.toMutableList() ?: mutableListOf()
|
||||
if (existingBalancers.none { it.tag == balancerTag }) {
|
||||
@@ -1180,9 +1189,10 @@ object CoreConfigManager {
|
||||
* Build balancer and probe settings from one policy-group strategy value.
|
||||
*/
|
||||
private fun buildBalancerStrategy(
|
||||
policyGroupType: String?,
|
||||
strategyType: BalancerStrategyType,
|
||||
selector: List<String>,
|
||||
balancerTag: String = AppConfig.TAG_BALANCER,
|
||||
fallbackTag: String? = null,
|
||||
): BalancerStrategy {
|
||||
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)
|
||||
@@ -1190,13 +1200,13 @@ object CoreConfigManager {
|
||||
val leastLoadMethod = MmkvManager.decodeSettingsString(AppConfig.PREF_OBSERVATORY_LEAST_LOAD_METHOD, AppConfig.OBSERVATORY_LEAST_LOAD_METHOD)
|
||||
val leastLoadSampling = decodeObservatorySampling()
|
||||
val leastLoadTimeout = decodeObservatoryDuration(AppConfig.PREF_OBSERVATORY_LEAST_LOAD_TIMEOUT, AppConfig.OBSERVATORY_LEAST_LOAD_TIMEOUT)
|
||||
val strategyType = BalancerStrategyType.from(policyGroupType)
|
||||
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
||||
tag = balancerTag,
|
||||
selector = selector,
|
||||
fallbackTag = fallbackTag,
|
||||
strategy = V2rayConfig.RoutingBean.StrategyObject(type = strategyType.policyGroupType)
|
||||
)
|
||||
val observatory = if (strategyType.requiresObservatory) {
|
||||
val observatory = if (strategyType.requiresObservatory || fallbackTag != null) {
|
||||
V2rayConfig.ObservatoryObject(
|
||||
subjectSelector = selector,
|
||||
probeUrl = probeUrl,
|
||||
|
||||
@@ -67,6 +67,8 @@ data class ProfileItem(
|
||||
var policyGroupType: String? = null,
|
||||
var policyGroupSubscriptionId: String? = null,
|
||||
var policyGroupFilter: String? = null,
|
||||
var policyGroupTestOutbounds: Boolean? = null,
|
||||
var policyGroupFallbackTag: String? = null,
|
||||
var proxyChainProfiles: 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 requiresBurstObservatory Whether this strategy needs a burstObservatory (leastLoad).
|
||||
* @param requiresObservatory Whether this strategy needs an observatory (leastPing).
|
||||
* @param supportsObservatory Whether an observatory can health-filter this strategy.
|
||||
*/
|
||||
enum class BalancerStrategyType(
|
||||
val policyGroupType: String,
|
||||
val policyGroupTypeValue: String,
|
||||
val requiresBurstObservatory: Boolean = false,
|
||||
val requiresObservatory: Boolean = false,
|
||||
val supportsObservatory: Boolean = false,
|
||||
) {
|
||||
LEAST_LOAD("leastLoad", "1", requiresBurstObservatory = true),
|
||||
RANDOM("random", "2"),
|
||||
ROUND_ROBIN("roundRobin", "3"),
|
||||
RANDOM("random", "2", supportsObservatory = true),
|
||||
ROUND_ROBIN("roundRobin", "3", supportsObservatory = true),
|
||||
LEAST_PING("leastPing", "", requiresObservatory = true); // default / else
|
||||
|
||||
companion object {
|
||||
fun from(policyGroupType: String?): BalancerStrategyType =
|
||||
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.imePadding
|
||||
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.IconButton
|
||||
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.stringResource
|
||||
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.compose.AppTopBar
|
||||
import com.v2ray.ang.compose.ConfirmDialog
|
||||
import com.v2ray.ang.compose.FormDropdownField
|
||||
import com.v2ray.ang.compose.FormTextField
|
||||
import com.v2ray.ang.compose.SettingsSwitchItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.BalancerStrategyType
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.ui.base.BaseComponentActivity
|
||||
|
||||
class ServerGroupActivity : BaseComponentActivity() {
|
||||
@@ -45,21 +52,32 @@ class ServerGroupActivity : BaseComponentActivity() {
|
||||
private val subscriptionId by lazy { intent.getStringExtra("subscriptionId") }
|
||||
private val subIds = mutableListOf<String>()
|
||||
private val subDisplay = mutableListOf<String>()
|
||||
private lateinit var fallbackSuggestions: List<String>
|
||||
|
||||
private lateinit var initialRemarks: String
|
||||
private lateinit var initialFilter: String
|
||||
private var initialType: Int = 0
|
||||
private var initialSubIndex: Int = 0
|
||||
private var initialTestOutbounds: Boolean = false
|
||||
private lateinit var initialFallbackTag: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val config = MmkvManager.decodeServerConfig(editGuid)
|
||||
populateSubscriptionSpinner()
|
||||
fallbackSuggestions = (
|
||||
BUILTIN_OUTBOUND_TAGS + SettingsManager.getProfileRemarks(
|
||||
excludeConfigTypes = setOf(EConfigType.CUSTOM, EConfigType.POLICYGROUP)
|
||||
)
|
||||
).filter { it != TAG_PROXY }
|
||||
|
||||
initialRemarks = config?.remarks ?: ""
|
||||
initialFilter = config?.policyGroupFilter ?: ""
|
||||
initialType = config?.policyGroupType?.toIntOrNull() ?: 0
|
||||
initialTestOutbounds = config == null || config.policyGroupTestOutbounds != false ||
|
||||
!BalancerStrategyType.from(config.policyGroupType).supportsObservatory
|
||||
initialFallbackTag = config?.policyGroupFallbackTag.orEmpty()
|
||||
initialSubIndex = if (config != null) {
|
||||
subIds.indexOf(config.policyGroupSubscriptionId ?: "").let { if (it >= 0) it else 0 }
|
||||
} else if (subscriptionId.isNotNullEmpty()) {
|
||||
@@ -77,8 +95,13 @@ class ServerGroupActivity : BaseComponentActivity() {
|
||||
initialFilter = initialFilter,
|
||||
initialType = initialType,
|
||||
initialSubIndex = initialSubIndex,
|
||||
initialTestOutbounds = initialTestOutbounds,
|
||||
initialFallbackTag = initialFallbackTag,
|
||||
fallbackSuggestions = fallbackSuggestions,
|
||||
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() }
|
||||
)
|
||||
}
|
||||
@@ -87,7 +110,9 @@ class ServerGroupActivity : BaseComponentActivity() {
|
||||
remarks: String,
|
||||
filter: String,
|
||||
typeIdx: Int,
|
||||
subIdx: Int
|
||||
subIdx: Int,
|
||||
testOutbounds: Boolean,
|
||||
fallbackTag: String,
|
||||
): Boolean {
|
||||
if (remarks.isBlank()) {
|
||||
toast(R.string.server_lab_remarks)
|
||||
@@ -103,6 +128,8 @@ class ServerGroupActivity : BaseComponentActivity() {
|
||||
config.policyGroupType = typeIdx.toString()
|
||||
config.policyGroupSubscriptionId =
|
||||
subIds.getOrNull(subIdx)
|
||||
config.policyGroupTestOutbounds = testOutbounds
|
||||
config.policyGroupFallbackTag = fallbackTag.trim().takeIf { it.isNotEmpty() }
|
||||
|
||||
if (
|
||||
config.subscriptionId.isEmpty() &&
|
||||
@@ -189,8 +216,11 @@ fun ServerGroupScreen(
|
||||
initialFilter: String,
|
||||
initialType: Int,
|
||||
initialSubIndex: Int,
|
||||
initialTestOutbounds: Boolean,
|
||||
initialFallbackTag: String,
|
||||
fallbackSuggestions: List<String>,
|
||||
onBackClick: () -> Unit,
|
||||
onSave: (String, String, Int, Int) -> Boolean,
|
||||
onSave: (String, String, Int, Int, Boolean, String) -> Boolean,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
val typeEntries = stringArrayResource(R.array.policy_group_type).toList()
|
||||
@@ -199,8 +229,12 @@ fun ServerGroupScreen(
|
||||
var filter by rememberSaveable { mutableStateOf(initialFilter) }
|
||||
var typeValue by rememberSaveable { mutableStateOf(typeEntries.getOrNull(initialType).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) }
|
||||
val showDelete = editGuid.isNotEmpty() && !isRunning
|
||||
val selectedType = typeEntries.indexOf(typeValue).coerceAtLeast(0).toString()
|
||||
val supportsObservatory = BalancerStrategyType.from(selectedType).supportsObservatory
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = ScaffoldDefaults.contentWindowInsets,
|
||||
@@ -217,7 +251,7 @@ fun ServerGroupScreen(
|
||||
IconButton(onClick = {
|
||||
val typeIdx = typeEntries.indexOf(typeValue).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))
|
||||
}
|
||||
@@ -232,6 +266,7 @@ fun ServerGroupScreen(
|
||||
.consumeWindowInsets(innerPadding)
|
||||
.imePadding()
|
||||
.padding(vertical = 8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
FormTextField(stringResource(R.string.server_lab_remarks), remarks, { remarks = it })
|
||||
FormDropdownField(
|
||||
@@ -247,6 +282,22 @@ fun ServerGroupScreen(
|
||||
onValueChange = { subValue = 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_subscription_id">From subscription group</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_pick_members">اضغط هنا لاختيار عضو</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_subscription_id">From subscription group</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_pick_members">Tap here to pick 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_subscription_id">ز بونکۊ اشتراک</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_pick_members">سی پسند عوزو ایچو بزنین</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_subscription_id">از گروه اشتراک</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_pick_members">برای انتخاب عضو اینجا بزنید</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_subscription_id">Из группы подписки</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_pick_members">Нажмите здесь, чтобы выбрать участника</string>
|
||||
|
||||
@@ -388,6 +388,8 @@
|
||||
<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_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_pick_members">Tap here to pick 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_subscription_id">来自订阅分组</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_pick_members">点此选择成员</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_subscription_id">來自訂閱分組</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_pick_members">點此選擇成員</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_subscription_id">From subscription group</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_pick_members">Tap here to pick member</string>
|
||||
|
||||
Reference in new issue
Block a user