1
0
世界 4 жил өмнө
parent
commit
02c8085204

+ 167 - 144
app/src/main/java/io/nekohasekai/sagernet/bg/ProxyInstance.kt

@@ -33,14 +33,14 @@ import io.nekohasekai.sagernet.SagerNet
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.database.ProxyEntity
 import io.nekohasekai.sagernet.database.SagerDatabase
+import io.nekohasekai.sagernet.fmt.V2rayBuildResult
+import io.nekohasekai.sagernet.fmt.buildV2RayConfig
+import io.nekohasekai.sagernet.fmt.buildXrayConfig
 import io.nekohasekai.sagernet.fmt.gson.gson
 import io.nekohasekai.sagernet.fmt.shadowsocks.buildShadowsocksConfig
 import io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean
 import io.nekohasekai.sagernet.fmt.shadowsocksr.buildShadowsocksRConfig
 import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig
-import io.nekohasekai.sagernet.fmt.v2ray.V2rayBuildResult
-import io.nekohasekai.sagernet.fmt.v2ray.buildV2RayConfig
-import io.nekohasekai.sagernet.fmt.v2ray.buildXrayConfig
 import io.nekohasekai.sagernet.ktx.Logs
 import io.nekohasekai.sagernet.ktx.onMainDispatcher
 import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
@@ -57,7 +57,6 @@ import java.io.IOException
 import java.util.*
 import io.nekohasekai.sagernet.plugin.PluginManager as PluginManagerS
 
