From 3e823a7440c7863bad7e3c22557932c4fca406b7 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Sat, 2 May 2026 14:47:03 +0800 Subject: [PATCH] Pre-resolve and inject custom outbounds Pre-resolve outbound profiles referenced by routing rules and pass them through the runtime context so they can be injected into generated V2ray configs. Key changes: - Added customOutboundProfiles: Map to CoreConfigContext. - CoreConfigContextBuilder: new resolveCustomOutbounds() to collect non-builtin outbound tags from routing rulesets and build the map; builder now populates customOutboundProfiles and simplifies resolvedProfiles/type handling. - CoreConfigManager: refactored many APIs to accept CoreConfigContext instead of raw Context/guid (toConfigResult, initV2rayConfig, getRouting, getOutbounds, getBalance, etc.), and uses the pre-resolved customOutboundProfiles when injecting custom outbounds. - injectCustomOutbounds now accepts the pre-resolved map and converts ProfileItem -> Outbound only for missing tags. - Added logging/error handling and minor tidying to avoid repeated lookups and ensure custom outbounds are available before routing is applied. Rationale: ensure outbounds referenced by rulesets are available early in the build pipeline, reduce duplicate lookups, and centralize runtime data in CoreConfigContext. --- .../ang/core/CoreConfigContextBuilder.kt | 77 +++++++++--- .../com/v2ray/ang/core/CoreConfigManager.kt | 114 ++++++++---------- .../com/v2ray/ang/dto/CoreConfigContext.kt | 1 + 3 files changed, 113 insertions(+), 79 deletions(-) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/core/CoreConfigContextBuilder.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/core/CoreConfigContextBuilder.kt index 564d9850..d0e87592 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/core/CoreConfigContextBuilder.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/core/CoreConfigContextBuilder.kt @@ -9,6 +9,7 @@ import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.util.LogUtil import com.v2ray.ang.util.Utils /** @@ -20,38 +21,42 @@ object CoreConfigContextBuilder { /** Loads profile by guid and returns a resolved runtime context. */ fun build(context: Context, guid: String): CoreConfigContext? { val config = MmkvManager.decodeServerConfig(guid) ?: return null - - return when (config.configType) { - EConfigType.CUSTOM -> CoreConfigContext( + if (config.configType == EConfigType.CUSTOM) { + return CoreConfigContext( context = context, guid = guid, selectedProfile = config, resolvedProfiles = listOf(config), resolvedType = CoreResolvedType.CUSTOM, ) + } + // Pre-resolve custom outbound profiles from routing rulesets + val customOutbounds = resolveCustomOutbounds() + + // Determine resolved profiles and type based on config type + val (resolvedProfiles, resolvedType) = when (config.configType) { EConfigType.POLICYGROUP -> { val profiles = resolvePolicyGroupProfiles(config) - CoreConfigContext( - context = context, - guid = guid, - selectedProfile = config, - resolvedProfiles = profiles, - resolvedType = CoreResolvedType.POLICYGROUP, - ) + Pair(profiles, CoreResolvedType.POLICYGROUP) } else -> { val chainProfiles = resolveProxyChainProfiles(config) - CoreConfigContext( - context = context, - guid = guid, - selectedProfile = config, - resolvedProfiles = chainProfiles, - resolvedType = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN, - ) + val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN + Pair(chainProfiles, type) } } + + // Create context with common fields + return CoreConfigContext( + context = context, + guid = guid, + selectedProfile = config, + resolvedProfiles = resolvedProfiles, + resolvedType = resolvedType, + customOutboundProfiles = customOutbounds, + ) } /** Resolves policy-group members with the same filters as runtime build. */ @@ -110,5 +115,43 @@ object CoreConfigContextBuilder { return resolved } + + /** + * Resolves custom outbound profiles from routing rulesets. + * Scans rulesets for non-builtin outbound tags and looks up matching profiles by remarks. + * Returns a map of tag -> ProfileItem for independent processing. + */ + private fun resolveCustomOutbounds(): Map { + val customMap = mutableMapOf() + val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return customMap + + try { + val processedTags = mutableSetOf() + + rulesetItems + .filter { it.enabled } + .mapNotNull { it.outboundTag.takeIf { tag -> tag.isNotBlank() } } + .filter { tag -> tag !in AppConfig.BUILTIN_OUTBOUND_TAGS } + .distinct() + .forEach { tag -> + if (tag in processedTags) return@forEach + processedTags.add(tag) + + try { + val profile = SettingsManager.getServerViaRemarks(tag) ?: run { + return@forEach + } + + customMap[tag] = profile + } catch (e: Exception) { + LogUtil.e(AppConfig.TAG, "Failed to resolve custom outbound for tag '$tag', skipping", e) + } + } + } catch (e: Exception) { + LogUtil.e(AppConfig.TAG, "Failed to resolve custom outbound profiles", e) + } + + return customMap + } } 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 a81097e7..f15a9821 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 @@ -46,7 +46,7 @@ object CoreConfigManager { CoreResolvedType.CUSTOM -> null } ?: return ConfigResult(false) - return toConfigResult(configContext.guid, v2rayConfig) + return toConfigResult(configContext, v2rayConfig) } catch (e: Exception) { LogUtil.e(AppConfig.TAG, "Failed to get V2ray config", e) return ConfigResult(false) @@ -78,7 +78,7 @@ object CoreConfigManager { postProcessForSpeedtest(v2rayConfig) - return toConfigResult(configContext.guid, v2rayConfig) + return toConfigResult(configContext, v2rayConfig) } catch (e: Exception) { LogUtil.e(AppConfig.TAG, "Failed to get V2ray config for speedtest", e) return ConfigResult(false) @@ -90,9 +90,8 @@ object CoreConfigManager { */ private fun getV2rayCustomConfig(configContext: CoreConfigContext): ConfigResult { val context = configContext.context - val guid = configContext.guid - val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false) - val result = ConfigResult(true, guid, raw) + val raw = MmkvManager.decodeServerRaw(configContext.guid) ?: return ConfigResult(false) + val result = ConfigResult(true, configContext.guid, raw) if (!needTun()) { return result } @@ -128,7 +127,7 @@ object CoreConfigManager { if (tunNotExists) { // add tun inbound from template - initV2rayConfig(context)?.let { templateConfig -> + initV2rayConfig(configContext)?.let { templateConfig -> templateConfig.inbounds.firstOrNull { it.tag == "tun" }?.let { inboundTun -> inboundTun.settings?.mtu = SettingsManager.getVpnMtu() inboundsJson.add(JsonUtil.parseString(JsonUtil.toJson(inboundTun))) @@ -136,12 +135,11 @@ object CoreConfigManager { } } - return JsonUtil.toJsonPretty(json)?.let { ConfigResult(true, guid, it) } ?: result + return JsonUtil.toJsonPretty(json)?.let { ConfigResult(true, configContext.guid, it) } ?: result } /** Builds full config for policy-group mode. */ private fun buildGroupConfig(configContext: CoreConfigContext): V2rayConfig? { - val context = configContext.context val config = configContext.selectedProfile val validConfigs = configContext.resolvedProfiles @@ -150,7 +148,7 @@ object CoreConfigManager { return null } - val v2rayConfig = initV2rayConfig(context) ?: return null + val v2rayConfig = initV2rayConfig(configContext) ?: return null v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning" v2rayConfig.remarks = config.remarks @@ -168,10 +166,10 @@ object CoreConfigManager { outboundsList.addAll(v2rayConfig.outbounds) v2rayConfig.outbounds = ArrayList(outboundsList) - getRouting(context, v2rayConfig) + getRouting(configContext, v2rayConfig) getFakeDns(v2rayConfig) getDns(v2rayConfig) - getBalance(v2rayConfig, config) + getBalance(configContext, v2rayConfig) if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) { getCustomLocalDns(v2rayConfig) @@ -196,7 +194,6 @@ object CoreConfigManager { * with dialerProxy. */ private fun buildProxyChainConfig(configContext: CoreConfigContext): V2rayConfig? { - val context = configContext.context val config = configContext.selectedProfile val resolvedProfiles = configContext.resolvedProfiles @@ -206,7 +203,7 @@ object CoreConfigManager { return null } - val v2rayConfig = initV2rayConfig(context) ?: return null + val v2rayConfig = initV2rayConfig(configContext) ?: return null v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning" v2rayConfig.remarks = config.remarks @@ -234,7 +231,7 @@ object CoreConfigManager { } v2rayConfig.outbounds = ArrayList(chainOutbounds + builtinOutbounds) - getRouting(context, v2rayConfig) + getRouting(configContext, v2rayConfig) getFakeDns(v2rayConfig) getDns(v2rayConfig) @@ -256,7 +253,6 @@ object CoreConfigManager { /** Builds full config for normal single-node mode. */ private fun buildNormalConfig(configContext: CoreConfigContext): V2rayConfig? { - val context = configContext.context val config = configContext.selectedProfile val address = config.server ?: return null @@ -265,13 +261,13 @@ object CoreConfigManager { return null } - val v2rayConfig = initV2rayConfig(context) ?: return null + val v2rayConfig = initV2rayConfig(configContext) ?: return null v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning" v2rayConfig.remarks = config.remarks getInbounds(v2rayConfig) - getOutbounds(v2rayConfig, config) ?: return null - getRouting(context, v2rayConfig) + getOutbounds(configContext, v2rayConfig) ?: return null + getRouting(configContext, v2rayConfig) getFakeDns(v2rayConfig) getDns(v2rayConfig) @@ -306,10 +302,10 @@ object CoreConfigManager { } /** Converts a built config object into a unified result payload. */ - private fun toConfigResult(guid: String, v2rayConfig: V2rayConfig): ConfigResult { + private fun toConfigResult(configContext: CoreConfigContext, v2rayConfig: V2rayConfig): ConfigResult { return ConfigResult( status = true, - guid = guid, + guid = configContext.guid, content = JsonUtil.toJsonPretty(v2rayConfig) ?: "" ) } @@ -321,10 +317,11 @@ object CoreConfigManager { * It first attempts to use the cached configuration if available, otherwise reads * the configuration from the "v2ray_config.json" asset file. * - * @param context Android context used to access application assets + * @param configContext Runtime context used to access app assets and profile data * @return V2rayConfig object parsed from the JSON configuration, or null if the configuration is empty */ - private fun initV2rayConfig(context: Context): V2rayConfig? { + private fun initV2rayConfig(configContext: CoreConfigContext): V2rayConfig? { + val context = configContext.context var assets = "" if (needTun()) { assets = initConfigCacheWithTun ?: Utils.readTextFromAssets(context, "v2ray_config_with_tun.json") @@ -449,64 +446,54 @@ object CoreConfigManager { } /** - * Pre-pass: scans all routing rulesets for non-builtin outbound tags, - * looks up the matching profile by remarks, converts it to an OutboundBean, - * and appends it to v2rayConfig.outbounds. This must run before getRouting - * so that every custom tag is already present when routing rules are applied. + * Injects custom outbound profiles into the V2ray configuration. + * Uses pre-resolved profiles from the context instead of re-parsing. * - * @param v2rayConfig The V2ray configuration object to be modified + * @param v2rayConfig The V2ray configuration to modify + * @param customOutbounds Pre-resolved custom outbound profiles (tag -> ProfileItem mapping) */ - private fun injectCustomOutbounds(v2rayConfig: V2rayConfig) { + private fun injectCustomOutbounds(v2rayConfig: V2rayConfig, customOutbounds: Map) { val existingTags = v2rayConfig.outbounds.mapTo(mutableSetOf()) { it.tag } - val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return - rulesetItems - .filter { it.enabled } - .mapNotNull { it.outboundTag.takeIf { tag -> tag.isNotBlank() } } - .filter { it !in AppConfig.BUILTIN_OUTBOUND_TAGS } - .distinct() - .forEach { tag -> - if (tag in existingTags) return@forEach - try { - val profile = SettingsManager.getServerViaRemarks(tag) ?: run { - LogUtil.w(AppConfig.TAG, "Custom outbound tag '$tag' not found by remarks, skipping") - return@forEach - } - val outbound = convertProfile2Outbound(profile) ?: run { - LogUtil.w(AppConfig.TAG, "Could not convert profile '$tag' to outbound, skipping") - return@forEach - } - outbound.tag = tag - v2rayConfig.outbounds.add(outbound) - existingTags.add(tag) - LogUtil.d(AppConfig.TAG, "Injected custom outbound: tag='$tag'") - } catch (e: Exception) { - LogUtil.e(AppConfig.TAG, "Failed to inject custom outbound for tag '$tag', skipping", e) + customOutbounds.forEach { (tag, profile) -> + if (tag in existingTags) return@forEach + try { + val outbound = convertProfile2Outbound(profile) ?: run { + LogUtil.w(AppConfig.TAG, "Could not convert profile '$tag' to outbound, skipping") + return@forEach } + outbound.tag = tag + v2rayConfig.outbounds.add(outbound) + existingTags.add(tag) + LogUtil.d(AppConfig.TAG, "Injected custom outbound: tag='$tag'") + } catch (e: Exception) { + LogUtil.e(AppConfig.TAG, "Failed to inject custom outbound for tag '$tag', skipping", e) } + } } /** * Configures routing settings for V2ray. * - * Sets up the domain strategy and adds routing rules from saved rulesets. + * Sets up the domain strategy, injects custom outbounds from rulesets, and adds routing rules. * + * @param configContext Configuration context with custom outbound profiles * @param v2rayConfig The V2ray configuration object to be modified * @return true if routing configuration was successful, false otherwise */ - private fun getRouting(context: Context, v2rayConfig: V2rayConfig): Boolean { + private fun getRouting(configContext: CoreConfigContext, v2rayConfig: V2rayConfig): Boolean { try { v2rayConfig.routing.domainStrategy = MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "AsIs" - // Pre-pass: inject outbounds referenced by remarks in routing rules - injectCustomOutbounds(v2rayConfig) + // Inject custom outbound profiles from routing rulesets + injectCustomOutbounds(v2rayConfig, configContext.customOutboundProfiles) val rulesetItems = MmkvManager.decodeRoutingRulesets() rulesetItems?.forEach { key -> - getRoutingUserRule(context, key, v2rayConfig) + getRoutingUserRule(configContext, key, v2rayConfig) } } catch (e: Exception) { LogUtil.e(AppConfig.TAG, "Failed to configure routing", e) @@ -518,10 +505,12 @@ object CoreConfigManager { /** * Adds a specific ruleset item to the routing configuration. * + * @param configContext Runtime context used by routing helpers * @param item The ruleset item to add * @param v2rayConfig The V2ray configuration object to be modified */ - private fun getRoutingUserRule(context: Context, item: RulesetItem?, v2rayConfig: V2rayConfig) { + private fun getRoutingUserRule(configContext: CoreConfigContext, item: RulesetItem?, v2rayConfig: V2rayConfig) { + val context = configContext.context try { if (item == null || !item.enabled) { return @@ -780,12 +769,12 @@ object CoreConfigManager { * * Converts the profile to an outbound configuration and applies global settings. * + * @param configContext Runtime context containing the selected profile * @param v2rayConfig The V2ray configuration object to be modified - * @param config The profile item containing connection details * @return true if outbound configuration was successful, null if there was an error */ - private fun getOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Boolean? { - val outbound = convertProfile2Outbound(config) ?: return null + private fun getOutbounds(configContext: CoreConfigContext, v2rayConfig: V2rayConfig): Boolean? { + val outbound = convertProfile2Outbound(configContext.selectedProfile) ?: return null if (v2rayConfig.outbounds.isNotEmpty()) { v2rayConfig.outbounds[0] = outbound @@ -798,10 +787,11 @@ object CoreConfigManager { /** * Configures load balancing settings for the V2ray configuration. * + * @param configContext Runtime context containing policy group settings * @param v2rayConfig The V2ray configuration object to be modified with balancing settings - * @param config The profile item containing policy group settings */ - private fun getBalance(v2rayConfig: V2rayConfig, config: ProfileItem) { + private fun getBalance(configContext: CoreConfigContext, v2rayConfig: V2rayConfig) { + val config = configContext.selectedProfile try { v2rayConfig.routing.rules.forEach { rule -> if (rule.outboundTag == AppConfig.TAG_PROXY) { diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CoreConfigContext.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CoreConfigContext.kt index 097d9ab5..2c3248bc 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CoreConfigContext.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CoreConfigContext.kt @@ -12,4 +12,5 @@ data class CoreConfigContext( val selectedProfile: ProfileItem, val resolvedProfiles: List, val resolvedType: CoreResolvedType, + val customOutboundProfiles: Map = emptyMap(), ) \ No newline at end of file