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<String, ProfileItem> 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.
This commit is contained in:
1 parent
dc1d3a27dd
commit
3e823a7440
3 files changed
+113
-79
No files matched your search
@@ -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<String, ProfileItem> {
|
||||
val customMap = mutableMapOf<String, ProfileItem>()
|
||||
val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return customMap
|
||||
|
||||
try {
|
||||
val processedTags = mutableSetOf<String>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, ProfileItem>) {
|
||||
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) {
|
||||
|
||||
@@ -12,4 +12,5 @@ data class CoreConfigContext(
|
||||
val selectedProfile: ProfileItem,
|
||||
val resolvedProfiles: List<ProfileItem>,
|
||||
val resolvedType: CoreResolvedType,
|
||||
val customOutboundProfiles: Map<String, ProfileItem> = emptyMap(),
|
||||
)
|
||||
Reference in new issue
Block a user