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 22db29ce..58689092 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 @@ -43,11 +43,13 @@ object CoreConfigContextBuilder { // Step 2: Resolve all non-builtin routing outbound tags. val routingResolvedOutbounds = resolveRoutingOutbounds() + val routingDomainRules = collectRoutingDomainRulesForDns() return CoreConfigContext( context = context, guid = guid, resolvedOutbounds = listOf(primaryResolvedOutbound) + routingResolvedOutbounds, + routingDomainRules = routingDomainRules, ) } @@ -214,4 +216,34 @@ object CoreConfigContextBuilder { return listOf(config) } } + + /** + * Collect enabled routing domain rules in original order for DNS segmentation. + * + * outbounds are normalized into three tags only: proxy / direct / block. + */ + private fun collectRoutingDomainRulesForDns(): List { + val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return emptyList() + val result = mutableListOf() + + rulesetItems + .asSequence() + .filter { it.enabled } + .filter { !it.domain.isNullOrEmpty() } + .forEach { rule -> + val normalizedOutboundTag = when (rule.outboundTag) { + AppConfig.TAG_DIRECT -> AppConfig.TAG_DIRECT + AppConfig.TAG_BLOCKED -> AppConfig.TAG_BLOCKED + else -> AppConfig.TAG_PROXY + } + result.add( + CoreConfigContext.RoutingDomainRule( + domain = rule.domain.orEmpty(), + outboundTag = normalizedOutboundTag + ) + ) + } + + return result + } } 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 8b33f26d..0e64cf89 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 @@ -166,8 +166,8 @@ object CoreConfigManager { // User routing rules (policyGroupBalancerTags rewrites TAG_PROXY→balancer when main is POLICYGROUP). configureRouting(configContext, v2rayConfig, policyGroupBalancerTags) configureFakeDns(v2rayConfig) - configureDns(v2rayConfig, policyGroupBalancerTags) - configureLocalDns(v2rayConfig) + configureDns(configContext, v2rayConfig, policyGroupBalancerTags) + configureLocalDns(configContext, v2rayConfig) configureRootModeDns(v2rayConfig) // (added by getDns / getCustomLocalDns) to use the balancer, then add @@ -570,16 +570,20 @@ object CoreConfigManager { /** * Configure local DNS inbounds, outbounds, and routing rules. */ - private fun configureLocalDns(v2rayConfig: V2rayConfig) { + private fun configureLocalDns(configContext: CoreConfigContext, v2rayConfig: V2rayConfig) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) != true) { return } if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) { val geositeCn = arrayListOf(AppConfig.GEOSITE_CN) - val proxyDomain = collectUserRuleDomainsByTag(AppConfig.TAG_PROXY) - val directDomain = collectUserRuleDomainsByTag(AppConfig.TAG_DIRECT) - val finalDomain = geositeCn.plus(proxyDomain).plus(directDomain).distinct() + val routingDomains = configContext.routingDomainRules + .asSequence() + .filter { it.outboundTag != AppConfig.TAG_BLOCKED } + .flatMap { it.domain.asSequence() } + .toList() + .distinct() + val finalDomain = geositeCn + routingDomains // fakedns with all domains to make it always top priority v2rayConfig.dns?.servers?.add( 0, @@ -668,6 +672,7 @@ object CoreConfigManager { } } + /* /** * Configure DNS servers, hosts, and DNS routing rules. */ @@ -807,7 +812,188 @@ object CoreConfigManager { ) } } + */ + /** + * Configure DNS servers, hosts, and DNS routing rules. + */ + private fun configureDns( + configContext: CoreConfigContext, + v2rayConfig: V2rayConfig, + policyGroupBalancerTags: Map, + ) { + val servers = ArrayList() + val remoteDns = SettingsManager.getRemoteDnsServers() + val domesticDns = SettingsManager.getDomesticDnsServers() + + remoteDns.forEach { servers.add(it) } + + val hosts = buildDnsHostsFromRoutingRules(configContext) + val cnDomesticDnsTags = buildDnsCnModeFromRoutingRules(configContext, servers, domesticDns) + val domesticDnsTags = buildDnsFromRoutingRules( + configContext = configContext, + servers = servers, + remoteDns = remoteDns, + domesticDns = domesticDns + ) + domesticDnsTags.addAll(cnDomesticDnsTags) + + v2rayConfig.dns = V2rayConfig.DnsBean( + servers = servers, + hosts = hosts, + tag = AppConfig.TAG_DNS, + enableParallelQuery = if ((domesticDns.size + remoteDns.size) > 2) true else null + ) + + if (domesticDnsTags.isNotEmpty()) { + v2rayConfig.routing.rules.add( + V2rayConfig.RoutingBean.RulesBean( + outboundTag = AppConfig.TAG_DIRECT, + inboundTag = ArrayList(domesticDnsTags), + domain = null + ) + ) + } + + val dnsProxyBalancerTag = policyGroupBalancerTags[AppConfig.TAG_PROXY] + if (dnsProxyBalancerTag != null) { + v2rayConfig.routing.rules.add( + V2rayConfig.RoutingBean.RulesBean( + balancerTag = dnsProxyBalancerTag, + inboundTag = arrayListOf(AppConfig.TAG_DNS), + domain = null + ) + ) + } else { + v2rayConfig.routing.rules.add( + V2rayConfig.RoutingBean.RulesBean( + outboundTag = AppConfig.TAG_PROXY, + inboundTag = arrayListOf(AppConfig.TAG_DNS), + domain = null + ) + ) + } + } + + private fun buildDnsHostsFromRoutingRules(configContext: CoreConfigContext): MutableMap { + val hosts = mutableMapOf() + + val blockDomains = configContext.routingDomainRules + .asSequence() + .filter { it.outboundTag == AppConfig.TAG_BLOCKED } + .flatMap { it.domain.asSequence() } + .toList() + if (blockDomains.isNotEmpty()) { + hosts.putAll(blockDomains.map { it to AppConfig.LOOPBACK }) + } + + hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN + hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES + hosts[AppConfig.DNS_CISCO_SSE_DOMAIN] = AppConfig.DNS_CISCO_SSE_ADDRESSES + hosts[AppConfig.DNS_CISCO_UMBRELLA_DOMAIN] = AppConfig.DNS_CISCO_UMBRELLA_ADDRESSES + hosts[AppConfig.DNS_CLOUDFLARE_ONE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONE_ADDRESSES + hosts[AppConfig.DNS_CLOUDFLARE_ONEDOT_DNS_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONEDOT_DNS_ADDRESSES + hosts[AppConfig.DNS_CLOUDFLARE_DNS_COM_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_COM_ADDRESSES + hosts[AppConfig.DNS_CLOUDFLARE_DNS_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_ADDRESSES + hosts[AppConfig.DNS_CLOUDFLARE_WARP_DOMAIN] = AppConfig.DNS_CLOUDFLARE_WARP_ADDRESSES + hosts[AppConfig.DNS_DNSPOD_DOH_DOMAIN] = AppConfig.DNS_DNSPOD_DOH_ADDRESSES + hosts[AppConfig.DNS_DNSPOD_DOT_DOMAIN] = AppConfig.DNS_DNSPOD_DOT_ADDRESSES + hosts[AppConfig.DNS_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES + hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES + hosts[AppConfig.DNS_SB_DOMAIN] = AppConfig.DNS_SB_ADDRESSES + hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES + + val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS) + if (userHosts.isNotNullEmpty()) { + val userHostsMap = userHosts?.split(",").orEmpty() + .filter { it.isNotEmpty() } + .filter { it.contains(":") } + .associate { it.split(":").let { (k, v) -> k to v } } + hosts.putAll(userHostsMap) + } + + return hosts + } + + private fun buildDnsCnModeFromRoutingRules(configContext: CoreConfigContext, servers: ArrayList, domesticDns: List, ): List { + val cnRegionFilter = { domain: String -> + domain.startsWith("geosite:") && (domain.endsWith("-cn") || domain.endsWith("@cn")) + || domain == AppConfig.GEOSITE_CN + } + val isCnRoutingMode = configContext.routingDomainRules + .asSequence() + .filter { it.outboundTag == AppConfig.TAG_DIRECT } + .flatMap { it.domain.asSequence() } + .any { it == AppConfig.GEOSITE_CN } + + if (!isCnRoutingMode) { + return emptyList() + } + + val geoipCn = arrayListOf(AppConfig.GEOIP_CN) + val cnDomains = configContext.routingDomainRules + .asSequence() + .filter { it.outboundTag == AppConfig.TAG_DIRECT } + .flatMap { it.domain.asSequence() } + .filter { cnRegionFilter(it) } + .toList() + if (cnDomains.isEmpty()) { + return emptyList() + } + + val cnDomesticDnsTags = mutableListOf() + domesticDns.forEachIndexed { index, address -> + val cnDomesticDnsTag = "${AppConfig.TAG_DOMESTIC_DNS}_cn_expect_${index}" + servers.add( + V2rayConfig.DnsBean.ServersBean( + address = address, + domains = cnDomains, + expectIPs = geoipCn, + skipFallback = true, + tag = cnDomesticDnsTag + ) + ) + cnDomesticDnsTags.add(cnDomesticDnsTag) + } + return cnDomesticDnsTags + } + + private fun buildDnsFromRoutingRules( + configContext: CoreConfigContext, + servers: ArrayList, + remoteDns: List, + domesticDns: List, + ): MutableList { + val domesticDnsTags = mutableListOf() + configContext.routingDomainRules.forEachIndexed { ruleIndex, rule -> + when (rule.outboundTag) { + AppConfig.TAG_DIRECT -> { + domesticDns.forEachIndexed { dnsIndex, address -> + val tag = "${AppConfig.TAG_DOMESTIC_DNS}_${ruleIndex}_$dnsIndex" + servers.add( + V2rayConfig.DnsBean.ServersBean( + address = address, + domains = rule.domain, + skipFallback = true, + tag = tag + ) + ) + domesticDnsTags.add(tag) + } + } + AppConfig.TAG_BLOCKED -> Unit + else -> { + servers.add( + V2rayConfig.DnsBean.ServersBean( + address = remoteDns.first(), + domains = rule.domain, + ) + ) + } + } + } + return domesticDnsTags + } //endregion 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 5be4a39a..6345cc2c 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 @@ -9,6 +9,7 @@ data class CoreConfigContext( val guid: String, val isCustom: Boolean = false, val resolvedOutbounds: List = emptyList(), + val routingDomainRules: List = emptyList(), ) { data class ResolvedOutbound( val tag: String, @@ -16,4 +17,9 @@ data class CoreConfigContext( val resolvedProfiles: List, val resolvedType: CoreResolvedType, ) -} \ No newline at end of file + + data class RoutingDomainRule( + val domain: List, + val outboundTag: String, + ) +}