Skip to content

Adds balance support #4716

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ object AppConfig {
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
const val PREF_LOGLEVEL = "pref_core_loglevel"
const val PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD = "pref_outbound_domain_resolve_method"
const val PREF_INTELLIGENT_SELECTION_METHOD = "pref_intelligent_selection_method"
const val PREF_MODE = "pref_mode"
const val PREF_IS_BOOTED = "pref_is_booted"
const val PREF_CHECK_UPDATE_PRE_RELEASE = "pref_check_update_pre_release"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ data class SubscriptionItem(
var prevProfile: String? = null,
var nextProfile: String? = null,
var filter: String? = null,
var intelligentSelectionFilter: String? = null,
var allowInsecureUrl: Boolean = false,
)

50 changes: 48 additions & 2 deletions V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -496,14 +496,14 @@ data class V2rayConfig(
var domainStrategy: String,
var domainMatcher: String? = null,
var rules: ArrayList<RulesBean>,
val balancers: List<Any>? = null
var balancers: List<BalancerBean>? = null
) {

data class RulesBean(
var type: String = "field",
var ip: ArrayList<String>? = null,
var domain: ArrayList<String>? = null,
var outboundTag: String = "",
var outboundTag: String? = null,
var balancerTag: String? = null,
var port: String? = null,
val sourcePort: String? = null,
Expand All @@ -515,6 +515,32 @@ data class V2rayConfig(
val attrs: String? = null,
val domainMatcher: String? = null
)

data class BalancerBean(
val tag: String,
val selector: List<String>,
val fallbackTag: String? = null,
val strategy: StrategyObject? = null
)

data class StrategyObject(
val type: String = "random", // "random" | "roundRobin" | "leastPing" | "leastLoad"
val settings: StrategySettingsObject? = null
)

data class StrategySettingsObject(
val expected: Int? = null,
val maxRTT: String? = null,
val tolerance: Double? = null,
val baselines: List<String>? = null,
val costs: List<CostObject>? = null
)

data class CostObject(
val regexp: Boolean = false,
val match: String,
val value: Double
)
}

data class PolicyBean(
Expand All @@ -532,6 +558,26 @@ data class V2rayConfig(
)
}

data class ObservatoryObject(
val subjectSelector: List<String>,
val probeUrl: String,
val probeInterval: String,
val enableConcurrency: Boolean = false
)

data class BurstObservatoryObject(
val subjectSelector: List<String>,
val pingConfig: PingConfigObject
) {
data class PingConfigObject(
val destination: String,
val connectivity: String? = null,
val interval: String,
val sampling: Int,
val timeout: String? = null
)
}

data class FakednsBean(
var ipPool: String = "198.18.0.0/15",
var poolSize: Int = 10000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,4 +490,31 @@ object AngConfigManager {
MmkvManager.encodeSubscription("", subItem)
return 1
}

/**
* Creates an intelligent selection configuration based on multiple server configurations.
*
* @param context The application context used for configuration generation.
* @param guidList The list of server GUIDs to be included in the intelligent selection.
* Each GUID represents a server configuration that will be combined.
* @param subid The subscription ID to associate with the generated configuration.
* This helps organize the configuration under a specific subscription.
* @return The GUID key of the newly created intelligent selection configuration,
* or null if the operation fails (e.g., empty guidList or configuration parsing error).
*/
fun createIntelligentSelection(
context: Context,
guidList: List<String>,
subid: String
): String? {
if (guidList.isEmpty()) {
return null
}
val result = V2rayConfigManager.genV2rayConfig(context, guidList) ?: return null
val config = CustomFmt.parse(JsonUtil.toJson(result)) ?: return null
config.subscriptionId = subid
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(result) ?: "")
return key
}
}
151 changes: 151 additions & 0 deletions V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.dto.ConfigResult
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
Expand All @@ -15,6 +16,7 @@ import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.fmt.HttpFmt
import com.v2ray.ang.fmt.ShadowsocksFmt
import com.v2ray.ang.fmt.SocksFmt
Expand Down Expand Up @@ -52,6 +54,27 @@ object V2rayConfigManager {
}
}

