/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai
*
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.fmt
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.provider.Settings
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginManager
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import io.nekohasekai.sagernet.Key
import io.nekohasekai.sagernet.LogLevel
import io.nekohasekai.sagernet.RouteMode
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.TLS_FRAGMENTATION_METHOD
import io.nekohasekai.sagernet.TunImplementation
import io.nekohasekai.sagernet.bg.VpnService
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.database.SagerDatabase
import io.nekohasekai.sagernet.fmt.V2rayBuildResult.IndexEntity
import io.nekohasekai.sagernet.fmt.anytls.AnyTLSBean
import io.nekohasekai.sagernet.fmt.gson.gson
import io.nekohasekai.sagernet.fmt.http.HttpBean
import io.nekohasekai.sagernet.fmt.http3.Http3Bean
import io.nekohasekai.sagernet.fmt.hysteria2.Hysteria2Bean
import io.nekohasekai.sagernet.fmt.internal.BalancerBean
import io.nekohasekai.sagernet.fmt.internal.ConfigBean
import io.nekohasekai.sagernet.fmt.juicity.JuicityBean
import io.nekohasekai.sagernet.fmt.mieru.MieruBean
import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
import io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean
import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
import io.nekohasekai.sagernet.fmt.ssh.SSHBean
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
import io.nekohasekai.sagernet.fmt.trusttunnel.TrustTunnelBean
import io.nekohasekai.sagernet.fmt.snell.SnellBean
import io.nekohasekai.sagernet.fmt.tuic5.Tuic5Bean
import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.BrowserDialerObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.BrowserForwarderObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.DNSOutboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.DnsObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.DokodemoDoorInboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.FakeDnsObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.FreedomOutboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.GrpcObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.HTTPInboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.HTTPOutboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.HTTPUpgradeObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.HttpObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.Hysteria2Object
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.Hysteria2OutboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.InboundObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.KcpObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.LazyInboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.LazyOutboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.LogObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.MeekObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.MekyaObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.MultiObservatoryObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.ObservatoryObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.OutboundObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.PolicyObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.QuicObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.RealityObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.RoutingObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.RoutingObject.BalancerObject.StrategyObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.SSHOutboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.ShadowsocksOutboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.SocksInboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.SocksOutboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.SplitHTTPObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.StreamSettingsObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.TLSObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.TcpObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.TrojanOutboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.VLESSOutboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.VMessOutboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.WebSocketObject
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.WireGuardOutboundConfigurationObject
import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean
import io.nekohasekai.sagernet.fmt.v2ray.VMessBean
import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ktx.getArray
import io.nekohasekai.sagernet.ktx.getBoolean
import io.nekohasekai.sagernet.ktx.getBooleanProperty
import io.nekohasekai.sagernet.ktx.getInt
import io.nekohasekai.sagernet.ktx.getObject
import io.nekohasekai.sagernet.ktx.getString
import io.nekohasekai.sagernet.ktx.getStringArray
import io.nekohasekai.sagernet.ktx.isValidHysteriaMultiPort
import io.nekohasekai.sagernet.ktx.joinHostPort
import io.nekohasekai.sagernet.ktx.listByLine
import io.nekohasekai.sagernet.ktx.listByLineOrComma
import io.nekohasekai.sagernet.ktx.mkPort
import io.nekohasekai.sagernet.ktx.parseJson
import io.nekohasekai.sagernet.ktx.toHysteriaPort
import io.nekohasekai.sagernet.ktx.unescapeLineFeed
import io.nekohasekai.sagernet.ktx.uuidOrGenerate
import io.nekohasekai.sagernet.utils.PackageCache
import kotlin.io.encoding.Base64
import libexclavecore.Libexclavecore
import java.io.File
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
const val TAG_SOCKS = "socks"
const val TAG_HTTP = "http"
const val TAG_TRANS = "trans"
const val TAG_TRANS6 = "trans6"
const val TAG_AGENT = "proxy"
const val TAG_DIRECT = "direct"
const val TAG_BYPASS = "bypass"
const val TAG_BLOCK = "block"
const val TAG_DNS_IN = "dns-in"
const val TAG_DNS_OUT = "dns-out"
const val TAG_DNS_DIRECT = "dns-direct"
const val LOCALHOST = "127.0.0.1"
const val LOCALHOST6 = "::1"
class V2rayBuildResult(
var config: String,
var index: List,
var requireWs: Boolean,
var wsPort: Int,
var requireSh: Boolean,
var shPort: Int,
var outboundTags: List,
var outboundTagsCurrent: List,
var outboundTagsAll: Map,
var bypassTag: String,
var observerTag: String,
var observatoryTags: Set,
val dumpUID: Boolean,
val alerts: List>,
val useFakeDNS: Boolean,
) {
data class IndexEntity(var isBalancer: Boolean, var chain: LinkedHashMap, ProxyEntity>)
}
@OptIn(ExperimentalUuidApi::class)
fun buildV2RayConfig(
proxy: ProxyEntity, forTest: Boolean = false, forExport: Boolean = false
): V2rayBuildResult {
if (proxy.type == ProxyEntity.TYPE_CONFIG && proxy.configBean!!.type == "v2ray") {
return buildCustomConfig(proxy, forTest, forExport)
}
val outboundTags = ArrayList()
val outboundTagsCurrent = ArrayList()
val outboundTagsAll = HashMap()
val globalOutbounds = ArrayList()
fun ProxyEntity.resolveChainRecursively(): MutableList {
when (type) {
ProxyEntity.TYPE_BALANCER -> error("balancer in proxy chain is not supported")
ProxyEntity.TYPE_CHAIN -> {
val beans = SagerDatabase.proxyDao.getEntities(chainBean!!.proxies)
val beansMap = beans.associateBy { it.id }
val beanList = ArrayList()
for ((index, proxyId) in chainBean!!.proxies.withIndex()) {
val item = beansMap[proxyId] ?: continue
if (!item.requireBean().canMapping() && index != 0) error("${item.displayName()} can be the front proxy only")
if (item.type == ProxyEntity.TYPE_CONFIG && item.configBean!!.type == "v2ray") error("custom config in proxy chain is not supported")
beanList.addAll(item.resolveChainRecursively())
}
return beanList
}
else -> return mutableListOf(this)
}
}
fun ProxyEntity.resolveChain(): MutableList {
if (type == ProxyEntity.TYPE_BALANCER) {
val beans = if (balancerBean!!.type == BalancerBean.TYPE_LIST) {
SagerDatabase.proxyDao.getEntities(balancerBean!!.proxies)
} else {
SagerDatabase.proxyDao.getByGroup(balancerBean!!.groupId)
.filter { if (balancerBean!!.nameFilter.isEmpty()) { true } else { !Regex(balancerBean!!.nameFilter).containsMatchIn(it.requireBean().name) } }
.filter { if (balancerBean!!.nameFilter1.isEmpty()) { true } else { Regex(balancerBean!!.nameFilter1).containsMatchIn(it.requireBean().name) } }
}
val beansMap = beans.associateBy { it.id }
val beanList = ArrayList()
// For balancer, we don't add landing proxy here
// It will be handled in buildChain() function
for (proxyId in beansMap.keys) {
val item = beansMap[proxyId] ?: continue
if (item.id == id) continue
when (item.type) {
ProxyEntity.TYPE_BALANCER -> error("balancer in balancer is not supported")
ProxyEntity.TYPE_CHAIN -> error("proxy chain in balancer is not supported")
ProxyEntity.TYPE_CONFIG -> if (item.configBean!!.type == "v2ray") error("custom config in balancer is not supported")
}
beanList.add(item)
}
return beanList
}
val list = resolveChainRecursively().asReversed()
if (type == ProxyEntity.TYPE_CHAIN) return list
if (type == ProxyEntity.TYPE_CONFIG && configBean!!.type == "v2ray") return list
SagerDatabase.groupDao.getById(groupId)?.let { group ->
group.frontProxy.takeIf { it > 0L }?.let { id ->
SagerDatabase.proxyDao.getById(id)?.let {
when (it.type) {
ProxyEntity.TYPE_BALANCER -> error("balancer can not be the front proxy")
ProxyEntity.TYPE_CHAIN -> list.addAll(it.resolveChainRecursively().asReversed())
else -> {
if (it.type == ProxyEntity.TYPE_CONFIG && it.configBean!!.type == "v2ray") error("custom config can not be the front proxy")
list.add(it)
}
}
} ?: error("front proxy not found for ${group.displayName()}")
}
group.landingProxy.takeIf { it > 0L }?.let { id ->
SagerDatabase.proxyDao.getById(id)?.let {
when (it.type) {
ProxyEntity.TYPE_BALANCER -> error("balancer can not be the landing proxy")
ProxyEntity.TYPE_CHAIN -> list.addAll(0, it.resolveChainRecursively().asReversed())
else -> {
if (it.type == ProxyEntity.TYPE_CONFIG && it.configBean!!.type == "v2ray") error("custom config can not be the landing proxy")
if (!it.requireBean().canMapping()) error("${it.displayName()} can be the front proxy only and can not be the landing proxy")
list.add(0, it)
}
}
} ?: error("landing proxy not found for ${group.displayName()}")
}
}
return list
}
val routeMode = DataStore.routeMode
val proxies = proxy.resolveChain()
val extraRules = if (forTest || routeMode != RouteMode.RULE) listOf() else SagerDatabase.rulesDao.enabledRules()
val extraProxies = if (forTest) mapOf() else SagerDatabase.proxyDao.getEntities(extraRules.mapNotNull { rule ->
rule.outbound.takeIf { it > 0 && it != proxy.id }
}.toHashSet().toList()).associate {
(it.id to ((it.type == ProxyEntity.TYPE_BALANCER) to lazy {
it.balancerBean
})) to it.resolveChain()
}
val allowAccess = DataStore.allowAccess
val bind = if (!forTest && allowAccess) "0.0.0.0" else LOCALHOST
var directDNS = DataStore.directDns.listByLineOrComma().filter { !it.startsWith("#") }
if (DataStore.useLocalDnsAsDirectDns) directDNS = listOf("localhost")
val remoteDNS = if (routeMode == RouteMode.DIRECT) {
directDNS
} else {
DataStore.remoteDns.listByLineOrComma().filter { !it.startsWith("#") }
}
var bootstrapDNS = DataStore.bootstrapDns.listByLineOrComma().filter { !it.startsWith("#") }
if (DataStore.useLocalDnsAsBootstrapDns) bootstrapDNS = listOf("localhost")
val useFakeDns = DataStore.enableFakeDns
val remoteDnsQueryStrategy = DataStore.remoteDnsQueryStrategy
val directDnsQueryStrategy = DataStore.directDnsQueryStrategy
val trafficSniffing = DataStore.trafficSniffing
val indexMap = ArrayList()
var requireWs = false
var requireSh = false
val destinationOverride = DataStore.destinationOverride
val trafficStatistics = !forTest && DataStore.profileTrafficStatistics
var hasTagDirect = false
var directNeedsInterruption = false
val shouldDumpUID = extraRules.any { it.packages.isNotEmpty() || it.customPackageNames.isNotEmpty() }
val alerts = mutableListOf>()
lateinit var result: V2rayBuildResult
V2RayConfig().apply {
dns = DnsObject().apply {
if (DataStore.hosts.isNotEmpty()) {
hosts = mutableMapOf()
for (singleLine in DataStore.hosts.listByLine()) {
val key = singleLine.substringBefore(" ")
val values = singleLine.substringAfter(" ").split("\\s+".toRegex()).toMutableList()
if (hosts.contains(key)) {
if (!hosts[key]!!.valueX.isNullOrEmpty()) {
values.add(hosts[key]!!.valueX)
} else if (!hosts[key]!!.valueY.isNullOrEmpty()) {
values.addAll(hosts[key]!!.valueY)
}
}
if (values.size > 1) {
hosts[key] = DnsObject.StringOrListObject().apply {
valueX = null
valueY = values
}
} else if (values.size == 1) {
hosts[key] = DnsObject.StringOrListObject().apply {
valueX = values[0]
valueY = null
}
}
}
}
servers = mutableListOf()
fallbackStrategy = "disabledIfAnyMatch"
}
log = LogObject().apply {
loglevel = when (DataStore.logLevel) {
LogLevel.DEBUG -> "debug"
LogLevel.INFO -> "info"
LogLevel.WARNING -> "warning"
LogLevel.ERROR -> "error"
else -> "none"
}
if (DataStore.logLevel == LogLevel.NONE) {
access = "none"
}
}
policy = PolicyObject().apply {
levels = mapOf(
// dns
"1" to PolicyObject.LevelPolicyObject().apply {
connIdle = 30
})
if (trafficStatistics) {
system = PolicyObject.SystemPolicyObject().apply {
statsOutboundDownlink = true
statsOutboundUplink = true
}
}
}
inbounds = mutableListOf()
if (!forTest) {
if (!forExport) {
inbounds.add(InboundObject().apply {
tag = "ipc-in"
protocol = "ipc"
val path = SagerNet.deviceStorage.noBackupFilesDir.toString() + "/ipc.sock"
val udsFile = File(path)
if (udsFile.exists()) udsFile.delete()
listen = path
if (trafficSniffing || useFakeDns) {
sniffing = InboundObject.SniffingObject().apply {
enabled = true
destOverride = when {
useFakeDns && !trafficSniffing -> listOf("fakedns")
useFakeDns -> listOf("fakedns", "http", "tls", "quic")
else -> listOf("http", "tls", "quic")
}
metadataOnly = useFakeDns && !trafficSniffing
routeOnly = !destinationOverride
}
}
})
}
if (DataStore.requireSocks) {
inbounds.add(InboundObject().apply {
tag = TAG_SOCKS
listen = bind
port = DataStore.socksPort
protocol = "socks"
settings = LazyInboundConfigurationObject(this, SocksInboundConfigurationObject().apply {
if (DataStore.socksUsername.isEmpty() && DataStore.socksPassword.isEmpty()) {
auth = "noauth"
} else if (DataStore.socksUsername.isEmpty() && DataStore.socksPassword.isNotEmpty()) {
error("username is empty but password is not empty for SOCKS5 inbound")
} else if (DataStore.socksUsername.isNotEmpty() && DataStore.socksPassword.isEmpty()) {
error("username is not empty but password is empty for SOCKS5 inbound")
} else {
auth = "password"
accounts = listOf(SocksInboundConfigurationObject.AccountObject().apply {
user = DataStore.socksUsername
pass = DataStore.socksPassword
})
}
udp = DataStore.socksUDP
})
if (trafficSniffing || useFakeDns) {
sniffing = InboundObject.SniffingObject().apply {
enabled = true
destOverride = when {
useFakeDns && !trafficSniffing -> listOf("fakedns")
useFakeDns -> listOf("fakedns", "http", "tls", "quic")
else -> listOf("http", "tls", "quic")
}
metadataOnly = useFakeDns && !trafficSniffing
routeOnly = !destinationOverride
}
}
if (shouldDumpUID) dumpUID = true
})
}
if (DataStore.requireHttp) {
inbounds.add(InboundObject().apply {
tag = TAG_HTTP
listen = bind
port = DataStore.httpPort
protocol = "http"
settings = LazyInboundConfigurationObject(this,
HTTPInboundConfigurationObject().apply {
allowTransparent = true
if (DataStore.httpUsername.isNotEmpty() || DataStore.httpPassword.isNotEmpty()) {
accounts = listOf(HTTPInboundConfigurationObject.AccountObject().apply {
user = DataStore.httpUsername
pass = DataStore.httpPassword
})
}
})
if (trafficSniffing || useFakeDns) {
sniffing = InboundObject.SniffingObject().apply {
enabled = true
destOverride = when {
useFakeDns && !trafficSniffing -> listOf("fakedns")
useFakeDns -> listOf("fakedns", "http", "tls")
else -> listOf("http", "tls")
}
metadataOnly = useFakeDns && !trafficSniffing
routeOnly = !destinationOverride
}
}
if (shouldDumpUID) dumpUID = true
})
}
if (DataStore.requireTransproxy) {
inbounds.add(InboundObject().apply {
tag = TAG_TRANS
listen = bind
port = DataStore.transproxyPort
protocol = "dokodemo-door"
settings = LazyInboundConfigurationObject(this,
DokodemoDoorInboundConfigurationObject().apply {
network = "tcp"
followRedirect = true
})
if (trafficSniffing || useFakeDns) {
sniffing = InboundObject.SniffingObject().apply {
enabled = true
destOverride = when {
useFakeDns && !trafficSniffing -> listOf("fakedns")
useFakeDns -> listOf("fakedns", "http", "tls")
else -> listOf("http", "tls")
}
metadataOnly = useFakeDns && !trafficSniffing
routeOnly = !destinationOverride
}
}
if (shouldDumpUID) dumpUID = true
})
if (bind == LOCALHOST) {
inbounds.add(InboundObject().apply {
tag = TAG_TRANS6
listen = LOCALHOST6
port = DataStore.transproxyPort
protocol = "dokodemo-door"
settings = LazyInboundConfigurationObject(this,
DokodemoDoorInboundConfigurationObject().apply {
network = "tcp"
followRedirect = true
})
if (trafficSniffing || useFakeDns) {
sniffing = InboundObject.SniffingObject().apply {
enabled = true
destOverride = when {
useFakeDns && !trafficSniffing -> listOf("fakedns")
useFakeDns -> listOf("fakedns", "http", "tls")
else -> listOf("http", "tls")
}
metadataOnly = useFakeDns && !trafficSniffing
routeOnly = !destinationOverride
}
}
if (shouldDumpUID) dumpUID = true
})
}
}
}
outbounds = mutableListOf()
routing = RoutingObject().apply {
domainStrategy = DataStore.domainStrategy
rules = mutableListOf()
val wsRules = HashMap()
for (proxyEntity in proxies) {
val bean = proxyEntity.requireBean()
val needBrowserForwarder = when {
bean !is StandardV2RayBean -> false
bean.type == "ws" && bean.wsUseBrowserForwarder -> true
bean.type == "splithttp" && bean.shUseBrowserForwarder -> true
else -> false
}
if (needBrowserForwarder) {
hasTagDirect = true
bean as StandardV2RayBean
// dirty hack to exclude browser forwarder traffic from VpnService
// this will not work on all cases,
// but this is not the main function of this software, just keep it broken
if (bean.security == "none" && bean.host.isNotEmpty()) {
val host = try {
val u = Libexclavecore.newURL("placeholder").apply {
rawHost = if (Libexclavecore.isIPv6(bean.host)) "[${bean.host}]" else bean.host
}.string
Libexclavecore.parseURL(u).host
} catch (_: Exception) {
bean.host
}
wsRules[host] = RoutingObject.RuleObject().apply {
type = "field"
outboundTag = TAG_DIRECT
port = bean.serverPort.toString()
if (Libexclavecore.isIP(host)) {
ip = listOf(host)
if (DataStore.domainStrategy != "AsIs") {
skipDomain = true
}
} else {
domains = listOf(host)
}
}
}
if (bean.security != "none" && bean.sni.isNotEmpty()) {
wsRules[bean.sni] = RoutingObject.RuleObject().apply {
type = "field"
outboundTag = TAG_DIRECT
port = bean.serverPort.toString()
if (!Libexclavecore.isIP(bean.sni)) {
domains = listOf(bean.sni)
}
}
}
if (bean.serverAddress.isNotEmpty()) {
wsRules[bean.serverAddress] = RoutingObject.RuleObject().apply {
type = "field"
outboundTag = TAG_DIRECT
port = bean.serverPort.toString()
if (Libexclavecore.isIP(bean.serverAddress)) {
ip = listOf(bean.serverAddress)
if (DataStore.domainStrategy != "AsIs") {
skipDomain = true
}
} else {
domains = listOf(bean.serverAddress)
}
}
}
}
}
rules.addAll(wsRules.values)
}
var rootBalancer: RoutingObject.RuleObject? = null
var rootObserver: MultiObservatoryObject.MultiObservatoryItem? = null
fun buildChain(
tagOutbound: String,
profileList: List,
isBalancer: Boolean,
balancer: () -> BalancerBean?,
): String {
var pastExternal = false
lateinit var pastOutbound: OutboundObject
lateinit var currentOutbound: OutboundObject
lateinit var pastInboundTag: String
val chainMap = LinkedHashMap, ProxyEntity>()
indexMap.add(IndexEntity(isBalancer, chainMap))
val chainOutbounds = ArrayList()
var chainOutbound = ""
profileList.forEachIndexed { index, proxyEntity ->
val bean = proxyEntity.requireBean()
currentOutbound = OutboundObject()
val tagIn: String
var needGlobal: Boolean
if (isBalancer || index == profileList.lastIndex && !pastExternal) {
tagIn = "$TAG_AGENT-global-${proxyEntity.id}"
needGlobal = true
} else {
tagIn = if (index == 0) tagOutbound else {
"$tagOutbound-${proxyEntity.id}"
}
needGlobal = false
}
if (index == 0) {
chainOutbound = tagIn
}
if (needGlobal) {
if (!globalOutbounds.contains(tagIn)) {
needGlobal = false
globalOutbounds.add(tagIn)
}
}
if (!needGlobal) {
outboundTagsAll[tagIn] = proxyEntity
if (isBalancer || index == 0) {
outboundTags.add(tagIn)
if (tagOutbound == TAG_AGENT) {
outboundTagsCurrent.add(tagIn)
}
}
var currentDomainStrategy = DataStore.outboundDomainStrategy
if (proxyEntity.needExternal()) {
val localPort = mkPort()
val username = Uuid.generateV4().toHexString()
val password = Uuid.generateV4().toHexString()
chainMap[Triple(localPort, username, password)] = proxyEntity
currentOutbound.apply {
protocol = "socks"
settings = LazyOutboundConfigurationObject(this, SocksOutboundConfigurationObject().apply {
servers = listOf(SocksOutboundConfigurationObject.ServerObject().apply {
address = LOCALHOST
port = localPort
users = listOf(SocksOutboundConfigurationObject.ServerObject.UserObject().apply {
user = username
pass = password
})
})
if (proxyEntity.naiveBean != null && proxyEntity.naiveBean!!.singUoT && DataStore.experimentalFlagsProperties.getBooleanProperty( "singuot")) {
uot = true
}
if (proxyEntity.naiveBean != null || proxyEntity.shadowquicBean != null) {
directNeedsInterruption = true
}
})
}
} else {
currentOutbound.apply {
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 {
id = uuidOrGenerate(bean.uuid)
if (bean.alterId > 0) {
alterId = bean.alterId
}
security = bean.encryption.takeIf { it.isNotEmpty() }
?: "auto"
experiments = ""
if (bean.experimentalAuthenticatedLength) {
experiments += "AuthenticatedLength"
}
if (bean.experimentalNoTerminationSignal) {
if (experiments != "") {
experiments += "|"
}
experiments += "NoTerminationSignal"
}
if (experiments.isEmpty()) experiments = null
})
})
when (bean.packetEncoding) {
"packet" -> {
packetEncoding = "packet"
}
"xudp" -> {
packetEncoding = "xudp"
}
}
})
} 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 = uuidOrGenerate(bean.uuid)
encryption = bean.encryption
if (bean.flow.isNotEmpty()) {
flow = bean.flow
}
})
})
when (bean.packetEncoding) {
"packet" -> {
packetEncoding = "packet"
}
"xudp" -> {
packetEncoding = "xudp"
}
}
})
} 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
})
})
} else if (bean is ShadowsocksBean) {
protocol = "shadowsocks"
settings = LazyOutboundConfigurationObject(this,
ShadowsocksOutboundConfigurationObject().apply {
servers = listOf(ShadowsocksOutboundConfigurationObject.ServerObject().apply {
address = bean.serverAddress
port = bean.serverPort
password = bean.password
method = bean.method
if (!bean.method.startsWith("2022-blake3-") && bean.experimentReducedIvHeadEntropy) {
experimentReducedIvHeadEntropy = bean.experimentReducedIvHeadEntropy
}
if (bean.plugin.isNotEmpty()) {
val pluginConfiguration = PluginConfiguration(bean.plugin)
if (pluginConfiguration.selected.isNotEmpty()) {
plugin = pluginConfiguration.selected
pluginOpts = pluginConfiguration.getOptions().toString()
if (!forExport
&& !(plugin == "v2ray-plugin" && DataStore.experimentalFlagsProperties.getBooleanProperty("useInternalV2RayPlugin"))
&& !(plugin == "obfs-local" && DataStore.experimentalFlagsProperties.getBooleanProperty("useInternalObfsLocal"))
) {
try {
PluginManager.init(pluginConfiguration)?.let { (path, opts, isV2) ->
plugin = path
val shouldProtect = if (forTest) {
DataStore.serviceMode == Key.MODE_VPN && DataStore.tunImplementation == TunImplementation.SYSTEM && DataStore.startedProfile > 0 && SagerNet.started
} else {
DataStore.serviceMode == Key.MODE_VPN && DataStore.tunImplementation == TunImplementation.SYSTEM
}
if (shouldProtect) {
pluginWorkingDir = SagerNet.deviceStorage.noBackupFilesDir.toString()
if (isV2) {
opts["__android_vpn"] = ""
} else {
pluginArgs = listOf("-V")
}
}
pluginOpts = opts.toString()
}
} catch (e: PluginManager.PluginNotFoundException) {
if (e.plugin in arrayOf("v2ray-plugin", "obfs-local")) {
plugin = e.plugin
pluginOpts = pluginConfiguration.getOptions().toString()
} else {
throw e
}
}
}
}
}
if (bean.singUoT && DataStore.experimentalFlagsProperties.getBooleanProperty( "singuot")) {
uot = bean.singUoT
}
})
}
)
} else 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.isNotEmpty() || bean.password.isNotEmpty() && bean.protocol == SOCKSBean.PROTOCOL_SOCKS5) {
users = listOf(SocksOutboundConfigurationObject.ServerObject.UserObject().apply {
user = bean.username
pass = bean.password
})
}
if (bean.protocol == SOCKSBean.PROTOCOL_SOCKS4 || bean.protocol == SOCKSBean.PROTOCOL_SOCKS4A && bean.username.isNotEmpty()) {
users = listOf(SocksOutboundConfigurationObject.ServerObject.UserObject().apply {
user = bean.username
})
}
})
version = bean.protocolVersionName()
if (bean.singUoT && DataStore.experimentalFlagsProperties.getBooleanProperty("singuot")) {
uot = bean.singUoT
}
}
)
} 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.isNotEmpty() || bean.password.isNotEmpty()) {
users = listOf(HTTPInboundConfigurationObject.AccountObject().apply {
user = bean.username
pass = bean.password
})
}
})
}
)
}
streamSettings = StreamSettingsObject().apply {
network = bean.type
if (bean.security.isNotEmpty()) {
security = bean.security
}
when (security) {
"tls" -> {
tlsSettings = TLSObject().apply {
if (bean.sni.isNotEmpty()) {
serverName = bean.sni
}
if (bean.alpn.isNotEmpty()) {
alpn = bean.alpn.listByLineOrComma()
}
if (bean.certificates.isNotEmpty() || bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates = mutableListOf()
if (bean.certificates.isNotEmpty()) {
disableSystemRoot = true
certificates.add(TLSObject.CertificateObject().apply {
usage = "verify"
certificate = bean.certificates.lines()
})
}
if (bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates.add(TLSObject.CertificateObject().apply {
usage = "encipherment"
certificate = bean.mtlsCertificate.lines()
key = bean.mtlsCertificatePrivateKey.lines()
})
}
}
if (bean.pinnedPeerCertificateSha256.isNotEmpty()) {
pinnedPeerCertificateSha256 = mutableListOf()
bean.pinnedPeerCertificateSha256.listByLineOrComma().forEach {
pinnedPeerCertificateSha256.add(it.replace(":", ""))
}
}
if (bean.pinnedPeerCertificatePublicKeySha256.isNotEmpty()) {
pinnedPeerCertificatePublicKeySha256 = bean.pinnedPeerCertificatePublicKeySha256.listByLineOrComma()
}
if (bean.pinnedPeerCertificateChainSha256.isNotEmpty()) {
pinnedPeerCertificateChainSha256 = bean.pinnedPeerCertificateChainSha256.listByLineOrComma()
}
if (bean.allowInsecure) {
allowInsecure = true
}
val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForTLS")
if (!overrideFingerprint.isNullOrEmpty()) {
fingerprint = overrideFingerprint
} else if (bean.utlsFingerprint.isNotEmpty()) {
fingerprint = bean.utlsFingerprint
}
if (bean.echEnabled) {
ech = TLSObject.ECHObject().apply {
enabled = bean.echEnabled
if (bean.echConfig.isNotEmpty()) {
config = bean.echConfig
}
}
}
if (bean.serverNameToVerify.isNotEmpty()) {
serverNameToVerify = bean.serverNameToVerify.listByLineOrComma()
}
}
}
"reality" -> {
realitySettings = RealityObject().apply {
if (bean.sni.isNotEmpty()) {
serverName = bean.sni
}
if (bean.realityPublicKey.isNotEmpty()) {
publicKey = bean.realityPublicKey
}
if (bean.realityShortId.isNotEmpty()) {
shortId = bean.realityShortId
}
if (bean.realityMldsa65Verify.isNotEmpty()) {
mldsa65Verify = bean.realityMldsa65Verify
}
val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForREALITY")
if (!overrideFingerprint.isNullOrEmpty()) {
fingerprint = overrideFingerprint
} else if (bean.realityFingerprint.isNotEmpty()) {
fingerprint = bean.realityFingerprint
}
if (DataStore.realityDisableX25519Mlkem768 || bean.realityDisableX25519Mlkem768 && !forExport) {
disableX25519MLKEM768 = true
}
}
}
}
when (network) {
"tcp" -> {
tcpSettings = TcpObject().apply {
if (bean.headerType == "http") {
header = TcpObject.HeaderObject().apply {
type = "http"
if (bean.host.isNotEmpty() || bean.path.isNotEmpty()) {
request = TcpObject.HeaderObject.HTTPRequestObject()
.apply {
headers = mutableMapOf()
if (bean.host.isNotEmpty()) {
headers["Host"] = TcpObject.HeaderObject.StringOrListObject()
.apply {
valueY = bean.host.listByLineOrComma()
}
}
if (bean.path.isNotEmpty()) {
path = bean.path.listByLineOrComma()
}
}
}
}
}
}
}
"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.isNotEmpty()) {
seed = bean.mKcpSeed
}
}
}
"ws" -> {
wsSettings = WebSocketObject().apply {
headers = mutableMapOf()
if (bean.host.isNotEmpty()) {
headers["Host"] = bean.host
}
path = bean.path.takeIf { it.isNotEmpty() } ?: "/"
if (bean.maxEarlyData > 0) {
maxEarlyData = bean.maxEarlyData
}
if (bean.earlyDataHeaderName.isNotEmpty()) {
earlyDataHeaderName = bean.earlyDataHeaderName
}
if (bean.wsUseBrowserForwarder) {
useBrowserForwarding = true
requireWs = true
}
}
}
"http" -> {
network = "http"
httpSettings = HttpObject().apply {
if (bean.host.isNotEmpty()) {
host = bean.host.listByLineOrComma()
}
if (bean.path.isNotEmpty()) {
path = bean.path
}
}
}
"quic" -> {
quicSettings = QuicObject().apply {
security = bean.quicSecurity
key = bean.quicKey
header = QuicObject.HeaderObject().apply {
type = bean.headerType
}
}
}
"grpc" -> {
grpcSettings = GrpcObject().apply {
serviceName = bean.grpcServiceName
if (DataStore.grpcServiceNameCompat || bean.grpcServiceNameCompat) {
serviceNameCompat = true
}
if (bean.grpcMultiMode) {
multiMode = true
}
}
}
"meek" -> {
meekSettings = MeekObject().apply {
if (bean.meekUrl.isNotEmpty()) {
url = bean.meekUrl
}
}
}
"httpupgrade" -> {
httpupgradeSettings = HTTPUpgradeObject().apply {
if (bean.host.isNotEmpty()) {
host = bean.host
}
if (bean.path.isNotEmpty()) {
path = bean.path
}
if (bean.maxEarlyData > 0) {
maxEarlyData = bean.maxEarlyData
}
if (bean.earlyDataHeaderName.isNotEmpty()) {
earlyDataHeaderName = bean.earlyDataHeaderName
}
}
}
"splithttp" -> {
splithttpSettings = SplitHTTPObject().apply {
if (bean.host.isNotEmpty()) {
host = bean.host
}
if (bean.path.isNotEmpty()) {
path = bean.path
}
if (bean.splithttpMode != "auto") {
mode = bean.splithttpMode
}
if (bean.splithttpExtra.isNotEmpty()) {
try {
parseJson(bean.splithttpExtra).asJsonObject?.also { extra ->
// fuck RPRX `extra`
extra.getInt("scMaxEachPostBytes", ignoreCase = true)?.also {
scMaxEachPostBytes = it.toString()
} ?: extra.getString("scMaxEachPostBytes", ignoreCase = true)?.also {
scMaxEachPostBytes = it
}
extra.getInt("scMinPostsIntervalMs", ignoreCase = true)?.also {
scMinPostsIntervalMs = it.toString()
} ?: extra.getString("scMinPostsIntervalMs", ignoreCase = true)?.also {
scMinPostsIntervalMs = it
}
extra.getInt("xPaddingBytes", ignoreCase = true)?.also {
xPaddingBytes = it.toString()
} ?: extra.getString("xPaddingBytes", ignoreCase = true)?.also {
xPaddingBytes = it
}
extra.getBoolean("noGRPCHeader", ignoreCase = true)?.also {
noGRPCHeader = it
}
extra.getObject("headers", ignoreCase = true)?.also {
headers = mutableMapOf()
for (key in it.keySet()) {
it.getString(key)?.also { value ->
headers[key] = value
}
}
}
extra.getObject("xmux", ignoreCase = true)?.also { xmuxSettings ->
xmux = SplitHTTPObject.XmuxObject().apply {
xmuxSettings.getInt("maxConcurrency", ignoreCase = true)?.also {
maxConcurrency = it.toString()
} ?: xmuxSettings.getString("maxConcurrency", ignoreCase = true)?.also {
maxConcurrency = it
}
xmuxSettings.getInt("maxConnections", ignoreCase = true)?.also {
maxConnections = it.toString()
} ?: xmuxSettings.getString("maxConnections", ignoreCase = true)?.also {
maxConnections = it
}
xmuxSettings.getInt("cMaxReuseTimes", ignoreCase = true)?.also {
cMaxReuseTimes = it.toString()
} ?: xmuxSettings.getString("cMaxReuseTimes", ignoreCase = true)?.also {
cMaxReuseTimes = it
}
xmuxSettings.getInt("hMaxRequestTimes", ignoreCase = true)?.also {
hMaxRequestTimes = it.toString()
} ?: xmuxSettings.getString("hMaxRequestTimes", ignoreCase = true)?.also {
hMaxRequestTimes = it
}
xmuxSettings.getInt("hMaxReusableSecs", ignoreCase = true)?.also {
hMaxReusableSecs = it.toString()
} ?: xmuxSettings.getString("hMaxReusableSecs", ignoreCase = true)?.also {
hMaxReusableSecs = it
}
}
}
extra.getBoolean("xPaddingObfsMode", ignoreCase = true)?.also {
xPaddingObfsMode = it
}
extra.getString("xPaddingKey", ignoreCase = true)?.also {
xPaddingKey = it
}
extra.getString("xPaddingHeader", ignoreCase = true)?.also {
xPaddingHeader = it
}
extra.getString("xPaddingPlacement", ignoreCase = true)?.also {
xPaddingPlacement = it
}
extra.getString("xPaddingMethod", ignoreCase = true)?.also {
xPaddingMethod = it
}
extra.getString("uplinkHTTPMethod", ignoreCase = true)?.also {
uplinkHTTPMethod = it
}
extra.getString("sessionIDPlacement", ignoreCase = true)?.also {
sessionIDPlacement = it
}
extra.getString("sessionIDKey", ignoreCase = true)?.also {
sessionIDKey = it
}
extra.getString("sessionIDTable", ignoreCase = true)?.also {
sessionIDTable = it
}
extra.getString("sessionIDLength", ignoreCase = true)?.also {
sessionIDLength = it
}
extra.getString("seqPlacement", ignoreCase = true)?.also {
seqPlacement = it
}
extra.getString("seqKey", ignoreCase = true)?.also {
seqKey = it
}
extra.getString("uplinkDataPlacement", ignoreCase = true)?.also {
uplinkDataPlacement = it
}
extra.getString("uplinkDataKey", ignoreCase = true)?.also {
uplinkDataKey = it
}
extra.getInt("uplinkChunkSize", ignoreCase = true)?.also {
uplinkChunkSize = it.toString()
} ?: extra.getString("uplinkChunkSize", ignoreCase = true)?.also {
uplinkChunkSize = it
}
}
} catch (e: Exception) {
error(e)
}
}
if (bean.shUseBrowserForwarder) {
useBrowserForwarding = true
requireSh = true
}
}
}
"hysteria2" -> {
hy2Settings = Hysteria2Object().apply {
// V2Ray transport is TCP only so it is safe to omit MaxDatagramFrameSize.
omitMaxDatagramFrameSize = true
if (bean.hy2Password.isNotEmpty()) {
password = bean.hy2Password
}
congestion = Hysteria2Object.CongestionObject().apply {
if (bean.hy2DownMbps > 0) {
down_mbps = bean.hy2DownMbps
}
if (bean.hy2UpMbps > 0) {
up_mbps = bean.hy2UpMbps
}
}
}
}
"mekya" -> {
mekyaSettings = MekyaObject().apply {
kcp = KcpObject().apply {
mtu = 1350
tti = 50
uplinkCapacity = 12
downlinkCapacity = 100
congestion = false
readBufferSize = 1
writeBufferSize = 1
header = KcpObject.HeaderObject().apply {
type = bean.mekyaKcpHeaderType
}
if (bean.mKcpSeed.isNotEmpty()) {
seed = bean.mekyaKcpSeed
}
}
if (bean.mekyaUrl.isNotEmpty()) {
url = bean.mekyaUrl
}
// magic values from https://github.com/v2fly/v2ray-core/pull/3120
maxWriteDelay = 80
maxRequestSize = 96000
pollingIntervalInitial = 200
h2PoolSize = 8
}
}
}
if (DataStore.enableFragment
&& (network != "kcp" && network != "quic" && network != "hysteria2")
&& (security == "tls" || security == "reality")
&& !(bean is ShadowsocksBean && bean.plugin.isNotEmpty() && PluginConfiguration(bean.plugin).selected.isNotEmpty())
&& !(network == "ws" && bean.wsUseBrowserForwarder)
&& !(network == "splithttp" && bean.shUseBrowserForwarder)
) {
sockopt = StreamSettingsObject.SockoptObject().apply {
tlsFragmentation = StreamSettingsObject.SockoptObject.TLSFragmentationObject().apply {
when (DataStore.fragmentMethod) {
TLS_FRAGMENTATION_METHOD.TLS_RECORD_FRAGMENTATION -> {
tlsRecordFragmentation = true
}
TLS_FRAGMENTATION_METHOD.TCP_SEGMENTATION -> {
tcpSegmentation = true
}
TLS_FRAGMENTATION_METHOD.TLS_RECORD_FRAGMENTATION_AND_TCP_SEGMENTATION -> {
tlsRecordFragmentation = true
tcpSegmentation = true
}
}
}
}
}
}
} else if (bean is ShadowsocksRBean) {
protocol = "shadowsocks"
settings = LazyOutboundConfigurationObject(this,
ShadowsocksOutboundConfigurationObject().apply {
servers = listOf(ShadowsocksOutboundConfigurationObject.ServerObject().apply {
address = bean.serverAddress
port = bean.serverPort
method = bean.method
password = bean.password
}
)
plugin = "shadowsocksr"
pluginArgs = listOf(
"--obfs=${bean.obfs}",
"--obfs-param=${bean.obfsParam}",
"--protocol=${bean.protocol}",
"--protocol-param=${bean.protocolParam}"
)
}
)
} else if (bean is WireGuardBean) {
protocol = "wireguard"
settings = LazyOutboundConfigurationObject(this,
WireGuardOutboundConfigurationObject().apply {
address = bean.localAddress.listByLineOrComma()
secretKey = bean.privateKey
mtu = bean.mtu
val values = bean.reserved.listByLineOrComma()
if (values.size == 3) {
val reserved0 = values[0].toUByteOrNull()
val reserved1 = values[1].toUByteOrNull()
val reserved2 = values[2].toUByteOrNull()
if (reserved0 != null && reserved1 != null && reserved2 != null) {
reserved = listOf(reserved0.toInt(), reserved1.toInt(), reserved2.toInt())
}
} else {
val array = Base64.decode(bean.reserved)
if (array.size == 3) {
reserved = listOf(array[0].toUByte().toInt(), array[1].toUByte().toInt(), array[2].toUByte().toInt())
}
}
peers = listOf(WireGuardOutboundConfigurationObject.WireGuardPeerObject().apply {
publicKey = bean.peerPublicKey
if (bean.peerPreSharedKey.isNotEmpty()) {
preSharedKey = bean.peerPreSharedKey
}
if (bean.keepaliveInterval > 0) {
keepAlive = bean.keepaliveInterval
}
endpoint = joinHostPort(bean.serverAddress, bean.serverPort)
})
})
if (currentDomainStrategy == "AsIs") {
currentDomainStrategy = "UseIP"
}
} else if (bean is SSHBean) {
protocol = "ssh"
settings = LazyOutboundConfigurationObject(this,
SSHOutboundConfigurationObject().apply {
address = bean.serverAddress
port = bean.serverPort
user = bean.username
when (bean.authType) {
SSHBean.AUTH_TYPE_PUBLIC_KEY -> {
privateKey = bean.privateKey
if (bean.privateKeyPassphrase.isNotEmpty()) {
privateKeyPassphrase = bean.privateKeyPassphrase
}
}
SSHBean.AUTH_TYPE_PASSWORD -> {
password = bean.password
}
}
if (bean.publicKey.isNotEmpty()) {
publicKey = bean.publicKey
}
if (bean.keepaliveInterval > 0) {
keepaliveInterval = bean.keepaliveInterval
}
})
} else if (bean is Hysteria2Bean) {
protocol = "hysteria2"
settings = LazyOutboundConfigurationObject(this,
Hysteria2OutboundConfigurationObject().apply {
servers = listOf(Hysteria2OutboundConfigurationObject.ServerObject().apply {
address = bean.serverAddress
port = bean.serverPorts.toHysteriaPort()
})
}
)
streamSettings = StreamSettingsObject().apply {
network = "hysteria2"
security = "tls"
hy2Settings = Hysteria2Object().apply {
use_udp_extension = true
if (DataStore.hysteria2OmitMaxDatagramFrameSize || bean.omitMaxDatagramFrameSize) {
omitMaxDatagramFrameSize = true
}
if (bean.auth.isNotEmpty()) {
password = bean.auth
}
congestion = Hysteria2Object.CongestionObject().apply {
if (bean.downloadMbps > 0) {
down_mbps = bean.downloadMbps
}
if (bean.uploadMbps > 0) {
up_mbps = bean.uploadMbps
}
type = bean.congestionControl
if (bean.congestionControl == "bbr") {
bbrProfile = bean.bbrProfile
}
}
if (bean.obfsType.isNotEmpty()) {
obfs = Hysteria2Object.OBFSObject().apply {
type = bean.obfsType
password = bean.obfsPassword
if (bean.obfsType == "gecko") {
if (bean.geckoMinPacketSize > 0) {
minPacketSize = bean.geckoMinPacketSize
}
if (bean.geckoMaxPacketSize > 0) {
maxPacketSize = bean.geckoMaxPacketSize
}
}
}
}
if (bean.serverPorts.isNotEmpty() && bean.serverPorts.isValidHysteriaMultiPort()) {
hopPorts = bean.serverPorts
if (bean.hopInterval > 0) {
hopInterval = bean.hopInterval
} else if (bean.hopIntervalMin > 0 || bean.hopIntervalMax > 0) {
hopIntervalMin = bean.hopIntervalMin
hopIntervalMax = bean.hopIntervalMax
}
}
}
tlsSettings = TLSObject().apply {
if (bean.sni.isNotEmpty()) {
serverName = bean.sni
}
if (bean.allowInsecure) {
allowInsecure = true
}
if (bean.pinnedPeerCertificateSha256.isNotEmpty()) {
pinnedPeerCertificateSha256 = mutableListOf()
bean.pinnedPeerCertificateSha256.listByLineOrComma().forEach {
pinnedPeerCertificateSha256.add(it.replace(":", "").replace("-", ""))
}
}
if (bean.pinnedPeerCertificatePublicKeySha256.isNotEmpty()) {
pinnedPeerCertificatePublicKeySha256 = bean.pinnedPeerCertificatePublicKeySha256.listByLineOrComma()
}
if (bean.pinnedPeerCertificateChainSha256.isNotEmpty()) {
pinnedPeerCertificateChainSha256 = bean.pinnedPeerCertificateChainSha256.listByLineOrComma()
}
if (bean.certificates.isNotEmpty() || bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates = mutableListOf()
if (bean.certificates.isNotEmpty()) {
disableSystemRoot = true
certificates.add(TLSObject.CertificateObject().apply {
usage = "verify"
certificate = bean.certificates.lines()
})
}
if (bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates.add(TLSObject.CertificateObject().apply {
usage = "encipherment"
certificate = bean.mtlsCertificate.lines()
key = bean.mtlsCertificatePrivateKey.lines()
})
}
}
if (bean.echEnabled) {
ech = TLSObject.ECHObject().apply {
enabled = bean.echEnabled
if (bean.echConfig.isNotEmpty()) {
config = bean.echConfig
}
}
}
if (bean.serverNameToVerify.isNotEmpty()) {
serverNameToVerify = bean.serverNameToVerify.listByLineOrComma()
}
}
}
} else if (bean is Tuic5Bean) {
protocol = "tuic"
settings = LazyOutboundConfigurationObject(this,
V2RayConfig.TUICOutboundConfigurationObject().apply {
address = bean.serverAddress
port = bean.serverPort
uuid = bean.uuid
password = bean.password
congestionControl = bean.congestionControl
udpRelayMode = bean.udpRelayMode
if (bean.zeroRTTHandshake) zeroRTTHandshake = bean.zeroRTTHandshake
if (bean.singUDPOverStream && DataStore.experimentalFlagsProperties.getBooleanProperty("singuot")) {
udpOverStream = bean.singUDPOverStream
}
}
)
streamSettings = StreamSettingsObject().apply {
security = "tls"
tlsSettings = TLSObject().apply {
if (bean.sni.isNotEmpty()) {
serverName = bean.sni
}
if (bean.alpn.isNotEmpty()) {
alpn = bean.alpn.listByLineOrComma()
}
if (bean.allowInsecure) {
allowInsecure = true
}
if (bean.certificates.isNotEmpty() || bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates = mutableListOf()
if (bean.certificates.isNotEmpty()) {
disableSystemRoot = true
certificates.add(TLSObject.CertificateObject().apply {
usage = "verify"
certificate = bean.certificates.lines()
})
}
if (bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates.add(TLSObject.CertificateObject().apply {
usage = "encipherment"
certificate = bean.mtlsCertificate.lines()
key = bean.mtlsCertificatePrivateKey.lines()
})
}
}
if (bean.pinnedPeerCertificateSha256.isNotEmpty()) {
pinnedPeerCertificateSha256 = mutableListOf()
bean.pinnedPeerCertificateSha256.listByLineOrComma().forEach {
pinnedPeerCertificateSha256.add(it.replace(":", ""))
}
}
if (bean.pinnedPeerCertificatePublicKeySha256.isNotEmpty()) {
pinnedPeerCertificatePublicKeySha256 = bean.pinnedPeerCertificatePublicKeySha256.listByLineOrComma()
}
if (bean.pinnedPeerCertificateChainSha256.isNotEmpty()) {
pinnedPeerCertificateChainSha256 = bean.pinnedPeerCertificateChainSha256.listByLineOrComma()
}
if (bean.echEnabled) {
ech = TLSObject.ECHObject().apply {
enabled = bean.echEnabled
if (bean.echConfig.isNotEmpty()) {
config = bean.echConfig
}
}
}
if (bean.serverNameToVerify.isNotEmpty()) {
serverNameToVerify = bean.serverNameToVerify.listByLineOrComma()
}
if (bean.disableSNI) {
serverName = "127.0.0.1" // Golang syntax suger
if (allowInsecure != true && serverNameToVerify.isNullOrEmpty()) {
serverNameToVerify = listOf(bean.sni.ifEmpty { bean.serverAddress })
}
}
}
}
} else if (bean is Http3Bean) {
protocol = "http3"
settings = LazyOutboundConfigurationObject(this,
V2RayConfig.HTTP3OutboundConfigurationObject().apply {
address = bean.serverAddress
port = bean.serverPort
if (bean.username.isNotEmpty()) username = bean.username
if (bean.password.isNotEmpty()) password = bean.password
}
)
streamSettings = StreamSettingsObject().apply {
security = "tls"
tlsSettings = TLSObject().apply {
if (bean.sni.isNotEmpty()) {
serverName = bean.sni
}
if (bean.certificates.isNotEmpty() || bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates = mutableListOf()
if (bean.certificates.isNotEmpty()) {
disableSystemRoot = true
certificates.add(TLSObject.CertificateObject().apply {
usage = "verify"
certificate = bean.certificates.lines()
})
}
if (bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates.add(TLSObject.CertificateObject().apply {
usage = "encipherment"
certificate = bean.mtlsCertificate.lines()
key = bean.mtlsCertificatePrivateKey.lines()
})
}
}
if (bean.pinnedPeerCertificateSha256.isNotEmpty()) {
pinnedPeerCertificateSha256 = mutableListOf()
bean.pinnedPeerCertificateSha256.listByLineOrComma().forEach {
pinnedPeerCertificateSha256.add(it.replace(":", ""))
}
}
if (bean.pinnedPeerCertificatePublicKeySha256.isNotEmpty()) {
pinnedPeerCertificatePublicKeySha256 = bean.pinnedPeerCertificatePublicKeySha256.listByLineOrComma()
}
if (bean.pinnedPeerCertificateChainSha256.isNotEmpty()) {
pinnedPeerCertificateChainSha256 = bean.pinnedPeerCertificateChainSha256.listByLineOrComma()
}
if (bean.allowInsecure) {
allowInsecure = true
}
if (bean.echEnabled) {
ech = TLSObject.ECHObject().apply {
enabled = bean.echEnabled
if (bean.echConfig.isNotEmpty()) {
config = bean.echConfig
}
}
}
if (bean.serverNameToVerify.isNotEmpty()) {
serverNameToVerify = bean.serverNameToVerify.listByLineOrComma()
}
}
}
} else if (bean is AnyTLSBean) {
protocol = "anytls"
settings = LazyOutboundConfigurationObject(this,
V2RayConfig.AnyTLSOutboundConfigurationObject().apply {
address = bean.serverAddress
port = bean.serverPort
if (bean.password.isNotEmpty()) password = bean.password
idleSessionCheckInterval = bean.idleSessionCheckInterval
idleSessionTimeout = bean.idleSessionTimeout
minIdleSession = bean.minIdleSession
if (bean.disableReuse) disableReuse = bean.disableReuse
}
)
streamSettings = StreamSettingsObject().apply {
if (bean.security.isNotEmpty()) {
security = bean.security
}
when (security) {
"tls" -> {
tlsSettings = TLSObject().apply {
if (bean.sni.isNotEmpty()) {
serverName = bean.sni
}
if (bean.alpn.isNotEmpty()) {
alpn = bean.alpn.listByLineOrComma()
}
if (bean.certificates.isNotEmpty() || bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates = mutableListOf()
if (bean.certificates.isNotEmpty()) {
disableSystemRoot = true
certificates.add(TLSObject.CertificateObject().apply {
usage = "verify"
certificate = bean.certificates.lines()
})
}
if (bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates.add(TLSObject.CertificateObject().apply {
usage = "encipherment"
certificate = bean.mtlsCertificate.lines()
key = bean.mtlsCertificatePrivateKey.lines()
})
}
}
if (bean.pinnedPeerCertificateSha256.isNotEmpty()) {
pinnedPeerCertificateSha256 = mutableListOf()
bean.pinnedPeerCertificateSha256.listByLineOrComma().forEach {
pinnedPeerCertificateSha256.add(it.replace(":", ""))
}
}
if (bean.pinnedPeerCertificatePublicKeySha256.isNotEmpty()) {
pinnedPeerCertificatePublicKeySha256 = bean.pinnedPeerCertificatePublicKeySha256.listByLineOrComma()
}
if (bean.pinnedPeerCertificateChainSha256.isNotEmpty()) {
pinnedPeerCertificateChainSha256 = bean.pinnedPeerCertificateChainSha256.listByLineOrComma()
}
if (bean.allowInsecure) {
allowInsecure = true
}
val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForTLS")
if (!overrideFingerprint.isNullOrEmpty()) {
fingerprint = overrideFingerprint
} else if (bean.utlsFingerprint.isNotEmpty()) {
fingerprint = bean.utlsFingerprint
}
if (bean.echEnabled) {
ech = TLSObject.ECHObject().apply {
enabled = bean.echEnabled
if (bean.echConfig.isNotEmpty()) {
config = bean.echConfig
}
}
}
if (bean.serverNameToVerify.isNotEmpty()) {
serverNameToVerify = bean.serverNameToVerify.listByLineOrComma()
}
}
}
"reality" -> {
realitySettings = RealityObject().apply {
if (bean.sni.isNotEmpty()) {
serverName = bean.sni
}
if (bean.realityPublicKey.isNotEmpty()) {
publicKey = bean.realityPublicKey
}
if (bean.realityShortId.isNotEmpty()) {
shortId = bean.realityShortId
}
val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForREALITY")
if (!overrideFingerprint.isNullOrEmpty()) {
fingerprint = overrideFingerprint
} else if (bean.realityFingerprint.isNotEmpty()) {
fingerprint = bean.realityFingerprint
}
if (DataStore.realityDisableX25519Mlkem768 || bean.realityDisableX25519Mlkem768 && !forExport) {
disableX25519MLKEM768 = true
}
}
}
}
if (DataStore.enableFragment) {
sockopt = StreamSettingsObject.SockoptObject().apply {
tlsFragmentation = StreamSettingsObject.SockoptObject.TLSFragmentationObject().apply {
when (DataStore.fragmentMethod) {
TLS_FRAGMENTATION_METHOD.TLS_RECORD_FRAGMENTATION -> {
tlsRecordFragmentation = true
}
TLS_FRAGMENTATION_METHOD.TCP_SEGMENTATION -> {
tcpSegmentation = true
}
TLS_FRAGMENTATION_METHOD.TLS_RECORD_FRAGMENTATION_AND_TCP_SEGMENTATION -> {
tlsRecordFragmentation = true
tcpSegmentation = true
}
}
}
}
}
}
} else if (bean is JuicityBean) {
protocol = "juicity"
settings = LazyOutboundConfigurationObject(this, V2RayConfig.JuicityOutboundConfigurationObject().apply {
address = bean.serverAddress
port = bean.serverPort
uuid = bean.uuid
password = bean.password
})
streamSettings = StreamSettingsObject().apply {
security = "tls"
tlsSettings = TLSObject().apply {
if (bean.sni.isNotEmpty()) {
serverName = bean.sni
}
if (bean.allowInsecure) {
allowInsecure = true
}
if (bean.pinnedPeerCertificateSha256.isNotEmpty()) {
pinnedPeerCertificateSha256 = mutableListOf()
bean.pinnedPeerCertificateSha256.listByLineOrComma().forEach {
pinnedPeerCertificateSha256.add(it.replace(":", ""))
}
}
if (bean.pinnedPeerCertificatePublicKeySha256.isNotEmpty()) {
pinnedPeerCertificatePublicKeySha256 = bean.pinnedPeerCertificatePublicKeySha256.listByLineOrComma()
}
if (bean.pinnedPeerCertificateChainSha256.isNotEmpty()) {
pinnedPeerCertificateChainSha256 = mutableListOf()
bean.pinnedPeerCertificateChainSha256.listByLineOrComma().forEach {
pinnedPeerCertificateChainSha256.add(
when {
it.length == 64 -> {
Base64.encode(bean.pinnedPeerCertificateChainSha256.chunked(2).map { it.toInt(16).toByte() }.toByteArray())
}
else -> {
bean.pinnedPeerCertificateChainSha256.replace('_', '/').replace('-', '+')
}
}
)
}
}
if (bean.certificates.isNotEmpty() || bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates = mutableListOf()
if (bean.certificates.isNotEmpty()) {
disableSystemRoot = true
certificates.add(
TLSObject.CertificateObject().apply {
usage = "verify"
certificate = bean.certificates.lines()
})
}
if (bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates.add(TLSObject.CertificateObject().apply {
usage = "encipherment"
certificate = bean.mtlsCertificate.lines()
key = bean.mtlsCertificatePrivateKey.lines()
})
}
}
if (bean.echEnabled) {
ech = TLSObject.ECHObject().apply {
enabled = bean.echEnabled
if (bean.echConfig.isNotEmpty()) {
config = bean.echConfig
}
}
}
if (bean.serverNameToVerify.isNotEmpty()) {
serverNameToVerify = bean.serverNameToVerify.listByLineOrComma()
}
}
}
} else if (bean is SnellBean) {
protocol = "snell"
settings = LazyOutboundConfigurationObject(this, V2RayConfig.SnellOutboundConfigurationObject().apply {
address = bean.serverAddress
port = bean.serverPort
psk = bean.psk
version = bean.version
reuse = bean.reuse
if (version == SnellBean.VERSION_4) {
obfsMode = bean.obfsMode
if (bean.obfsMode != SnellBean.OBFS_NONE && bean.obfsHost.isNotEmpty()) {
obfsHost = bean.obfsHost
}
}
if (version == SnellBean.VERSION_6) {
mode = bean.mode
}
if (DataStore.experimentalFlagsProperties.getBooleanProperty("singSnellUserKey") && bean.userKey.isNotEmpty()) {
userKey = bean.userKey
}
})
} else if (bean is MieruBean) {
protocol = "mieru"
settings = LazyOutboundConfigurationObject(this,
V2RayConfig.MieruOutboundConfigurationObject().apply {
address = bean.serverAddress
if (bean.portRange.isNotEmpty()) {
portRange = bean.portRange.listByLineOrComma()
} else {
port = bean.serverPort
}
username = bean.username
password = bean.password
when (bean.protocol) {
MieruBean.PROTOCOL_TCP -> protocol = "tcp"
MieruBean.PROTOCOL_UDP -> protocol = "udp"
}
when (bean.multiplexingLevel) {
MieruBean.MULTIPLEXING_DEFAULT -> multiplexing = "default"
MieruBean.MULTIPLEXING_OFF -> multiplexing = "off"
MieruBean.MULTIPLEXING_LOW -> multiplexing = "low"
MieruBean.MULTIPLEXING_MIDDLE -> multiplexing = "middle"
MieruBean.MULTIPLEXING_HIGH -> multiplexing = "high"
}
when (bean.handshakeMode) {
MieruBean.HANDSHAKE_DEFAULT -> handshakeMode = "default"
MieruBean.HANDSHAKE_STANDARD -> handshakeMode = "standard"
MieruBean.HANDSHAKE_NO_WAIT -> handshakeMode = "nowait"
}
if (bean.trafficPattern.isNotEmpty()) {
trafficPattern = bean.trafficPattern
}
}
)
} else if (bean is TrustTunnelBean) {
protocol = "trusttunnel"
settings = LazyOutboundConfigurationObject(this,
V2RayConfig.TrustTunnelOutboundConfigurationObject().apply {
address = bean.serverAddress
port = bean.serverPort
if (bean.username.isNotEmpty()) username = bean.username
if (bean.password.isNotEmpty()) password = bean.password
when (bean.protocol) {
"https" -> {}
"quic" -> http3 = true
else -> error("invalid")
}
}
)
streamSettings = StreamSettingsObject().apply {
security = "tls"
tlsSettings = TLSObject().apply {
if (bean.sni.isNotEmpty()) {
serverName = bean.sni
}
if (bean.certificate.isNotEmpty() || bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates = mutableListOf()
if (bean.certificate.isNotEmpty()) {
disableSystemRoot = true
certificates.add(TLSObject.CertificateObject().apply {
usage = "verify"
certificate = bean.certificate.lines()
})
}
if (bean.mtlsCertificate.isNotEmpty() || bean.mtlsCertificatePrivateKey.isNotEmpty()) {
certificates.add(TLSObject.CertificateObject().apply {
usage = "encipherment"
certificate = bean.mtlsCertificate.lines()
key = bean.mtlsCertificatePrivateKey.lines()
})
}
}
if (bean.pinnedPeerCertificateSha256.isNotEmpty()) {
pinnedPeerCertificateSha256 = mutableListOf()
bean.pinnedPeerCertificateSha256.listByLineOrComma().forEach {
pinnedPeerCertificateSha256.add(it.replace(":", ""))
}
}
if (bean.pinnedPeerCertificatePublicKeySha256.isNotEmpty()) {
pinnedPeerCertificatePublicKeySha256 = bean.pinnedPeerCertificatePublicKeySha256.listByLineOrComma()
}
if (bean.pinnedPeerCertificateChainSha256.isNotEmpty()) {
pinnedPeerCertificateChainSha256 = bean.pinnedPeerCertificateChainSha256.listByLineOrComma()
}
if (bean.allowInsecure) {
allowInsecure = true
}
if (protocol == "https") {
val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForTLS")
if (!overrideFingerprint.isNullOrEmpty()) {
fingerprint = overrideFingerprint
} else if (bean.utlsFingerprint.isNotEmpty()) {
fingerprint = bean.utlsFingerprint
}
}
if (bean.echEnabled) {
ech = TLSObject.ECHObject().apply {
enabled = bean.echEnabled
if (bean.echConfig.isNotEmpty()) {
config = bean.echConfig
}
}
}
if (bean.serverNameToVerify.isNotEmpty()) {
serverNameToVerify = bean.serverNameToVerify.listByLineOrComma()
}
}
}
}
if (bean is StandardV2RayBean && bean.mux) {
mux = OutboundObject.MuxObject().apply {
enabled = true
concurrency = bean.muxConcurrency
when (bean.muxPacketEncoding) {
"packet" -> {
packetEncoding = "packet"
}
"xudp" -> {
packetEncoding = "xudp"
}
}
}
}
if ((bean is ShadowsocksBean || bean is TrojanBean || bean is VMessBean || bean is VLESSBean)
&& bean.singMux && DataStore.experimentalFlagsProperties.getBooleanProperty("singmux")) {
smux = OutboundObject.SmuxObject().apply {
enabled = bean.singMux
protocol = bean.singMuxProtocol
if (bean.singMuxMaxConnections > 0) {
maxConnections = bean.singMuxMaxConnections
}
if (bean.singMuxMinStreams > 0) {
minStreams = bean.singMuxMinStreams
}
if (bean.singMuxMaxStreams > 0) {
maxStreams = bean.singMuxMaxStreams
}
if (bean.singMuxPadding) {
padding = bean.singMuxPadding
}
}
}
}
}
if (currentDomainStrategy != "AsIs") {
currentOutbound.domainStrategy = currentDomainStrategy
}
if (!(currentOutbound.domainStrategy == null && DataStore.outboundDomainStrategyForServer == "AsIs")
&& !(currentOutbound.domainStrategy == "AsIs" && DataStore.outboundDomainStrategyForServer == "AsIs")
&& currentOutbound.domainStrategy != DataStore.outboundDomainStrategyForServer) {
currentOutbound.dialDomainStrategy = DataStore.outboundDomainStrategyForServer
}
if (bean is ConfigBean && bean.type == "v2ray_outbound") {
currentOutbound = gson.fromJson(bean.content, OutboundObject::class.java).apply { init() }
}
currentOutbound.tag = tagIn
}
if (!isBalancer && index > 0) {
if (!pastExternal) {
pastOutbound.proxySettings = OutboundObject.ProxySettingsObject().apply {
tag = tagIn
transportLayer = true
}
} else {
routing.rules.add(RoutingObject.RuleObject().apply {
type = "field"
inboundTag = listOf(pastInboundTag)
outboundTag = tagIn
})
}
}
if (proxyEntity.needExternal() && !isBalancer && index != profileList.lastIndex) {
val mappingPort = mkPort()
bean.finalAddress = LOCALHOST
bean.finalPort = mappingPort
bean.isChain = true
inbounds.add(InboundObject().apply {
listen = LOCALHOST
port = mappingPort
tag = "$tagOutbound-mapping-${proxyEntity.id}"
protocol = "dokodemo-door"
settings = LazyInboundConfigurationObject(this,
DokodemoDoorInboundConfigurationObject().apply {
address = bean.serverAddress
network = bean.network()
port = bean.serverPort
})
pastInboundTag = tag
})
} else if (bean.canMapping() && proxyEntity.needExternal()) {
val mappingPort = mkPort()
bean.finalAddress = LOCALHOST
bean.finalPort = mappingPort
inbounds.add(InboundObject().apply {
listen = LOCALHOST
port = mappingPort
tag = "$tagOutbound-mapping-${proxyEntity.id}"
protocol = "dokodemo-door"
settings = LazyInboundConfigurationObject(this,
DokodemoDoorInboundConfigurationObject().apply {
address = bean.serverAddress
network = bean.network()
port = bean.serverPort
})
routing.rules.add(RoutingObject.RuleObject().apply {
type = "field"
inboundTag = listOf(tag)
outboundTag = TAG_DIRECT
})
})
hasTagDirect = true
}
if (!needGlobal) {
outbounds.add(currentOutbound)
chainOutbounds.add(currentOutbound)
pastExternal = proxyEntity.needExternal()
pastOutbound = currentOutbound
}
}
if (isBalancer) {
val balancerBean = balancer()!!
// Check if we need to apply landing proxy for TYPE_GROUP balancer
val shouldUseLandingProxy = balancerBean.type == BalancerBean.TYPE_GROUP &&
balancerBean.useLandingProxy == true
if (shouldUseLandingProxy) {
// Get the group and landing proxy
val group = SagerDatabase.groupDao.getById(balancerBean.groupId)
val landingProxyEntity = if (group != null && group.landingProxy > 0L) {
SagerDatabase.proxyDao.getById(group.landingProxy)
} else null
if (landingProxyEntity != null) {
// Validate landing proxy
when (landingProxyEntity.type) {
ProxyEntity.TYPE_BALANCER -> error("balancer can not be the landing proxy")
ProxyEntity.TYPE_CONFIG -> if (landingProxyEntity.configBean!!.type == "v2ray")
error("custom config can not be the landing proxy")
}
if (!landingProxyEntity.requireBean().canMapping()) {
error("${landingProxyEntity.displayName()} can be the front proxy only and can not be the landing proxy")
}
// Get landing proxy chain
val landingProxyList = when (landingProxyEntity.type) {
ProxyEntity.TYPE_CHAIN -> landingProxyEntity.resolveChainRecursively()
else -> mutableListOf(landingProxyEntity)
}
// For each proxy in profileList, we need to create a chain with landing proxy
// We'll rebuild chainOutbounds with chains
val originalProxies = profileList.toList()
chainOutbounds.clear()
for (mainProxy in originalProxies) {
// Create a chain: landingProxyList + mainProxy
// Landing proxy goes first to bypass whitelists, then mainProxy
val chainList = landingProxyList.toMutableList()
chainList.add(mainProxy)
// Build this chain as a sub-chain
val chainTag = buildChain(
"$tagOutbound-chain-${mainProxy.id}",
chainList,
false, // not a balancer
{ null }
)
// Find the first outbound of this chain and add it to chainOutbounds
val chainFirstOutbound = outbounds.findLast { it.tag == chainTag }
if (chainFirstOutbound != null) {
chainOutbounds.add(chainFirstOutbound)
}
}
}
}
// Check if we need to apply front proxy for TYPE_GROUP balancer
val shouldUseFrontProxy = balancerBean.type == BalancerBean.TYPE_GROUP &&
balancerBean.useFrontProxy == true
if (shouldUseFrontProxy) {
// Get the group and front proxy
val group = SagerDatabase.groupDao.getById(balancerBean.groupId)
val frontProxyEntity = if (group != null && group.frontProxy > 0L) {
SagerDatabase.proxyDao.getById(group.frontProxy)
} else null
if (frontProxyEntity != null) {
// Validate front proxy
when (frontProxyEntity.type) {
ProxyEntity.TYPE_BALANCER -> error("balancer can not be the front proxy")
ProxyEntity.TYPE_CONFIG -> if (frontProxyEntity.configBean!!.type == "v2ray")
error("custom config can not be the front proxy")
}
// Get front proxy chain
val frontProxyList = when (frontProxyEntity.type) {
ProxyEntity.TYPE_CHAIN -> frontProxyEntity.resolveChainRecursively()
else -> mutableListOf(frontProxyEntity)
}
// For each proxy in profileList, we need to create a chain with front proxy
// We'll rebuild chainOutbounds with chains
val originalProxies = profileList.toList()
chainOutbounds.clear()
for (mainProxy in originalProxies) {
// Create a chain: mainProxy + frontProxyList
// Main proxy goes first, then front proxy
val chainList = mutableListOf(mainProxy)
chainList.addAll(frontProxyList)
// Build this chain as a sub-chain
val chainTag = buildChain(
"$tagOutbound-chain-${mainProxy.id}",
chainList,
false, // not a balancer
{ null }
)
// Find the first outbound of this chain and add it to chainOutbounds
val chainFirstOutbound = outbounds.findLast { it.tag == chainTag }
if (chainFirstOutbound != null) {
chainOutbounds.add(chainFirstOutbound)
}
}
}
}
val observatory = ObservatoryObject().apply {
probeURL = balancerBean.probeUrl.ifEmpty {
DataStore.connectionTestURL
}
if (balancerBean.probeInterval > 0) {
probeInterval = "${balancerBean.probeInterval}s"
}
enableConcurrency = true
subjectSelector = HashSet(chainOutbounds.map { it.tag })
}
val observatoryItem = MultiObservatoryObject.MultiObservatoryItem().apply {
tag = "observer-$tagOutbound"
settings = mutableMapOf()
settings["probeURL"] = observatory.probeURL
settings["probeInterval"] = observatory.probeInterval
settings["enableConcurrency"] = observatory.enableConcurrency
settings["subjectSelector"] = observatory.subjectSelector
}
if (multiObservatory == null) multiObservatory = MultiObservatoryObject().apply {
observers = mutableListOf()
}
multiObservatory.observers.add(observatoryItem)
if (routing.balancers == null) routing.balancers = ArrayList()
routing.balancers.add(RoutingObject.BalancerObject().apply {
tag = "balancer-$tagOutbound"
selector = chainOutbounds.map { it.tag }
if (multiObservatory == null) {
multiObservatory = MultiObservatoryObject().apply {
observers = mutableListOf()
}
}
strategy = StrategyObject().apply {
type = balancerBean.strategy.takeIf { it.isNotEmpty() } ?: "random"
when (type) {
"leastPing", "leastLoad" -> {
settings = StrategyObject.strategyConfig().apply {
observerTag = "observer-$tagOutbound"
}
}
else -> {
settings = StrategyObject.strategyConfig().apply {
observerTag = "observer-$tagOutbound"
aliveOnly = true
}
}
}
}
})
if (tagOutbound == TAG_AGENT) {
if (observatoryItem.settings["probeURL"] == DataStore.connectionTestURL) {
rootObserver = observatoryItem
}
// if all outbounds of a balancer are dead, the first (default) outbound will be used
rootBalancer = RoutingObject.RuleObject().apply {
type = "field"
network = "tcp,udp"
balancerTag = "balancer-$tagOutbound"
}
}
}
return chainOutbound
}
val mainIsBalancer = proxy.balancerBean != null
val tagProxy = buildChain(
TAG_AGENT, proxies, mainIsBalancer
) { proxy.balancerBean }
val balancerMap = mutableMapOf()
val tagMap = mutableMapOf()
extraProxies.forEach { (key, entities) ->
val (id, balancer) = key
val (isBalancer, balancerBean) = balancer
tagMap[id] = buildChain("$TAG_AGENT-$id", entities, isBalancer, balancerBean::value)
if (isBalancer) {
balancerMap[id] = "balancer-$TAG_AGENT-$id"
}
}
val isVpn = DataStore.serviceMode == Key.MODE_VPN
for (rule in extraRules) {
val uidList = mutableListOf()
if (rule.packages.isNotEmpty() || rule.customPackageNames.isNotEmpty()) {
if (!isVpn) {
alerts.add(Alerts.ROUTE_ALERT_NOT_VPN to rule.displayName())
continue
}
PackageCache.awaitLoadSync()
if (rule.customPackageNames.isNotEmpty()) {
rule.customPackageNames.forEach {
it.toIntOrNull()?.let {
uidList.add(it)
} ?: PackageCache[it]?.let {
uidList.add(it)
}
}
} else {
rule.packages.forEach {
PackageCache[it]?.let {
uidList.add(it)
}
}
}
if (uidList.isEmpty()) {
alerts.add(Alerts.ROUTE_ALERT_ALL_PACKAGES_UNINSTALLED to rule.displayName())
continue
}
}
routing.rules.add(RoutingObject.RuleObject().apply {
type = "field"
if (uidList.isNotEmpty()) {
uid = uidList
}
if (!forExport && !forTest && rule.ssid.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
val isLocationPermissionGranted = app.checkSelfPermission(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Manifest.permission.ACCESS_FINE_LOCATION
} else {
Manifest.permission.ACCESS_COARSE_LOCATION
}
) == PackageManager.PERMISSION_GRANTED
if (!isLocationPermissionGranted) {
throw Alerts.RouteAlertException(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Alerts.ROUTE_ALERT_NEED_FINE_LOCATION_ACCESS
} else {
Alerts.ROUTE_ALERT_NEED_COARSE_LOCATION_ACCESS
}, rule.displayName()
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && app.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) {
throw Alerts.RouteAlertException(
Alerts.ROUTE_ALERT_NEED_BACKGROUND_LOCATION_ACCESS, rule.displayName()
)
}
val isLocationServiceEnabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
SagerNet.location.isLocationEnabled
} else {
try {
@Suppress("DEPRECATION")
Settings.Secure.getInt(app.contentResolver, Settings.Secure.LOCATION_MODE) != Settings.Secure.LOCATION_MODE_OFF
} catch (e: Settings.SettingNotFoundException) {
e.printStackTrace()
false
}
}
if (!isLocationServiceEnabled) {
throw Alerts.RouteAlertException(
Alerts.ROUTE_ALERT_LOCATION_DISABLED, rule.displayName()
)
}
}
if (rule.domains.isNotEmpty()) {
domains = rule.domains.listByLineOrComma()
}
if (rule.ip.isNotEmpty()) {
ip = rule.ip.listByLineOrComma()
}
if (rule.port.isNotEmpty()) {
port = rule.port
}
if (rule.sourcePort.isNotEmpty()) {
sourcePort = rule.sourcePort
}
if (rule.network.isNotEmpty()) {
network = rule.network
}
if (rule.source.isNotEmpty()) {
source = rule.source.listByLineOrComma()
}
if (rule.protocol.isNotEmpty()) {
protocol = rule.protocol.listByLineOrComma()
}
if (rule.attrs.isNotEmpty()) {
attrs = rule.attrs
}
if (rule.ssid.isNotEmpty()) {
// a hack for SSID containing `\n`
ssid = rule.ssid.split("\n").map { it.unescapeLineFeed() }
}
if (rule.networkType.isNotEmpty()) {
networkType = rule.networkType.toMutableList()
}
when {
balancerMap.containsKey(rule.outbound) -> {
balancerTag = balancerMap[rule.outbound]
}
mainIsBalancer && rule.outbound == 0L -> balancerTag = "balancer-$TAG_AGENT"
else -> {
outboundTag = when (val outId = rule.outbound) {
0L -> tagProxy
-1L -> TAG_BYPASS
-2L -> TAG_BLOCK
else -> if (outId == proxy.id) tagProxy else {
tagMap[outId] ?: error("outbound not found in rule ${rule.displayName()}")
}
}
}
}
})
}
if (requireWs) {
browserForwarder = BrowserForwarderObject().apply {
listenAddr = LOCALHOST
listenPort = mkPort()
}
}
if (requireSh) {
browserDialer = BrowserDialerObject().apply {
listenAddr = LOCALHOST
listenPort = mkPort()
}
}
if (hasTagDirect) {
outbounds.add(OutboundObject().apply {
tag = TAG_DIRECT
protocol = "freedom"
if (!forExport && DataStore.interruptReusedConnections && directNeedsInterruption) {
settings = LazyOutboundConfigurationObject(this,
FreedomOutboundConfigurationObject().apply {
interruptConnections = true
}
)
}
})
}
outbounds.add(OutboundObject().apply {
tag = TAG_BYPASS
protocol = "freedom"
if (DataStore.enableFragment && DataStore.enableFragmentForDirect) {
streamSettings = StreamSettingsObject().apply {
sockopt = StreamSettingsObject.SockoptObject().apply {
tlsFragmentation = StreamSettingsObject.SockoptObject.TLSFragmentationObject().apply {
when (DataStore.fragmentMethod) {
TLS_FRAGMENTATION_METHOD.TLS_RECORD_FRAGMENTATION -> {
tlsRecordFragmentation = true
}
TLS_FRAGMENTATION_METHOD.TCP_SEGMENTATION -> {
tcpSegmentation = true
}
TLS_FRAGMENTATION_METHOD.TLS_RECORD_FRAGMENTATION_AND_TCP_SEGMENTATION -> {
tlsRecordFragmentation = true
tcpSegmentation = true
}
}
}
}
}
}
if (DataStore.outboundDomainStrategyForDirect != "AsIs") {
settings = LazyOutboundConfigurationObject(this,
FreedomOutboundConfigurationObject().apply {
domainStrategy = DataStore.outboundDomainStrategyForDirect
}
)
}
})
outbounds.add(OutboundObject().apply {
tag = TAG_BLOCK
protocol = "blackhole"
})
if (!forTest && !forExport) {
inbounds.add(InboundObject().apply {
tag = TAG_DNS_IN
val path = SagerNet.deviceStorage.noBackupFilesDir.toString() + "/ipc_dns.sock"
val udsFile = File(path)
if (udsFile.exists()) udsFile.delete()
listen = path
protocol = "dokodemo-door"
settings = LazyInboundConfigurationObject(this,
DokodemoDoorInboundConfigurationObject().apply {
address = "/ipc_dns.sock" // placeholder, all queries are handled internally
network = "unix"
}
)
})
}
if (!forTest && DataStore.requireDnsInbound && DataStore.localDNSPort > 0) {
inbounds.add(InboundObject().apply {
tag = TAG_DNS_IN
listen = bind
port = DataStore.localDNSPort
protocol = "dokodemo-door"
settings = LazyInboundConfigurationObject(this,
DokodemoDoorInboundConfigurationObject().apply {
address = LOCALHOST // placeholder, all queries are handled internally
network = "tcp,udp"
port = 53 // placeholder, all queries are handled internally
}
)
if (shouldDumpUID) dumpUID = true
})
}
outbounds.add(OutboundObject().apply {
protocol = "dns"
tag = TAG_DNS_OUT
settings = LazyOutboundConfigurationObject(this,
DNSOutboundConfigurationObject().apply {
userLevel = 1
if (DataStore.experimentalFlagsProperties.getBooleanProperty("lookupAsExchange")) {
lookupAsExchange = true
}
})
})
val bypassDomain = HashSet()
val bypassDomainSkipFakeDns = HashSet()
val proxyDomain = HashSet()
val bootstrapDomain = HashSet()
(proxies + extraProxies.values.flatten()).forEach { it ->
val bean = it.requireBean()
bean.apply {
if (bean is ConfigBean && bean.type == "v2ray_outbound") {
// too dirty to read server addresses from a custom outbound config
// let users provide them manually
bean.serverAddresses.listByLineOrComma().forEach {
when {
it.isEmpty() -> {}
!Libexclavecore.isIP(it) -> {
bypassDomainSkipFakeDns.add("full:$it")
}
}
}
} else {
if (!Libexclavecore.isIP(serverAddress)) {
bypassDomainSkipFakeDns.add("full:$serverAddress")
}
when (bean) {
is StandardV2RayBean -> {
if (bean.echEnabled && bean.echConfig.isEmpty() && !Libexclavecore.isIP(bean.sni)) {
bypassDomainSkipFakeDns.add("full:${bean.sni}")
}
}
is AnyTLSBean -> {
if (bean.echEnabled && bean.echConfig.isEmpty() && !Libexclavecore.isIP(bean.sni)) {
bypassDomainSkipFakeDns.add("full:${bean.sni}")
}
}
is Http3Bean -> {
if (bean.echEnabled && bean.echConfig.isEmpty() && !Libexclavecore.isIP(bean.sni)) {
bypassDomainSkipFakeDns.add("full:${bean.sni}")
}
}
is Hysteria2Bean -> {
if (bean.echEnabled && bean.echConfig.isEmpty() && !Libexclavecore.isIP(bean.sni)) {
bypassDomainSkipFakeDns.add("full:${bean.sni}")
}
}
is JuicityBean -> {
if (bean.echEnabled && bean.echConfig.isEmpty() && !Libexclavecore.isIP(bean.sni)) {
bypassDomainSkipFakeDns.add("full:${bean.sni}")
}
}
is Tuic5Bean -> {
if (bean.echEnabled && bean.echConfig.isEmpty() && !Libexclavecore.isIP(bean.sni)) {
bypassDomainSkipFakeDns.add("full:${bean.sni}")
}
}
is TrustTunnelBean -> {
if (bean.echEnabled && bean.echConfig.isEmpty() && !Libexclavecore.isIP(bean.sni)) {
bypassDomainSkipFakeDns.add("full:${bean.sni}")
}
}
}
}
}
}
if (DataStore.enableDnsRouting) {
val directDNSDomainList = DataStore.experimentalFlagsProperties.getProperty("directDNSDomainList")
if (directDNSDomainList != null) {
if (!forTest && DataStore.routeMode == RouteMode.RULE) {
bypassDomain.addAll(directDNSDomainList.split(","))
}
} else {
for (bypassRule in extraRules.filter { it.isBypassRule() }) {
if (bypassRule.domains.isNotEmpty()) {
bypassDomain.addAll(bypassRule.domains.listByLineOrComma())
}
}
}
val remoteDNSDomainList = DataStore.experimentalFlagsProperties.getProperty("remoteDNSDomainList")
if (remoteDNSDomainList != null) {
if (!forTest && DataStore.routeMode == RouteMode.RULE) {
proxyDomain.addAll(remoteDNSDomainList.split(","))
}
} else {
for (proxyRule in extraRules.filter { it.isProxyRule() }) {
if (proxyRule.domains.isNotEmpty()) {
proxyDomain.addAll(proxyRule.domains.listByLineOrComma())
}
}
}
}
remoteDNS.forEach {
try {
if (it.lowercase() != "localhost" && it.lowercase() != "fakedns") {
if (it.contains("://")) {
val url = Libexclavecore.parseURL(it)
if (!Libexclavecore.isIP(url.host)) {
bypassDomainSkipFakeDns.add("full:${url.host}")
}
} else if (!Libexclavecore.isIP(it)) {
bypassDomainSkipFakeDns.add("full:$it")
}
}
} catch (_: Exception) {}
}
directDNS.forEach {
try {
if (it.lowercase() != "localhost" && it.lowercase() != "fakedns") {
if (it.contains("://")) {
val url = Libexclavecore.parseURL(it)
if (!Libexclavecore.isIP(url.host)) {
bootstrapDomain.add("full:${url.host}")
}
} else if (!Libexclavecore.isIP(it)) {
bootstrapDomain.add("full:$it")
}
}
} catch (_: Exception) {}
}
var hasDnsTagDirect = false
if (bypassDomain.isNotEmpty() || bypassDomainSkipFakeDns.isNotEmpty() || bootstrapDomain.isNotEmpty()) {
dns.servers.addAll(remoteDNS.map {
DnsObject.StringOrServerObject().apply {
valueY = DnsObject.ServerObject().apply {
address = it
domains = proxyDomain.toList() // v2fly/v2ray-core#1558, v2fly/v2ray-core#1855
queryStrategy = remoteDnsQueryStrategy
if (DataStore.ednsClientIp.isNotEmpty()) {
clientIp = DataStore.ednsClientIp
}
if (useFakeDns) {
fakedns = mutableListOf()
if (queryStrategy != "UseIPv6") {
fakedns.add(DnsObject.ServerObject.StringOrFakeDnsObject().apply {
valueY = FakeDnsObject().apply {
ipPool = "${VpnService.FAKEDNS_VLAN4_CLIENT}/${VpnService.FAKEDNS_VLAN4_CLIENT_PREFIX}"
poolSize = VpnService.FAKEDNS_VLAN4_CLIENT_POOL_SIZE
}
})
}
if (queryStrategy != "UseIPv4") {
fakedns.add(DnsObject.ServerObject.StringOrFakeDnsObject().apply {
valueY = FakeDnsObject().apply {
ipPool = "${VpnService.FAKEDNS_VLAN6_CLIENT}/${VpnService.FAKEDNS_VLAN6_CLIENT_PREFIX}"
poolSize = VpnService.FAKEDNS_VLAN6_CLIENT_POOL_SIZE
}
})
}
}
}
}
})
if (bootstrapDomain.isNotEmpty()) {
dns.servers.addAll(bootstrapDNS.map {
DnsObject.StringOrServerObject().apply {
valueY = DnsObject.ServerObject().apply {
address = it
domains = bootstrapDomain.toList() // v2fly/v2ray-core#1558, v2fly/v2ray-core#1855
queryStrategy = directDnsQueryStrategy
if (!it.lowercase().contains("+local://") && it.lowercase() != "localhost") {
tag = TAG_DNS_DIRECT
hasDnsTagDirect = true
}
fallbackStrategy = "disabled"
}
}
})
}
if (bypassDomainSkipFakeDns.isNotEmpty()) {
dns.servers.addAll(directDNS.map {
DnsObject.StringOrServerObject().apply {
valueY = DnsObject.ServerObject().apply {
address = it
// skip fake DNS for server addresses and DNS server addresses
domains = bypassDomainSkipFakeDns.toList()
queryStrategy = directDnsQueryStrategy
if (!it.lowercase().contains("+local://") && it.lowercase() != "localhost") {
tag = TAG_DNS_DIRECT
hasDnsTagDirect = true
}
fallbackStrategy = "disabled"
}
}
})
}
if (bypassDomain.isNotEmpty()) {
dns.servers.addAll(directDNS.map {
DnsObject.StringOrServerObject().apply {
valueY = DnsObject.ServerObject().apply {
address = it
//FIXME: This relies on the behavior of a bug.
domains = bypassDomain.toList() // v2fly/v2ray-core#1558, v2fly/v2ray-core#1855
queryStrategy = directDnsQueryStrategy
if (!it.contains("+local://") && it != "localhost") {
tag = TAG_DNS_DIRECT
hasDnsTagDirect = true
}
if (useFakeDns) {
fakedns = mutableListOf()
if (queryStrategy != "UseIPv6") {
fakedns.add(DnsObject.ServerObject.StringOrFakeDnsObject().apply {
valueY = FakeDnsObject().apply {
ipPool = "${VpnService.FAKEDNS_VLAN4_CLIENT}/${VpnService.FAKEDNS_VLAN4_CLIENT_PREFIX}"
poolSize = VpnService.FAKEDNS_VLAN4_CLIENT_POOL_SIZE
}
})
}
if (queryStrategy != "UseIPv4") {
fakedns.add(DnsObject.ServerObject.StringOrFakeDnsObject().apply {
valueY = FakeDnsObject().apply {
ipPool = "${VpnService.FAKEDNS_VLAN6_CLIENT}/${VpnService.FAKEDNS_VLAN6_CLIENT_PREFIX}"
poolSize = VpnService.FAKEDNS_VLAN6_CLIENT_POOL_SIZE
}
})
}
}
fallbackStrategy = "disabled"
}
}
})
}
} else {
dns.servers.addAll(remoteDNS.map {
DnsObject.StringOrServerObject().apply {
valueY = DnsObject.ServerObject().apply {
address = it
queryStrategy = remoteDnsQueryStrategy
if (DataStore.ednsClientIp.isNotEmpty()) {
clientIp = DataStore.ednsClientIp
}
if (useFakeDns) {
fakedns = mutableListOf()
if (queryStrategy != "UseIPv6") {
fakedns.add(DnsObject.ServerObject.StringOrFakeDnsObject().apply {
valueY = FakeDnsObject().apply {
ipPool = "${VpnService.FAKEDNS_VLAN4_CLIENT}/${VpnService.FAKEDNS_VLAN4_CLIENT_PREFIX}"
poolSize = VpnService.FAKEDNS_VLAN4_CLIENT_POOL_SIZE
}
})
}
if (queryStrategy != "UseIPv4") {
fakedns.add(DnsObject.ServerObject.StringOrFakeDnsObject().apply {
valueY = FakeDnsObject().apply {
ipPool = "${VpnService.FAKEDNS_VLAN6_CLIENT}/${VpnService.FAKEDNS_VLAN6_CLIENT_PREFIX}"
poolSize = VpnService.FAKEDNS_VLAN6_CLIENT_POOL_SIZE
}
})
}
}
}
}
})
}
if (routeMode == RouteMode.DIRECT) {
routing.rules.add(0, RoutingObject.RuleObject().apply {
type = "field"
port = "0-65535"
outboundTag = TAG_BYPASS
})
}
if (hasDnsTagDirect) {
routing.rules.add(0, RoutingObject.RuleObject().apply {
type = "field"
inboundTag = listOf(TAG_DNS_DIRECT)
outboundTag = TAG_BYPASS
})
}
if (!forTest && trafficSniffing && DataStore.hijackDns) {
routing.rules.add(0, RoutingObject.RuleObject().apply {
type = "field"
protocol = listOf("dns")
outboundTag = TAG_DNS_OUT
})
}
if (!forTest) {
routing.rules.add(0, RoutingObject.RuleObject().apply {
type = "field"
inboundTag = listOf(TAG_DNS_IN)
outboundTag = TAG_DNS_OUT
})
}
if (rootBalancer != null) routing.rules.add(rootBalancer)
if (DataStore.enableUnlockRu) {
val unlockDomains = listOf(
"openai.com", "chat.openai.com", "chatgpt.com",
"claude.ai", "anthropic.com",
"perplexity.ai",
"x.com", "twitter.com",
"google.com", "youtube.com", "youtu.be",
"github.com", "githubusercontent.com",
"discord.com", "discord.gg", "discordapp.com",
"reddit.com", "medium.com",
"netflix.com", "spotify.com",
"twitch.tv", "steam.com", "steampowered.com",
"instagram.com", "facebook.com", "whatsapp.com",
"telegram.org", "web.telegram.org",
"vimeo.com", "dailymotion.com",
"bloomberg.com", "nytimes.com", "washingtonpost.com",
"bbc.com", "bbc.co.uk",
"wikipedia.org", "wikimedia.org"
)
for (domain in unlockDomains) {
routing.rules.add(RoutingObject.RuleObject().apply {
type = "field"
domains = listOf("domain:$domain")
outboundTag = TAG_AGENT
})
}
}
if (DataStore.socksProxyChainEnabled && DataStore.socksProxyChainHost.isNotEmpty() && DataStore.socksProxyChainPort > 0) {
val chainTag = "socks-chain"
outbounds.add(V2RayConfig.OutboundObject().apply {
tag = chainTag
protocol = "socks"
settings = LazyOutboundConfigurationObject(this, SocksOutboundConfigurationObject().apply {
servers = listOf(SocksOutboundConfigurationObject.ServerObject().apply {
address = DataStore.socksProxyChainHost
port = DataStore.socksProxyChainPort
if (DataStore.socksProxyChainUsername.isNotEmpty()) {
users = listOf(SocksOutboundConfigurationObject.ServerObject.UserObject().apply {
user = DataStore.socksProxyChainUsername
pass = DataStore.socksProxyChainPassword
})
}
})
})
})
}
if (DataStore.directProxyMode) {
val directTag = "direct-proxy"
outbounds.add(V2RayConfig.OutboundObject().apply {
tag = directTag
protocol = "freedom"
})
routing.rules.add(0, RoutingObject.RuleObject().apply {
type = "field"
outboundTag = directTag
network = "tcp,udp"
})
}
if (trafficStatistics) stats = emptyMap()
@Suppress("UNCHECKED_CAST")
result = V2rayBuildResult(
gson.toJson(this),
indexMap,
requireWs,
if (requireWs) browserForwarder.listenPort else 0,
requireSh,
if (requireSh) browserDialer.listenPort else 0,
outboundTags,
outboundTagsCurrent,
outboundTagsAll,
TAG_BYPASS,
rootObserver?.tag ?: "",
rootObserver?.settings?.get("subjectSelector") as? Set ?: HashSet(),
shouldDumpUID,
alerts,
DataStore.enableFakeDns,
)
}
return result
}
fun buildCustomConfig(proxy: ProxyEntity, forTest: Boolean = false, forExport: Boolean = false): V2rayBuildResult {
val bean = proxy.configBean!!
val config = parseJson(bean.content, lenient = true).asJsonObject
// TODO: add fake DNS pool CIDR to TUN route address
var useFakeDns = false
runCatching {
config.getObject("fakedns", ignoreCase = true)?.also {
useFakeDns = true
}
}
runCatching {
config.getArray("fakedns", ignoreCase = true)?.takeIf { it.isNotEmpty() }?.also {
useFakeDns = true
}
}
config.getObject("dns", ignoreCase = true)?.also { dns ->
runCatching {
dns.getBoolean("fakedns", ignoreCase = true)?.takeIf { it }?.also {
useFakeDns = true
}
}
runCatching {
dns.getStringArray("fakedns", ignoreCase = true)?.also {
useFakeDns = true
}
}
runCatching {
dns.getArray("fakedns", ignoreCase = true)?.also {
useFakeDns = true
}
}
var servers: List? = null
try {
servers = dns.getArray("servers", ignoreCase = true)
} catch (_: Exception) {}
servers?.forEach { server ->
runCatching {
server.getBoolean("fakedns", ignoreCase = true)?.takeIf { it }?.also {
useFakeDns = true
return@forEach
}
}
runCatching {
server.getStringArray("fakedns", ignoreCase = true)?.also {
useFakeDns = true
return@forEach
}
}
runCatching {
server.getArray("fakedns", ignoreCase = true)?.also {
useFakeDns = true
return@forEach
}
}
}
}
var isConfigWithSniffing = false
var shouldDumpUID = false
config.getObject("routing", ignoreCase = true)?.also { routeObject ->
gson.fromJson(routeObject.toString(), RoutingObject::class.java)?.also { route ->
if (route.rules?.any { it.uid?.isNotEmpty() == true } == true) {
shouldDumpUID = true
}
if (route.rules?.any { it.protocol?.isNotEmpty() == true } == true) {
isConfigWithSniffing = true
}
}
}
val inbounds = config.getArray("inbounds")
?.map { gson.fromJson(it.toString(), InboundObject::class.java) }
?.toMutableList() ?: ArrayList()
if (!forTest && !forExport) {
inbounds.add(InboundObject().apply {
tag = "ipc-in"
protocol = "ipc"
val path = SagerNet.deviceStorage.noBackupFilesDir.toString() + "/ipc.sock"
val udsFile = File(path)
if (udsFile.exists()) udsFile.delete()
listen = path
if (DataStore.trafficSniffing || isConfigWithSniffing || useFakeDns) {
sniffing = InboundObject.SniffingObject().apply {
enabled = true
val protocols = mutableListOf().apply {
if (useFakeDns) add("fakedns")
if (DataStore.trafficSniffing) addAll(listOf("http", "tls", "quic"))
}
if (protocols.isNotEmpty()) {
destOverride = protocols
}
metadataOnly = useFakeDns && !DataStore.trafficSniffing && !isConfigWithSniffing
routeOnly = DataStore.trafficSniffing && !DataStore.destinationOverride
}
}
if (shouldDumpUID) dumpUID = true
})
inbounds.add(InboundObject().apply {
tag = TAG_DNS_IN
val path = SagerNet.deviceStorage.noBackupFilesDir.toString() + "/ipc_dns.sock"
val udsFile = File(path)
if (udsFile.exists()) udsFile.delete()
listen = path
protocol = "dokodemo-door"
settings = LazyInboundConfigurationObject(this,
DokodemoDoorInboundConfigurationObject().apply {
address = "/ipc_dns.sock" // placeholder, all queries are handled internally
network = "unix"
}
)
})
}
val outbounds = config.getArray("outbounds")?.map {
gson.fromJson(it.toString(), OutboundObject::class.java)
}?.toMutableList()
var flushOutbounds = false
val outboundTags = ArrayList()
val firstOutbound = outbounds?.get(0)
if (firstOutbound != null) {
if (firstOutbound.tag == null) {
firstOutbound.tag = TAG_AGENT
outboundTags.add(TAG_AGENT)
flushOutbounds = true
} else {
outboundTags.add(firstOutbound.tag)
}
}
var directTag = ""
val directOutbounds = outbounds?.filter { it.protocol == "freedom" }
if (!directOutbounds.isNullOrEmpty()) {
val directOutbound = if (directOutbounds.size == 1) {
directOutbounds[0]
} else {
val directOutboundsWithTag = directOutbounds.filter { it.tag != null }
if (directOutboundsWithTag.isNotEmpty()) {
directOutboundsWithTag[0]
} else {
directOutbounds[0]
}
}
if (directOutbound.tag.isNullOrEmpty()) {
directOutbound.tag = TAG_DIRECT
flushOutbounds = true
}
directTag = directOutbound.tag
}
inbounds.forEach { it.init() }
val inboundArray = JsonArray(inbounds.size)
for (inbound in inbounds) {
inboundArray.add(parseJson(gson.toJson(inbound), lenient = true))
}
config.add("inbounds", inboundArray)
if (flushOutbounds) {
outbounds!!.forEach { it.init() }
val outboundArray = JsonArray(outbounds.size)
for (outbound in outbounds) {
outboundArray.add(parseJson(gson.toJson(outbound), lenient = true))
}
}
return V2rayBuildResult(
config = GsonBuilder().setPrettyPrinting().create().toJson(config),
index = emptyList(),
requireWs = false,
wsPort = 0,
requireSh = false,
shPort = 0,
outboundTags = outboundTags,
outboundTagsCurrent = outboundTags,
outboundTagsAll = emptyMap(),
bypassTag = directTag,
observerTag = "",
observatoryTags = emptySet(),
dumpUID = shouldDumpUID,
alerts = emptyList(),
useFakeDNS = useFakeDns,
)
}