Add process-based routing support

Introduce process field for routing rules and wire it through UI, config parsing, and runtime resolution. Adds a PackageUidResolver utility to map package names to UIDs (with caching) and converts user-entered package names into UIDs when building v2ray routing rules. Updates V2rayConfig types, RoutingEditActivity layout/bindings/strings to accept process input, and adjusts getRouting to receive Context. Also adds extra logging and a defensive check in V2RayServiceManager when resolving connection owner UID.
This commit is contained in:
2dust
2026-04-15 15:46:06 +08:00
parent 5a5a81fa0b
commit ba25e05536
17 changed files with 107 additions and 18 deletions
@@ -4,6 +4,7 @@ data class RulesetItem(
var remarks: String? = "",
var ip: List<String>? = null,
var domain: List<String>? = null,
var process: List<String>? = null,
var outboundTag: String = "",
var port: String? = null,
var network: String? = null,
@@ -422,8 +422,9 @@ data class V2rayConfig(
data class RulesBean(
var type: String = "field",
var ip: ArrayList<String>? = null,
var domain: ArrayList<String>? = null,
var ip: List<String>? = null,
var domain: List<String>? = null,
var process: List<String>? = null,
var outboundTag: String? = null,
var balancerTag: String? = null,
var port: String? = null,
@@ -19,6 +19,7 @@ import com.v2ray.ang.extension.toast
import com.v2ray.ang.service.V2RayProxyOnlyService
import com.v2ray.ang.service.V2RayVpnService
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.PackageUidResolver
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -385,12 +386,21 @@ object V2RayServiceManager {
else -> return -1L
}
if (destIP.isBlank() || destPort == 0L) {
Log.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to :$destPort, (no dest)")
return -1L
}
return try {
cm.getConnectionOwnerUid(
val uid = cm.getConnectionOwnerUid(
proto,
InetSocketAddress(srcIP, srcPort.toInt()),
InetSocketAddress(destIP, destPort.toInt())
).toLong()
Log.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid")
//Log.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid,${PackageUidResolver.uidToPackageName(uid.toString())}")
uid
} catch (e: Exception) {
-1L
}
@@ -27,6 +27,7 @@ import com.v2ray.ang.fmt.VmessFmt
import com.v2ray.ang.fmt.WireguardFmt
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.PackageUidResolver
import com.v2ray.ang.util.Utils
object V2rayConfigManager {
@@ -200,7 +201,7 @@ object V2rayConfigManager {
getOutbounds(v2rayConfig, config) ?: return result
getMoreOutbounds(v2rayConfig, config.subscriptionId)
getRouting(v2rayConfig)
getRouting(context, v2rayConfig)
getFakeDns(v2rayConfig)
@@ -257,7 +258,7 @@ object V2rayConfigManager {
outboundsList.addAll(v2rayConfig.outbounds)
v2rayConfig.outbounds = ArrayList(outboundsList)
getRouting(v2rayConfig)
getRouting(context, v2rayConfig)
getFakeDns(v2rayConfig)
@@ -452,7 +453,7 @@ object V2rayConfigManager {
* @param v2rayConfig The V2ray configuration object to be modified
* @return true if routing configuration was successful, false otherwise
*/
private fun getRouting(v2rayConfig: V2rayConfig): Boolean {
private fun getRouting(context: Context, v2rayConfig: V2rayConfig): Boolean {
try {
v2rayConfig.routing.domainStrategy =
@@ -461,7 +462,7 @@ object V2rayConfigManager {
val rulesetItems = MmkvManager.decodeRoutingRulesets()
rulesetItems?.forEach { key ->
getRoutingUserRule(key, v2rayConfig)
getRoutingUserRule(context, key, v2rayConfig)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to configure routing", e)
@@ -476,7 +477,7 @@ object V2rayConfigManager {
* @param item The ruleset item to add
* @param v2rayConfig The V2ray configuration object to be modified
*/
private fun getRoutingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) {
private fun getRoutingUserRule(context: Context, item: RulesetItem?, v2rayConfig: V2rayConfig) {
try {
if (item == null || !item.enabled) {
return
@@ -497,6 +498,14 @@ object V2rayConfigManager {
rule.ip = updatedIpList
}
// Convert process package names to UIDs
rule.process?.let { processList ->
if (processList.isNotEmpty()) {
val uids = PackageUidResolver.packageNamesToUids(context, processList)
rule.process = uids.ifEmpty { null }
}
}
v2rayConfig.routing.rules.add(rule)
} catch (e: Exception) {
@@ -42,6 +42,7 @@ class RoutingEditActivity : BaseActivity() {
binding.chkLocked.isChecked = rulesetItem.locked == true
binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(","))
binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(","))
binding.etProcess.text = Utils.getEditable(rulesetItem.process?.joinToString(","))
binding.etPort.text = Utils.getEditable(rulesetItem.port)
binding.etProtocol.text = Utils.getEditable(rulesetItem.protocol?.joinToString(","))
binding.etNetwork.text = Utils.getEditable(rulesetItem.network)
@@ -65,6 +66,7 @@ class RoutingEditActivity : BaseActivity() {
locked = binding.chkLocked.isChecked
domain = binding.etDomain.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
ip = binding.etIp.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
process = binding.etProcess.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
protocol = binding.etProtocol.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
port = binding.etPort.text.toString().nullIfBlank()
network = binding.etNetwork.text.toString().nullIfBlank()
@@ -25,7 +25,7 @@ class RoutingSettingRecyclerAdapter(
val ruleset = rulesets[position]
holder.itemRoutingSettingBinding.remarks.text = ruleset.remarks
holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.port)?.toString()
holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.process ?: ruleset.port)?.toString()
holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag
holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled
holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.locked == true
@@ -0,0 +1,45 @@
package com.v2ray.ang.util
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import java.util.concurrent.ConcurrentHashMap
object PackageUidResolver {
private const val TAG = "PackageUidResolver"
// In-process cache to avoid resolving the same package UID repeatedly.
private val packageUidCache = ConcurrentHashMap<String, String>()
val packageUidMap: Map<String, String>
get() = packageUidCache
fun packageNamesToUids(context: Context, packageNames: List<String>): List<String> {
return packageNames.mapNotNull { pkg ->
packageUidCache[pkg] ?: resolveUid(context, pkg)?.also { uid ->
packageUidCache[pkg] = uid
}
}
}
fun uidsToPackageNames(uids: List<String>): List<String> {
if (uids.isEmpty()) return emptyList()
return packageUidCache.filterValues { it in uids }.keys.toList()
}
fun uidToPackageName(uid: String): String? {
return packageUidCache.entries.firstOrNull { it.value == uid }?.key
}
private fun resolveUid(context: Context, packageName: String): String? {
return try {
val uid = context.packageManager.getPackageUid(packageName, 0).toString()
Log.d(TAG, "Package: $packageName -> UID: $uid")
uid
} catch (_: PackageManager.NameNotFoundException) {
Log.w(TAG, "Package not found: $packageName")
null
}
}
}