/**
* Generates a V2ray configuration from multiple server profiles.
*
* @param context The context of the caller.
* @param guidList A list of server GUIDs to be included in the generated configuration.
* Each GUID represents a unique server profile stored in the system.
* @return A V2rayConfig object containing the combined configuration of all specified servers,
* or null if the operation fails (e.g., no valid configurations found, parsing errors)
*/
fun genV2rayConfig(context: Context, guidList: List<String>): V2rayConfig? {
try {
val configList = guidList.mapNotNull { guid ->
MmkvManager.decodeServerConfig(guid)
}
return genV2rayMultipleConfig(context, configList)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to generate V2ray config", e)
return null
}
}

/**
* Retrieves the speedtest V2ray configuration for the given GUID.
*
Expand Down Expand Up @@ -142,6 +165,80 @@ object V2rayConfigManager {
return result
}

private fun genV2rayMultipleConfig(context: Context, configList: List<ProfileItem>): V2rayConfig? {
val validConfigs = configList.asSequence().filter { it.server.isNotNullEmpty() }
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
.filter { it.configType != EConfigType.CUSTOM }
.filter { it.configType != EConfigType.HYSTERIA2 }
.filter { config ->
if (config.subscriptionId.isEmpty()) {
return@filter true
}
val subItem = MmkvManager.decodeSubscription(config.subscriptionId)
if (subItem?.intelligentSelectionFilter.isNullOrEmpty() || config.remarks.isEmpty()) {
return@filter true
}
Regex(pattern = subItem?.intelligentSelectionFilter!!).containsMatchIn(input = config.remarks)
}.toList()

if (validConfigs.isEmpty()) {
Log.w(AppConfig.TAG, "All configs are invalid")
return null
}

val v2rayConfig = initV2rayConfig(context) ?: return null
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"

val subIds = configList.map { it.subscriptionId }.toHashSet()
val remarks = if (subIds.size == 1 && subIds.first().isNotEmpty()) {
val sub = MmkvManager.decodeSubscription(subIds.first())
(sub?.remarks ?: "") + context.getString(R.string.intelligent_selection)
} else {
context.getString(R.string.intelligent_selection)
}

v2rayConfig.remarks = remarks

getInbounds(v2rayConfig)

v2rayConfig.outbounds.removeAt(0)
val outboundsList = mutableListOf<V2rayConfig.OutboundBean>()
var index = 0
for (config in validConfigs) {
index++
val outbound = convertProfile2Outbound(config) ?: continue
val ret = updateOutboundWithGlobalSettings(outbound)
if (!ret) continue
outbound.tag = "proxy-$index"
outboundsList.add(outbound)
}
outboundsList.addAll(v2rayConfig.outbounds)
v2rayConfig.outbounds = ArrayList(outboundsList)

getRouting(v2rayConfig)

getFakeDns(v2rayConfig)

getDns(v2rayConfig)

getBalance(v2rayConfig)

if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
getCustomLocalDns(v2rayConfig)
}
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) {
v2rayConfig.stats = null
v2rayConfig.policy = null
}

//Resolve and add to DNS Hosts
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") == "1") {
resolveOutboundDomainsToHosts(v2rayConfig)
}

return v2rayConfig
}

/**
* Retrieves the normal V2ray configuration for speedtest.
*
Expand Down Expand Up @@ -741,6 +838,60 @@ object V2rayConfigManager {
return true
}

/**
* Configures load balancing settings for the V2ray configuration.
*
* @param v2rayConfig The V2ray configuration object to be modified with balancing settings
*/
private fun getBalance(v2rayConfig: V2rayConfig)
{
try {
v2rayConfig.routing.rules.forEach { rule ->
if (rule.outboundTag == "proxy") {
rule.outboundTag = null
rule.balancerTag = "proxy-round"
}
}

if (MmkvManager.decodeSettingsString(AppConfig.PREF_INTELLIGENT_SELECTION_METHOD, "0") == "0") {
val balancer = V2rayConfig.RoutingBean.BalancerBean(
tag = "proxy-round",
selector = listOf("proxy-"),
strategy = V2rayConfig.RoutingBean.StrategyObject(
type = "leastPing"
)
)
v2rayConfig.routing.balancers = listOf(balancer)
v2rayConfig.observatory = V2rayConfig.ObservatoryObject(
subjectSelector = listOf("proxy-"),
probeUrl = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DELAY_TEST_URL,
probeInterval = "3m",
enableConcurrency = true
)
} else {
val balancer = V2rayConfig.RoutingBean.BalancerBean(
tag = "proxy-round",
selector = listOf("proxy-"),
strategy = V2rayConfig.RoutingBean.StrategyObject(
type = "leastLoad"
)
)
v2rayConfig.routing.balancers = listOf(balancer)
v2rayConfig.burstObservatory = V2rayConfig.BurstObservatoryObject(
subjectSelector = listOf("proxy-"),
pingConfig = V2rayConfig.BurstObservatoryObject.PingConfigObject(
destination = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DELAY_TEST_URL,
interval = "5m",
sampling = 2,
timeout = "30s"
)
)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to configure balance", e)
}
}

/**
* Updates the outbound with fragment settings for traffic optimization.
*
Expand Down
5 changes: 5 additions & 0 deletions V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
true
}

R.id.intelligent_selection_all -> {
mainViewModel.createIntelligentSelectionAll()
true
}

R.id.service_restart -> {
restartV2Ray()
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ class SettingsActivity : BaseActivity() {
AppConfig.PREF_UI_MODE_NIGHT,
AppConfig.PREF_LOGLEVEL,
AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD,
AppConfig.PREF_INTELLIGENT_SELECTION_METHOD,
AppConfig.PREF_MODE
).forEach { key ->
if (MmkvManager.decodeSettingsString(key) != null) {
Expand Down
3 changes: 3 additions & 0 deletions V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class SubEditActivity : BaseActivity() {
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
binding.etUrl.text = Utils.getEditable(subItem.url)
binding.etFilter.text = Utils.getEditable(subItem.filter)
binding.etIntelligentSelectionFilter.text = Utils.getEditable(subItem.intelligentSelectionFilter)
binding.chkEnable.isChecked = subItem.enabled
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
binding.allowInsecureUrl.isChecked = subItem.allowInsecureUrl
Expand All @@ -60,6 +61,7 @@ class SubEditActivity : BaseActivity() {
binding.etRemarks.text = null
binding.etUrl.text = null
binding.etFilter.text = null
binding.etIntelligentSelectionFilter.text = null
binding.chkEnable.isChecked = true
binding.etPreProfile.text = null
binding.etNextProfile.text = null
Expand All @@ -75,6 +77,7 @@ class SubEditActivity : BaseActivity() {
subItem.remarks = binding.etRemarks.text.toString()
subItem.url = binding.etUrl.text.toString()
subItem.filter = binding.etFilter.text.toString()
subItem.intelligentSelectionFilter = binding.etIntelligentSelectionFilter.text.toString()
subItem.enabled = binding.chkEnable.isChecked
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
subItem.prevProfile = binding.etPreProfile.text.toString()
Expand Down
23 changes: 23 additions & 0 deletions V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,29 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
MmkvManager.encodeServerList(serverList)
}

/**
* Creates an intelligent selection configuration containing all currently filtered servers.
*/
fun createIntelligentSelectionAll() {
viewModelScope.launch(Dispatchers.IO) {
val key = AngConfigManager.createIntelligentSelection(
getApplication<AngApplication>(),
serversCache.map { it.guid }.toList(),
subscriptionId
)

launch(Dispatchers.Main) {
if (key.isNullOrEmpty()) {
getApplication<AngApplication>().toastError(R.string.toast_failure)
} else {
getApplication<AngApplication>().toastSuccess(R.string.toast_success)
MmkvManager.setSelectServer(key)
reloadServerList()
}
}
}
}

/**
* Initializes assets.
* @param assets The asset manager.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
AppConfig.PREF_SOCKS_PORT,
AppConfig.PREF_LOGLEVEL,
AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD,
AppConfig.PREF_INTELLIGENT_SELECTION_METHOD,
AppConfig.PREF_LANGUAGE,
AppConfig.PREF_UI_MODE_NIGHT,
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
Expand Down
Loading