-
 class ProxyInstance(val profile: ProxyEntity) {
 
     lateinit var v2rayPoint: V2RayPoint
@@ -74,8 +73,12 @@ class ProxyInstance(val profile: ProxyEntity) {
 
     fun init(service: BaseService.Interface) {
         base = service
-        v2rayPoint = Libv2ray.newV2RayPoint(SagerSupportClass(if (service is VpnService)
-            service else null), false)
+        v2rayPoint = Libv2ray.newV2RayPoint(
+            SagerSupportClass(
+                if (service is VpnService)
+                    service else null
+            ), false
+        )
         val socksPort = DataStore.socksPort + 10
         if (profile.needExternal()) {
             v2rayPoint.domainName = "127.0.0.1:$socksPort"
@@ -90,34 +93,35 @@ class ProxyInstance(val profile: ProxyEntity) {
 
         Libv2ray.testConfig(jsonContent)
         v2rayPoint.configureFileContent = jsonContent
-
-        for ((index, profile) in config.index.entries) {
-            val port = socksPort + index
-            val needChain = index != config.index.size - 1
-            when {
-                profile.useExternalShadowsocks() -> {
-                    val bean = profile.requireSS()
-                    pluginConfigs[index] = bean.buildShadowsocksConfig(port).also {
-                        Logs.d(it)
-                    }
-                }
-                profile.type == 2 -> {
-                    pluginConfigs[index] = profile.requireSSR().buildShadowsocksRConfig().also {
-                        Logs.d(it)
+        for (chain in config.index) {
+            chain.entries.forEachIndexed { index, (port, profile) ->
+                val needChain = index != chain.size - 1
+                when {
+                    profile.useExternalShadowsocks() -> {
+                        val bean = profile.requireSS()
+                        pluginConfigs[index] = bean.buildShadowsocksConfig(port).also {
+                            Logs.d(it)
+                        }
                     }
-                }
-                profile.useXray() -> {
-                    initPlugin("xtls-plugin")
-                    pluginConfigs[index] =
-                        gson.toJson(buildXrayConfig(profile, port, needChain, index)).also {
+                    profile.type == 2 -> {
+                        pluginConfigs[index] = profile.requireSSR().buildShadowsocksRConfig().also {
                             Logs.d(it)
                         }
-                }
-                profile.type == 7 -> {
-                    val bean = profile.requireTrojanGo()
-                    initPlugin("trojan-go-plugin")
-                    pluginConfigs[index] = bean.buildTrojanGoConfig(port, needChain, index).also {
-                        Logs.d(it)
+                    }
+                    profile.useXray() -> {
+                        initPlugin("xtls-plugin")
+                        pluginConfigs[index] =
+                            gson.toJson(buildXrayConfig(profile, port, needChain, index)).also {
+                                Logs.d(it)
+                            }
+                    }
+                    profile.type == 7 -> {
+                        val bean = profile.requireTrojanGo()
+                        initPlugin("trojan-go-plugin")
+                        pluginConfigs[index] =
+                            bean.buildTrojanGoConfig(port, needChain, index).also {
+                                Logs.d(it)
+                            }
                     }
                 }
             }
@@ -129,122 +133,141 @@ class ProxyInstance(val profile: ProxyEntity) {
     @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
     fun start() {
 
-        val socksPort = DataStore.socksPort + 10
-        for ((index, profile) in config.index.entries) {
-            val bean = profile.requireBean()
-            val needChain = index != config.index.size - 1
-            val config = pluginConfigs[index] ?: continue
-
-            when {
-                profile.useExternalShadowsocks() -> {
-
-                    val context =
-                        if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked)
-                            SagerNet.application else SagerNet.deviceStorage
-                    val configFile =
-                        File(context.noBackupFilesDir,
-                            "shadowsocks_" + SystemClock.elapsedRealtime() + ".json")
-                    configFile.writeText(config)
-                    cacheFiles.add(configFile)
-
-                    val commands = mutableListOf(
-                        File(SagerNet.application.applicationInfo.nativeLibraryDir,
-                            Executable.SS_LOCAL).absolutePath,
-                        "-c", configFile.absolutePath
-                    )
-
-                    val env = mutableMapOf<String, String>()
-
-                    if (needChain) {
-                        val proxychainsConfigFile =
-                            File(context.noBackupFilesDir,
-                                "proxychains_ss_" + SystemClock.elapsedRealtime() + ".json")
-                        proxychainsConfigFile.writeText("strict_chain\n[ProxyList]\nsocks5 127.0.0.1 ${socksPort + index + 1}")
-                        cacheFiles.add(proxychainsConfigFile)
-
-                        env["LD_PRELOAD"] =
-                            File(SagerNet.application.applicationInfo.nativeLibraryDir,
-                                Executable.PROXYCHAINS).absolutePath
-                        env["PROXYCHAINS_CONF_FILE"] = proxychainsConfigFile.absolutePath
-                    }
+        for (chain in config.index) {
+            chain.entries.forEachIndexed { index, (port, profile) ->
+                val bean = profile.requireBean()
+                val needChain = index != config.index.size - 1
+                val config = pluginConfigs[index] ?: return@forEachIndexed
+
+                when {
+                    profile.useExternalShadowsocks() -> {
+
+                        val context =
+                            if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked)
+                                SagerNet.application else SagerNet.deviceStorage
+                        val configFile =
+                            File(
+                                context.noBackupFilesDir,
+                                "shadowsocks_" + SystemClock.elapsedRealtime() + ".json"
+                            )
+                        configFile.writeText(config)
+                        cacheFiles.add(configFile)
+
+                        val commands = mutableListOf(
+                            File(
+                                SagerNet.application.applicationInfo.nativeLibraryDir,
+                                Executable.SS_LOCAL
+                            ).absolutePath,
+                            "-c", configFile.absolutePath
+                        )
+
+                        val env = mutableMapOf<String, String>()
+
+                        if (needChain) {
+                            val proxychainsConfigFile =
+                                File(
+                                    context.noBackupFilesDir,
+                                    "proxychains_ss_" + SystemClock.elapsedRealtime() + ".json"
+                                )
+                            proxychainsConfigFile.writeText("strict_chain\n[ProxyList]\nsocks5 127.0.0.1 ${port + 1}")
+                            cacheFiles.add(proxychainsConfigFile)
+
+                            env["LD_PRELOAD"] =
+                                File(
+                                    SagerNet.application.applicationInfo.nativeLibraryDir,
+                                    Executable.PROXYCHAINS
+                                ).absolutePath
+                            env["PROXYCHAINS_CONF_FILE"] = proxychainsConfigFile.absolutePath
+                        }
 
-                    base.data.processes!!.start(commands, env)
-                }
-                profile.type == 2 -> {
-                    bean as ShadowsocksRBean
-                    val port = socksPort + index
-
-                    val context =
-                        if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked)
-                            SagerNet.application else SagerNet.deviceStorage
-
-                    val configFile =
-                        File(context.noBackupFilesDir,
-                            "shadowsocksr_" + SystemClock.elapsedRealtime() + ".json")
-
-                    configFile.writeText(config)
-                    cacheFiles.add(configFile)
-
-                    val commands = mutableListOf(
-                        File(SagerNet.application.applicationInfo.nativeLibraryDir,
-                            Executable.SSR_LOCAL).absolutePath,
-                        "-b", "127.0.0.1",
-                        "-c", configFile.absolutePath,
-                        "-l", "$port"
-                    )
-
-                    val env = mutableMapOf<String, String>()
-
-                    if (needChain) {
-                        val proxychainsConfigFile =
-                            File(context.noBackupFilesDir,
-                                "proxychains_ssr_" + SystemClock.elapsedRealtime() + ".json")
-                        proxychainsConfigFile.writeText("strict_chain\n[ProxyList]\nsocks5 127.0.0.1 ${port + 1}")
-                        cacheFiles.add(proxychainsConfigFile)
-
-                        env["LD_PRELOAD"] =
-                            File(SagerNet.application.applicationInfo.nativeLibraryDir,
-                                Executable.PROXYCHAINS).absolutePath
-                        env["PROXYCHAINS_CONF_FILE"] = proxychainsConfigFile.absolutePath
+                        base.data.processes!!.start(commands, env)
                     }
+                    profile.type == 2 -> {
+                        bean as ShadowsocksRBean
+                        val context =
+                            if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked)
+                                SagerNet.application else SagerNet.deviceStorage
+
+                        val configFile =
+                            File(
+                                context.noBackupFilesDir,
+                                "shadowsocksr_" + SystemClock.elapsedRealtime() + ".json"
+                            )
+
+                        configFile.writeText(config)
+                        cacheFiles.add(configFile)
+
+                        val commands = mutableListOf(
+                            File(
+                                SagerNet.application.applicationInfo.nativeLibraryDir,
+                                Executable.SSR_LOCAL
+                            ).absolutePath,
+                            "-b", "127.0.0.1",
+                            "-c", configFile.absolutePath,
+                            "-l", "$port"
+                        )
+
+                        val env = mutableMapOf<String, String>()
+
+                        if (needChain) {
+                            val proxychainsConfigFile =
+                                File(
+                                    context.noBackupFilesDir,
+                                    "proxychains_ssr_" + SystemClock.elapsedRealtime() + ".json"
+                                )
+                            proxychainsConfigFile.writeText("strict_chain\n[ProxyList]\nsocks5 127.0.0.1 ${port + 1}")
+                            cacheFiles.add(proxychainsConfigFile)
+
+                            env["LD_PRELOAD"] =
+                                File(
+                                    SagerNet.application.applicationInfo.nativeLibraryDir,
+                                    Executable.PROXYCHAINS
+                                ).absolutePath
+                            env["PROXYCHAINS_CONF_FILE"] = proxychainsConfigFile.absolutePath
+                        }
 
-                    base.data.processes!!.start(commands, env)
-                }
-                profile.useXray() -> {
-                    val context =
-                        if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked)
-                            SagerNet.application else SagerNet.deviceStorage
-
-                    val configFile =
-                        File(context.noBackupFilesDir,
-                            "xray_" + SystemClock.elapsedRealtime() + ".json")
-                    configFile.parentFile.mkdirs()
-                    configFile.writeText(config)
-                    cacheFiles.add(configFile)
-
-                    val commands = mutableListOf(
-                        initPlugin("xtls-plugin").path, "-c", configFile.absolutePath
-                    )
-
-                    base.data.processes!!.start(commands)
-                }
-                profile.type == 7 -> {
-                    val context =
-                        if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked)
-                            SagerNet.application else SagerNet.deviceStorage
-
-                    val configFile =
-                        File(context.noBackupFilesDir,
-                            "trojan_go_" + SystemClock.elapsedRealtime() + ".json")
-                    configFile.parentFile.mkdirs()
-                    configFile.writeText(config)
-                    cacheFiles.add(configFile)
-
-                    val commands = mutableListOf(
-                        initPlugin("trojan-go-plugin").path, "-config", configFile.absolutePath
-                    )
-
-                    base.data.processes!!.start(commands)
+                        base.data.processes!!.start(commands, env)
+                    }
+                    profile.useXray() -> {
+                        val context =
+                            if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked)
+                                SagerNet.application else SagerNet.deviceStorage
+
+                        val configFile =
+                            File(
+                                context.noBackupFilesDir,
+                                "xray_" + SystemClock.elapsedRealtime() + ".json"
+                            )
+                        configFile.parentFile.mkdirs()
+                        configFile.writeText(config)
+                        cacheFiles.add(configFile)
+
+                        val commands = mutableListOf(
+                            initPlugin("xtls-plugin").path, "-c", configFile.absolutePath
+                        )
+
+                        base.data.processes!!.start(commands)
+                    }
+                    profile.type == 7 -> {
+                        val context =
+                            if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked)
+                                SagerNet.application else SagerNet.deviceStorage
+
+                        val configFile =
+                            File(
+                                context.noBackupFilesDir,
+                                "trojan_go_" + SystemClock.elapsedRealtime() + ".json"
+                            )
+                        configFile.parentFile.mkdirs()
+                        configFile.writeText(config)
+                        cacheFiles.add(configFile)
+
+                        val commands = mutableListOf(
+                            initPlugin("trojan-go-plugin").path, "-config", configFile.absolutePath
+                        )
+
+                        base.data.processes!!.start(commands)
+                    }
                 }
             }
         }

+ 3 - 0
app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt

@@ -50,6 +50,9 @@ data class RuleEntity(
         @Query("SELECT * FROM rules ORDER BY userOrder")
         fun allRules(): List<RuleEntity>
 
+        @Query("SELECT * FROM rules WHERE enabled = :enabled ORDER BY userOrder")
+        fun enabledRules(enabled: Boolean = true): List<RuleEntity>
+
         @Query("SELECT MAX(userOrder) + 1 FROM rules")
         fun nextOrder(): Long?
 

+ 1 - 0
app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt

@@ -55,6 +55,7 @@ abstract class SagerDatabase : RoomDatabase() {
         val profileCacheDao get() = instance.profileCacheDao()
         val groupDao get() = instance.groupDao()
         val proxyDao get() = instance.proxyDao()
+        val rulesDao get() = instance.rulesDao()
 
     }
 

+ 403 - 342
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayBuilder.kt → app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt

@@ -19,7 +19,7 @@
  *                                                                            *
  ******************************************************************************/
 
-package io.nekohasekai.sagernet.fmt.v2ray
+package io.nekohasekai.sagernet.fmt
 
 import cn.hutool.core.lang.Validator
 import io.nekohasekai.sagernet.BuildConfig
@@ -31,13 +31,14 @@ import io.nekohasekai.sagernet.fmt.http.HttpBean
 import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
 import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
 import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
+import io.nekohasekai.sagernet.fmt.v2ray.*
 import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.*
 import io.nekohasekai.sagernet.ktx.Logs
 import io.nekohasekai.sagernet.ktx.formatObject
 import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
 import java.util.*
 import kotlin.collections.HashMap
-
+import kotlin.collections.LinkedHashMap
 
 const val TAG_SOCKS = "in"
 const val TAG_HTTP = "http"
@@ -50,7 +51,7 @@ const val TAG_DNS_OUT = "dns-out"
 
 class V2rayBuildResult(
     var config: V2RayConfig,
-    var index: HashMap<Int, ProxyEntity>,
+    var index: LinkedList<LinkedHashMap<Int, ProxyEntity>>,
     var requireWs: Boolean,
 )
 
@@ -69,6 +70,10 @@ fun buildV2RayConfig(proxy: ProxyEntity): V2rayBuildResult {
     }
 
     val proxies = proxy.resolveChain().asReversed()
+    val extraRules = SagerDatabase.rulesDao.enabledRules()
+    val extraProxies = SagerDatabase.proxyDao.getEntities(extraRules.mapNotNull { rule ->
+        rule.outbound.takeIf { it > 0 }
+    }.toHashSet().toList()).map { it.id to it.resolveChain() }.toMap()
 
     val bind = if (DataStore.allowAccess) "0.0.0.0" else "127.0.0.1"
     val remoteDns = DataStore.remoteDNS.split(",")
@@ -76,7 +81,7 @@ fun buildV2RayConfig(proxy: ProxyEntity): V2rayBuildResult {
     val enableLocalDNS = DataStore.enableLocalDNS
     val routeChina = DataStore.routeChina
     val trafficSniffing = DataStore.trafficSniffing
-    val indexMap = hashMapOf<Int, ProxyEntity>()
+    val indexMap = LinkedList<LinkedHashMap<Int, ProxyEntity>>()
     var requireWs = false
 
     return V2RayConfig().apply {
@@ -241,412 +246,468 @@ fun buildV2RayConfig(proxy: ProxyEntity): V2rayBuildResult {
             }
         }
 
-        var pastExternal = false
-        lateinit var pastOutbound: OutboundObject
+        var currentPort = socksPort + 10
+        fun requirePort() = currentPort++
 
-        proxies.forEachIndexed { index, proxyEntity ->
-            Logs.d("Index $index, proxyEntity: ")
-            Logs.d(formatObject(proxyEntity))
+        fun buildChain(tagInbound: String, profileList: List<ProxyEntity>) {
+            var pastExternal = false
+            lateinit var pastOutbound: OutboundObject
+            val chainMap = LinkedHashMap<Int, ProxyEntity>()
+            indexMap.add(chainMap)
 
-            val bean = proxyEntity.requireBean()
-            indexMap[index] = proxyEntity
-            val localPort = socksPort + 10 + index
-            val outbound = OutboundObject()
+            profileList.forEachIndexed { index, proxyEntity ->
+                Logs.d("Index $index, proxyEntity: ")
+                Logs.d(formatObject(proxyEntity))
 
-            if (proxyEntity.needExternal()) {
-                if (!pastExternal) {
-                    outbound.apply {
-                        protocol = "socks"
-                        settings = LazyOutboundConfigurationObject(this,
-                            SocksOutboundConfigurationObject().apply {
-                                servers = listOf(
-                                    SocksOutboundConfigurationObject.ServerObject()
-                                        .apply {
-                                            address = "127.0.0.1"
-                                            port = localPort
-                                        }
-                                )
-                            })
-                        tag = if (index == 0) TAG_AGENT else "${proxyEntity.id}"
-                        if (index > 0) {
-                            pastOutbound.proxySettings =
-                                OutboundObject.ProxySettingsObject().apply {
-                                    tag = "${proxyEntity.id}"
-                                    transportLayer = true
-                                }
-                        }
-                    }
-                    pastOutbound = outbound
-                    outbounds.add(outbound)
-                }
+                val bean = proxyEntity.requireBean()
+                val outbound = OutboundObject()
 
-                pastExternal = true
-                return@forEachIndexed
-            } else {
-                outbound.apply {
-                    val keepAliveInterval = DataStore.tcpKeepAliveInterval
-                    val needKeepAliveInterval = keepAliveInterval !in intArrayOf(0, 15)
-
-                    if (bean is SOCKSBean) {
-                        protocol = "socks"
-                        settings = LazyOutboundConfigurationObject(this,
-                            SocksOutboundConfigurationObject().apply {
-                                servers = listOf(
-                                    SocksOutboundConfigurationObject.ServerObject()
-                                        .apply {
-                                            address = bean.serverAddress
-                                            port = bean.serverPort
-                                            if (!bean.username.isNullOrBlank()) {
-                                                users =
-                                                    listOf(SocksOutboundConfigurationObject.ServerObject.UserObject()
-                                                        .apply {
-                                                            user = bean.username
-                                                            pass = bean.password
-                                                        })
+                if (proxyEntity.needExternal()) {
+                    val localPort = requirePort()
+                    chainMap[localPort] = proxyEntity
+                    if (!pastExternal) {
+                        outbound.apply {
+                            protocol = "socks"
+                            settings = LazyOutboundConfigurationObject(this,
+                                SocksOutboundConfigurationObject().apply {
+                                    servers = listOf(
+                                        SocksOutboundConfigurationObject.ServerObject()
+                                            .apply {
+                                                address = "127.0.0.1"
+                                                port = localPort
                                             }
-                                        }
-                                )
-                            })
-                        if (bean.tls || needKeepAliveInterval) {
-                            streamSettings = StreamSettingsObject().apply {
-                                network = "tcp"
-                                if (bean.tls) {
-                                    security = "tls"
-                                    if (bean.sni.isNotBlank()) {
-                                        tlsSettings = TLSObject().apply {
-                                            serverName = bean.sni
-                                        }
-                                    }
-                                }
-                                if (needKeepAliveInterval) {
-                                    sockopt =
-                                        StreamSettingsObject.SockoptObject().apply {
-                                            tcpKeepAliveInterval = keepAliveInterval
-                                        }
-                                }
+                                    )
+                                })
+                            tag = if (index == 0) tagInbound else {
+                                "$tagInbound-${proxyEntity.id}"
                             }
-                        }
-                    } else if (bean is HttpBean) {
-                        protocol = "http"
-                        settings = LazyOutboundConfigurationObject(this,
-                            HTTPOutboundConfigurationObject().apply {
-                                servers = listOf(
-                                    HTTPOutboundConfigurationObject.ServerObject()
-                                        .apply {
-                                            address = bean.serverAddress
-                                            port = bean.serverPort
-                                            if (!bean.username.isNullOrBlank()) {
-                                                users =
-                                                    listOf(HTTPInboundConfigurationObject.AccountObject()
-                                                        .apply {
-                                                            user = bean.username
-                                                            pass = bean.password
-                                                        })
-                                            }
-                                        }
-                                )
-                            })
-                        if (bean.tls || needKeepAliveInterval) {
-                            streamSettings = StreamSettingsObject().apply {
-                                network = "tcp"
-                                if (bean.tls) {
-                                    security = "tls"
-                                    if (bean.sni.isNotBlank()) {
-                                        tlsSettings = TLSObject().apply {
-                                            serverName = bean.sni
-                                        }
+                            if (index > 0) {
+                                pastOutbound.proxySettings =
+                                    OutboundObject.ProxySettingsObject().apply {
+                                        tag = "$tagInbound-${proxyEntity.id}"
+                                        transportLayer = true
                                     }
-                                }
-                                if (needKeepAliveInterval) {
-                                    sockopt =
-                                        StreamSettingsObject.SockoptObject().apply {
-                                            tcpKeepAliveInterval = keepAliveInterval
-                                        }
-                                }
                             }
                         }
-                    } else if (bean is StandardV2RayBean) {
-                        if (bean is VMessBean) {
-                            protocol = "vmess"
+                        pastOutbound = outbound
+                        outbounds.add(outbound)
+                    }
+
+                    pastExternal = true
+                    return@forEachIndexed
+                } else {
+                    outbound.apply {
+                        val keepAliveInterval = DataStore.tcpKeepAliveInterval
+                        val needKeepAliveInterval = keepAliveInterval !in intArrayOf(0, 15)
+
+                        if (bean is SOCKSBean) {
+                            protocol = "socks"
                             settings = LazyOutboundConfigurationObject(this,
-                                VMessOutboundConfigurationObject().apply {
-                                    vnext = listOf(
-                                        VMessOutboundConfigurationObject.ServerObject()
+                                SocksOutboundConfigurationObject().apply {
+                                    servers = listOf(
+                                        SocksOutboundConfigurationObject.ServerObject()
                                             .apply {
                                                 address = bean.serverAddress
                                                 port = bean.serverPort
-                                                users = listOf(
-                                                    VMessOutboundConfigurationObject.ServerObject.UserObject()
-                                                        .apply {
-                                                            id = bean.uuidOrGenerate()
-                                                            alterId = bean.alterId
-                                                            security =
-                                                                bean.encryption.takeIf { it.isNotBlank() }
-                                                                    ?: "auto"
-                                                            level = 8
-                                                        }
-                                                )
+                                                if (!bean.username.isNullOrBlank()) {
+                                                    users =
+                                                        listOf(SocksOutboundConfigurationObject.ServerObject.UserObject()
+                                                            .apply {
+                                                                user = bean.username
+                                                                pass = bean.password
+                                                            })
+                                                }
                                             }
                                     )
                                 })
-                        } else if (bean is VLESSBean) {
-                            protocol = "vless"
+                            if (bean.tls || needKeepAliveInterval) {
+                                streamSettings = StreamSettingsObject().apply {
+                                    network = "tcp"
+                                    if (bean.tls) {
+                                        security = "tls"
+                                        if (bean.sni.isNotBlank()) {
+                                            tlsSettings = TLSObject().apply {
+                                                serverName = bean.sni
+                                            }
+                                        }
+                                    }
+                                    if (needKeepAliveInterval) {
+                                        sockopt =
+                                            StreamSettingsObject.SockoptObject().apply {
+                                                tcpKeepAliveInterval = keepAliveInterval
+                                            }
+                                    }
+                                }
+                            }
+                        } else if (bean is HttpBean) {
+                            protocol = "http"
                             settings = LazyOutboundConfigurationObject(this,
-                                VLESSOutboundConfigurationObject().apply {
-                                    vnext = listOf(
-                                        VLESSOutboundConfigurationObject.ServerObject()
+                                HTTPOutboundConfigurationObject().apply {
+                                    servers = listOf(
+                                        HTTPOutboundConfigurationObject.ServerObject()
                                             .apply {
                                                 address = bean.serverAddress
                                                 port = bean.serverPort
-                                                users = listOf(
-                                                    VLESSOutboundConfigurationObject.ServerObject.UserObject()
-                                                        .apply {
-                                                            id = bean.uuidOrGenerate()
-                                                            encryption = bean.encryption
-                                                            level = 8
-                                                        }
-                                                )
+                                                if (!bean.username.isNullOrBlank()) {
+                                                    users =
+                                                        listOf(HTTPInboundConfigurationObject.AccountObject()
+                                                            .apply {
+                                                                user = bean.username
+                                                                pass = bean.password
+                                                            })
+                                                }
                                             }
                                     )
                                 })
-                        }
-
-                        streamSettings = StreamSettingsObject().apply {
-                            network = bean.type
-                            if (bean.security.isNotBlank()) {
-                                security = bean.security
-                            }
-                            if (security == "tls") {
-                                tlsSettings = TLSObject().apply {
-                                    if (bean.sni.isNotBlank()) {
-                                        serverName = bean.sni
+                            if (bean.tls || needKeepAliveInterval) {
+                                streamSettings = StreamSettingsObject().apply {
+                                    network = "tcp"
+                                    if (bean.tls) {
+                                        security = "tls"
+                                        if (bean.sni.isNotBlank()) {
+                                            tlsSettings = TLSObject().apply {
+                                                serverName = bean.sni
+                                            }
+                                        }
                                     }
-                                    if (bean.alpn.isNotBlank()) {
-                                        alpn = bean.alpn.split(",")
+                                    if (needKeepAliveInterval) {
+                                        sockopt =
+                                            StreamSettingsObject.SockoptObject().apply {
+                                                tcpKeepAliveInterval = keepAliveInterval
+                                            }
                                     }
                                 }
                             }
-
-                            when (network) {
-                                "tcp" -> {
-                                    tcpSettings = TcpObject().apply {
-                                        if (bean.headerType == "http") {
-                                            header = TcpObject.HeaderObject().apply {
-                                                type = "http"
-                                                if (bean.host.isNotBlank() || bean.path.isNotBlank()) {
-                                                    request =
-                                                        TcpObject.HeaderObject.HTTPRequestObject()
+                        } else if (bean is StandardV2RayBean) {
+                            if (bean is VMessBean) {
+                                protocol = "vmess"
+                                settings = LazyOutboundConfigurationObject(this,
+                                    VMessOutboundConfigurationObject().apply {
+                                        vnext = listOf(
+                                            VMessOutboundConfigurationObject.ServerObject()
+                                                .apply {
+                                                    address = bean.serverAddress
+                                                    port = bean.serverPort
+                                                    users = listOf(
+                                                        VMessOutboundConfigurationObject.ServerObject.UserObject()
                                                             .apply {
-                                                                headers = mutableMapOf()
-                                                                if (bean.host.isNotBlank()) {
-                                                                    headers["Host"] = TcpObject.HeaderObject.StringOrListObject().apply {
-                                                                        valueY = bean.host.split(",")
-                                                                            .map { it.trim() }
-                                                                    }
-                                                                }
-                                                                if (bean.path.isNotBlank()) {
-                                                                    path = bean.path.split(",")
-                                                                }
+                                                                id = bean.uuidOrGenerate()
+                                                                alterId = bean.alterId
+                                                                security =
+                                                                    bean.encryption.takeIf { it.isNotBlank() }
+                                                                        ?: "auto"
+                                                                level = 8
                                                             }
+                                                    )
                                                 }
-                                            }
-                                        }
-                                    }
+                                        )
+                                    })
+                            } else if (bean is VLESSBean) {
+                                protocol = "vless"
+                                settings = LazyOutboundConfigurationObject(this,
+                                    VLESSOutboundConfigurationObject().apply {
+                                        vnext = listOf(
+                                            VLESSOutboundConfigurationObject.ServerObject()
+                                                .apply {
+                                                    address = bean.serverAddress
+                                                    port = bean.serverPort
+                                                    users = listOf(
+                                                        VLESSOutboundConfigurationObject.ServerObject.UserObject()
+                                                            .apply {
+                                                                id = bean.uuidOrGenerate()
+                                                                encryption = bean.encryption
+                                                                level = 8
+                                                            }
+                                                    )
+                                                }
+                                        )
+                                    })
+                            }
+
+                            streamSettings = StreamSettingsObject().apply {
+                                network = bean.type
+                                if (bean.security.isNotBlank()) {
+                                    security = bean.security
                                 }
-                                "kcp" -> {
-                                    kcpSettings = KcpObject().apply {
-                                        mtu = 1350
-                                        tti = 50
-                                        uplinkCapacity = 12
-                                        downlinkCapacity = 100
-                                        congestion = false
-                                        readBufferSize = 1
-                                        writeBufferSize = 1
-                                        header = KcpObject.HeaderObject().apply {
-                                            type = bean.headerType
+                                if (security == "tls") {
+                                    tlsSettings = TLSObject().apply {
+                                        if (bean.sni.isNotBlank()) {
+                                            serverName = bean.sni
                                         }
-                                        if (bean.mKcpSeed.isNotBlank()) {
-                                            seed = bean.mKcpSeed
+                                        if (bean.alpn.isNotBlank()) {
+                                            alpn = bean.alpn.split(",")
                                         }
                                     }
                                 }
-                                "ws" -> {
-                                    wsSettings = WebSocketObject().apply {
-                                        headers = mutableMapOf()
 
-                                        if (bean.host.isNotBlank()) {
-                                            headers["Host"] = bean.host
+                                when (network) {
+                                    "tcp" -> {
+                                        tcpSettings = TcpObject().apply {
+                                            if (bean.headerType == "http") {
+                                                header = TcpObject.HeaderObject().apply {
+                                                    type = "http"
+                                                    if (bean.host.isNotBlank() || bean.path.isNotBlank()) {
+                                                        request =
+                                                            TcpObject.HeaderObject.HTTPRequestObject()
+                                                                .apply {
+                                                                    headers = mutableMapOf()
+                                                                    if (bean.host.isNotBlank()) {
+                                                                        headers["Host"] =
+                                                                            TcpObject.HeaderObject.StringOrListObject()
+                                                                                .apply {
+                                                                                    valueY =
+                                                                                        bean.host.split(
+                                                                                            ","
+                                                                                        )
+                                                                                            .map { it.trim() }
+                                                                                }
+                                                                    }
+                                                                    if (bean.path.isNotBlank()) {
+                                                                        path = bean.path.split(",")
+                                                                    }
+                                                                }
+                                                    }
+                                                }
+                                            }
                                         }
+                                    }
+                                    "kcp" -> {
+                                        kcpSettings = KcpObject().apply {
+                                            mtu = 1350
+                                            tti = 50
+                                            uplinkCapacity = 12
+                                            downlinkCapacity = 100
+                                            congestion = false
+                                            readBufferSize = 1
+                                            writeBufferSize = 1
+                                            header = KcpObject.HeaderObject().apply {
+                                                type = bean.headerType
+                                            }
+                                            if (bean.mKcpSeed.isNotBlank()) {
+                                                seed = bean.mKcpSeed
+                                            }
+                                        }
+                                    }
+                                    "ws" -> {
+                                        wsSettings = WebSocketObject().apply {
+                                            headers = mutableMapOf()
 
-                                        path = bean.path.takeIf { it.isNotBlank() } ?: "/"
-
-                                        if (bean.wsMaxEarlyData > 0) {
-                                            maxEarlyData = bean.wsMaxEarlyData
+                                            if (bean.host.isNotBlank()) {
+                                                headers["Host"] = bean.host
+                                            }
 
-                                            val pathUrl = "http://localhost$path".toHttpUrlOrNull()
-                                            if (pathUrl != null) {
-                                                pathUrl.queryParameter("ed")?.let {
-                                                    path = pathUrl.newBuilder()
-                                                        .removeAllQueryParameters("ed")
-                                                        .build()
-                                                        .toString()
-                                                        .substringAfter("http://localhost")
-                                                    earlyDataHeaderName = "Sec-WebSocket-Protocol"
+                                            path = bean.path.takeIf { it.isNotBlank() } ?: "/"
+
+                                            if (bean.wsMaxEarlyData > 0) {
+                                                maxEarlyData = bean.wsMaxEarlyData
+
+                                                val pathUrl =
+                                                    "http://localhost$path".toHttpUrlOrNull()
+                                                if (pathUrl != null) {
+                                                    pathUrl.queryParameter("ed")?.let {
+                                                        path = pathUrl.newBuilder()
+                                                            .removeAllQueryParameters("ed")
+                                                            .build()
+                                                            .toString()
+                                                            .substringAfter("http://localhost")
+                                                        earlyDataHeaderName =
+                                                            "Sec-WebSocket-Protocol"
+                                                    }
                                                 }
                                             }
-                                        }
 
-                                        if (bean.wsUseBrowserForwarder) {
-                                            useBrowserForwarding = true
-                                            requireWs = true
+                                            if (bean.wsUseBrowserForwarder) {
+                                                useBrowserForwarding = true
+                                                requireWs = true
+                                            }
                                         }
                                     }
-                                }
-                                "http", "h2" -> {
-                                    network = "h2"
+                                    "http", "h2" -> {
+                                        network = "h2"
 
-                                    httpSettings = HttpObject().apply {
-                                        if (bean.host.isNotBlank()) {
-                                            host = bean.host.split(",")
-                                        }
+                                        httpSettings = HttpObject().apply {
+                                            if (bean.host.isNotBlank()) {
+                                                host = bean.host.split(",")
+                                            }
 
-                                        path = bean.path.takeIf { it.isNotBlank() } ?: "/"
+                                            path = bean.path.takeIf { it.isNotBlank() } ?: "/"
+                                        }
                                     }
-                                }
-                                "quic" -> {
-                                    quicSettings = QuicObject().apply {
-                                        security =
-                                            bean.quicSecurity.takeIf { it.isNotBlank() } ?: "none"
-                                        key = bean.quicKey
-                                        header = QuicObject.HeaderObject().apply {
-                                            type =
-                                                bean.headerType.takeIf { it.isNotBlank() } ?: "none"
+                                    "quic" -> {
+                                        quicSettings = QuicObject().apply {
+                                            security =
+                                                bean.quicSecurity.takeIf { it.isNotBlank() }
+                                                    ?: "none"
+                                            key = bean.quicKey
+                                            header = QuicObject.HeaderObject().apply {
+                                                type =
+                                                    bean.headerType.takeIf { it.isNotBlank() }
+                                                        ?: "none"
+                                            }
                                         }
                                     }
-                                }
-                                "grpc" -> {
-                                    grpcSettings = GrpcObject().apply {
-                                        serviceName = bean.grpcServiceName
+                                    "grpc" -> {
+                                        grpcSettings = GrpcObject().apply {
+                                            serviceName = bean.grpcServiceName
+                                        }
                                     }
                                 }
-                            }
 
-                            if (needKeepAliveInterval) {
-                                sockopt = StreamSettingsObject.SockoptObject().apply {
-                                    tcpKeepAliveInterval = keepAliveInterval
+                                if (needKeepAliveInterval) {
+                                    sockopt = StreamSettingsObject.SockoptObject().apply {
+                                        tcpKeepAliveInterval = keepAliveInterval
+                                    }
                                 }
-                            }
 
-                        }
-                    } else if (bean is ShadowsocksBean) {
-                        protocol = "shadowsocks"
-                        settings = LazyOutboundConfigurationObject(this,
-                            ShadowsocksOutboundConfigurationObject().apply {
-                                servers = listOf(
-                                    ShadowsocksOutboundConfigurationObject.ServerObject()
-                                        .apply {
-                                            address = bean.serverAddress
-                                            port = bean.serverPort
-                                            method = bean.method
-                                            password = bean.password
-                                        }
-                                )
-                                if (needKeepAliveInterval) {
-                                    streamSettings = StreamSettingsObject().apply {
-                                        sockopt =
-                                            StreamSettingsObject.SockoptObject().apply {
-                                                tcpKeepAliveInterval = keepAliveInterval
+                            }
+                        } else if (bean is ShadowsocksBean) {
+                            protocol = "shadowsocks"
+                            settings = LazyOutboundConfigurationObject(this,
+                                ShadowsocksOutboundConfigurationObject().apply {
+                                    servers = listOf(
+                                        ShadowsocksOutboundConfigurationObject.ServerObject()
+                                            .apply {
+                                                address = bean.serverAddress
+                                                port = bean.serverPort
+                                                method = bean.method
+                                                password = bean.password
                                             }
+                                    )
+                                    if (needKeepAliveInterval) {
+                                        streamSettings = StreamSettingsObject().apply {
+                                            sockopt =
+                                                StreamSettingsObject.SockoptObject().apply {
+                                                    tcpKeepAliveInterval = keepAliveInterval
+                                                }
+                                        }
                                     }
+                                })
+                        } else if (bean is TrojanBean) {
+                            protocol = "trojan"
+                            settings = LazyOutboundConfigurationObject(this,
+                                TrojanOutboundConfigurationObject().apply {
+                                    servers = listOf(
+                                        TrojanOutboundConfigurationObject.ServerObject()
+                                            .apply {
+                                                address = bean.serverAddress
+                                                port = bean.serverPort
+                                                password = bean.password
+                                                level = 8
+                                            }
+                                    )
                                 }
-                            })
-                    } else if (bean is TrojanBean) {
-                        protocol = "trojan"
-                        settings = LazyOutboundConfigurationObject(this,
-                            TrojanOutboundConfigurationObject().apply {
-                                servers = listOf(
-                                    TrojanOutboundConfigurationObject.ServerObject()
-                                        .apply {
-                                            address = bean.serverAddress
-                                            port = bean.serverPort
-                                            password = bean.password
-                                            level = 8
-                                        }
-                                )
-                            }
-                        )
-                        streamSettings = StreamSettingsObject().apply {
-                            network = "tcp"
-                            security = "tls"
-                            if (bean.sni.isNotBlank()) {
-                                tlsSettings = TLSObject().apply {
-                                    serverName = bean.sni
+                            )
+                            streamSettings = StreamSettingsObject().apply {
+                                network = "tcp"
+                                security = "tls"
+                                if (bean.sni.isNotBlank()) {
+                                    tlsSettings = TLSObject().apply {
+                                        serverName = bean.sni
+                                    }
                                 }
-                            }
-                            if (needKeepAliveInterval) {
-                                sockopt = StreamSettingsObject.SockoptObject().apply {
-                                    tcpKeepAliveInterval = keepAliveInterval
+                                if (needKeepAliveInterval) {
+                                    sockopt = StreamSettingsObject.SockoptObject().apply {
+                                        tcpKeepAliveInterval = keepAliveInterval
+                                    }
                                 }
                             }
                         }
-                    }
-                    if (index == 0 && proxyEntity.needCoreMux() && DataStore.enableMux) {
-                        mux = OutboundObject.MuxObject().apply {
-                            enabled = true
-                            concurrency = DataStore.muxConcurrency
+                        if (index == 0 && proxyEntity.needCoreMux() && DataStore.enableMux) {
+                            mux = OutboundObject.MuxObject().apply {
+                                enabled = true
+                                concurrency = DataStore.muxConcurrency
+                            }
                         }
-                    }
-                    tag = if (index == 0) TAG_AGENT else "${proxyEntity.id}"
-                    if (pastExternal) {
-                        inbounds.add(InboundObject().apply {
-                            tag = "${proxyEntity.id}-in"
-                            listen = "127.0.0.1"
-                            port = localPort
-                            protocol = "socks"
-                            settings = LazyInboundConfigurationObject(this,
-                                SocksInboundConfigurationObject().apply {
-                                    auth = "noauth"
-                                    udp = true
-                                    userLevel = 8
-                                })
-                            if (trafficSniffing) {
-                                sniffing = InboundObject.SniffingObject().apply {
-                                    enabled = true
-                                    destOverride = listOf("http", "tls")
-                                    metadataOnly = false
+                        tag = if (index == 0) tagInbound else "$tagInbound-${proxyEntity.id}"
+                        if (pastExternal) {
+                            val localPort = requirePort()
+                            chainMap[localPort] = proxyEntity
+                            inbounds.add(InboundObject().apply {
+                                tag = "$tagInbound-${proxyEntity.id}-in"
+                                listen = "127.0.0.1"
+                                port = localPort
+                                protocol = "socks"
+                                settings = LazyInboundConfigurationObject(this,
+                                    SocksInboundConfigurationObject().apply {
+                                        auth = "noauth"
+                                        udp = true
+                                        userLevel = 8
+                                    })
+                                if (trafficSniffing) {
+                                    sniffing = InboundObject.SniffingObject().apply {
+                                        enabled = true
+                                        destOverride = listOf("http", "tls")
+                                        metadataOnly = false
+                                    }
                                 }
-                            }
-                        })
-                        routing.rules.add(RoutingObject.RuleObject().apply {
-                            type = "field"
-                            inboundTag = listOf("${proxyEntity.id}-in")
-                            outboundTag = "${proxyEntity.id}"
-                        })
-                    } else if (index > 0) {
-                        pastOutbound.proxySettings =
-                            OutboundObject.ProxySettingsObject().apply {
-                                tag = "${proxyEntity.id}"
-                                transportLayer = true
-                            }
+                            })
+                            routing.rules.add(RoutingObject.RuleObject().apply {
+                                type = "field"
+                                inboundTag = listOf("$tagInbound-${proxyEntity.id}-in")
+                                outboundTag = "$tagInbound-${proxyEntity.id}"
+                            })
+                        } else if (index > 0) {
+                            pastOutbound.proxySettings =
+                                OutboundObject.ProxySettingsObject().apply {
+                                    tag = "$tagInbound-${proxyEntity.id}"
+                                    transportLayer = true
+                                }
+                        }
                     }
-                }
 
-                pastExternal = false
-                pastOutbound = outbound
-                outbounds.add(outbound)
+                    pastExternal = false
+                    pastOutbound = outbound
+                    outbounds.add(outbound)
+                }
             }
         }
 
-        /*   if (proxies.size > 1) {
-               val outNode = proxies.last()
-               if (!outNode.needExternal()) {
-                   routing.rules.add(RoutingObject.RuleObject().apply {
-                       type = "field"
-                       inboundTag = listOf("${outNode.id}-in")
-                       outboundTag = TAG_DIRECT
-                   })
-               }
-           }*/
+        buildChain(TAG_AGENT, proxies)
+        if (extraProxies.isNotEmpty()) {
+            extraProxies.forEach { (id, entities) ->
+                buildChain("$TAG_AGENT-$id", entities)
+            }
+            routing.rules.add(RoutingObject.RuleObject().apply {
+                type = "field"
+                inboundTag = extraProxies.keys.map { "$TAG_AGENT-$it" }
+                outboundTag = TAG_DIRECT
+            })
+        }
+        extraRules.forEach { rule ->
+            routing.rules.add(RoutingObject.RuleObject().apply {
+                type = "field"
+                if (rule.domains.isNotBlank()) {
+                    domain = rule.domains.split("\n")
+                }
+                if (rule.ip.isNotBlank()) {
+                    ip = rule.ip.split("\n")
+                }
+                if (rule.port.isNotBlank()) {
+                    port = rule.port
+                }
+                if (rule.sourcePort.isNotBlank()) {
+                    sourcePort = rule.sourcePort
+                }
+                if (rule.network.isNotBlank()) {
+                    network = rule.network
+                }
+                if (rule.source.isNotBlank()) {
+                    source = rule.source.split("\n")
+                }
+                if (rule.protocol.isNotBlank()) {
+                    protocol = rule.protocol.split("\n")
+                }
+                if (rule.attrs.isNotBlank()) {
+                    attrs = rule.attrs
+                }
+                outboundTag = when (val outId = rule.outbound) {
+                    0L -> TAG_AGENT
+                    -1L -> TAG_DIRECT
+                    -2L -> TAG_BLOCK
+                    else -> "$TAG_AGENT-$outId"
+                }
+            })
+        }
 
         if (requireWs) {
             browserForwarder = BrowserForwarderObject().apply {

+ 2 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt

@@ -23,6 +23,8 @@ package io.nekohasekai.sagernet.fmt.v2ray
 
 import cn.hutool.core.codec.Base64
 import cn.hutool.json.JSONObject
+import io.nekohasekai.sagernet.database.ProxyEntity
+import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.OutboundObject
 import io.nekohasekai.sagernet.ktx.*
 import okhttp3.HttpUrl.Companion.toHttpUrl
 import okhttp3.HttpUrl.Companion.toHttpUrlOrNull