Add CoreOutboundBuilder to build OutboundBeans
Introduce CoreOutboundBuilder.kt to centralize ProfileItem → OutboundBean conversion logic. Provides createInitOutbound and per-protocol builders for VMESS, VLESS, Shadowsocks, SOCKS, Trojan, WireGuard, Hysteria2 and HTTP. Includes reusable helpers populateTransportSettings and populateTlsSettings to configure network transport, TLS/REALITY settings, finalmask manipulation and optional fragmentation via MMKV prefs. Also adds domain resolution logic (with IPv6 preference) in getServerAddress and preserves original format-specific behaviors.
This commit is contained in:
@@ -0,0 +1,635 @@
|
||||
package com.v2ray.ang.core
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
/**
|
||||
* Centralises all ProfileItem → OutboundBean conversion logic.
|
||||
*
|
||||
* keeping every build method implementation identical to the original
|
||||
* toOutbound implementations in each *Fmt class.
|
||||
*/
|
||||
object CoreOutboundBuilder {
|
||||
|
||||
/**
|
||||
* Dispatches [profileItem] to the appropriate protocol builder
|
||||
* and returns the resulting [OutboundBean].
|
||||
*/
|
||||
fun convert(profileItem: ProfileItem): OutboundBean? {
|
||||
return when (profileItem.configType) {
|
||||
EConfigType.VMESS -> toOutboundVmess(profileItem)
|
||||
EConfigType.CUSTOM -> null
|
||||
EConfigType.SHADOWSOCKS -> toOutboundShadowsocks(profileItem)
|
||||
EConfigType.SOCKS -> toOutboundSocks(profileItem)
|
||||
EConfigType.VLESS -> toOutboundVless(profileItem)
|
||||
EConfigType.TROJAN -> toOutboundTrojan(profileItem)
|
||||
EConfigType.WIREGUARD -> toOutboundWireguard(profileItem)
|
||||
EConfigType.HYSTERIA2 -> toOutboundHysteria2(profileItem)
|
||||
EConfigType.HTTP -> toOutboundHttp(profileItem)
|
||||
EConfigType.POLICYGROUP -> null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an initial outbound configuration for a specific protocol type.
|
||||
*
|
||||
* Provides a template configuration for different protocol types.
|
||||
*
|
||||
* @param configType The type of configuration to create
|
||||
* @return An initial OutboundBean for the specified configuration type, or null for custom types
|
||||
*/
|
||||
fun createInitOutbound(configType: EConfigType): OutboundBean? {
|
||||
return when (configType) {
|
||||
EConfigType.VMESS,
|
||||
EConfigType.VLESS ->
|
||||
return OutboundBean(
|
||||
protocol = configType.name.lowercase(),
|
||||
settings = OutboundBean.OutSettingsBean(
|
||||
vnext = listOf(
|
||||
OutboundBean.OutSettingsBean.VnextBean(
|
||||
users = listOf(OutboundBean.OutSettingsBean.VnextBean.UsersBean())
|
||||
)
|
||||
)
|
||||
),
|
||||
streamSettings = OutboundBean.StreamSettingsBean()
|
||||
)
|
||||
|
||||
EConfigType.SHADOWSOCKS,
|
||||
EConfigType.SOCKS,
|
||||
EConfigType.HTTP,
|
||||
EConfigType.TROJAN ->
|
||||
return OutboundBean(
|
||||
protocol = configType.name.lowercase(),
|
||||
settings = OutboundBean.OutSettingsBean(
|
||||
servers = listOf(OutboundBean.OutSettingsBean.ServersBean())
|
||||
),
|
||||
streamSettings = OutboundBean.StreamSettingsBean()
|
||||
)
|
||||
|
||||
EConfigType.WIREGUARD ->
|
||||
return OutboundBean(
|
||||
protocol = configType.name.lowercase(),
|
||||
settings = OutboundBean.OutSettingsBean(
|
||||
secretKey = "",
|
||||
peers = listOf(OutboundBean.OutSettingsBean.WireGuardBean())
|
||||
)
|
||||
)
|
||||
|
||||
EConfigType.HYSTERIA,
|
||||
EConfigType.HYSTERIA2 ->
|
||||
return OutboundBean(
|
||||
protocol = EConfigType.HYSTERIA.name.lowercase(),
|
||||
settings = OutboundBean.OutSettingsBean(
|
||||
servers = null
|
||||
),
|
||||
streamSettings = OutboundBean.StreamSettingsBean()
|
||||
)
|
||||
|
||||
EConfigType.CUSTOM -> null
|
||||
EConfigType.POLICYGROUP -> null
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-protocol builders — implementations are identical to each *Fmt.toOutbound ──
|
||||
|
||||
private fun toOutboundVmess(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = createInitOutbound(EConfigType.VMESS)
|
||||
|
||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||
vnext.address = getServerAddress(profileItem)
|
||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||
vnext.users[0].id = profileItem.password.orEmpty()
|
||||
vnext.users[0].security = profileItem.method
|
||||
}
|
||||
|
||||
val sni = outboundBean?.streamSettings?.let {
|
||||
populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean?.streamSettings?.let {
|
||||
populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
private fun toOutboundVless(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = createInitOutbound(EConfigType.VLESS)
|
||||
|
||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||
vnext.address = getServerAddress(profileItem)
|
||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||
vnext.users[0].id = profileItem.password.orEmpty()
|
||||
vnext.users[0].encryption = profileItem.method
|
||||
vnext.users[0].flow = profileItem.flow
|
||||
}
|
||||
|
||||
val sni = outboundBean?.streamSettings?.let {
|
||||
populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean?.streamSettings?.let {
|
||||
populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
private fun toOutboundShadowsocks(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = createInitOutbound(EConfigType.SHADOWSOCKS)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.password = profileItem.password
|
||||
server.method = profileItem.method
|
||||
}
|
||||
|
||||
val sni = outboundBean?.streamSettings?.let {
|
||||
populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean?.streamSettings?.let {
|
||||
populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
private fun toOutboundTrojan(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = createInitOutbound(EConfigType.TROJAN)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.password = profileItem.password
|
||||
server.flow = profileItem.flow
|
||||
}
|
||||
|
||||
val sni = outboundBean?.streamSettings?.let {
|
||||
populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean?.streamSettings?.let {
|
||||
populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
private fun toOutboundSocks(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = createInitOutbound(EConfigType.SOCKS)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
if (profileItem.username.isNotNullEmpty()) {
|
||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||
socksUsersBean.user = profileItem.username.orEmpty()
|
||||
socksUsersBean.pass = profileItem.password.orEmpty()
|
||||
server.users = listOf(socksUsersBean)
|
||||
}
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
private fun toOutboundHttp(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = createInitOutbound(EConfigType.HTTP)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
if (profileItem.username.isNotNullEmpty()) {
|
||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||
socksUsersBean.user = profileItem.username.orEmpty()
|
||||
socksUsersBean.pass = profileItem.password.orEmpty()
|
||||
server.users = listOf(socksUsersBean)
|
||||
}
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
private fun toOutboundWireguard(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = createInitOutbound(EConfigType.WIREGUARD)
|
||||
|
||||
outboundBean?.settings?.let { wireguard ->
|
||||
wireguard.secretKey = profileItem.secretKey
|
||||
wireguard.address = (profileItem.localAddress ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
||||
wireguard.peers?.firstOrNull()?.let { peer ->
|
||||
peer.publicKey = profileItem.publicKey.orEmpty()
|
||||
peer.preSharedKey = profileItem.preSharedKey?.nullIfBlank()
|
||||
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
||||
}
|
||||
wireguard.mtu = profileItem.mtu
|
||||
wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() }
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
private fun toOutboundHysteria2(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = createInitOutbound(EConfigType.HYSTERIA2) ?: return null
|
||||
profileItem.network = NetworkType.HYSTERIA.type
|
||||
profileItem.alpn = "h3"
|
||||
|
||||
outboundBean.settings?.let { server ->
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.version = 2
|
||||
}
|
||||
|
||||
val sni = outboundBean.streamSettings?.let {
|
||||
populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean.streamSettings?.let {
|
||||
populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures transport settings for an outbound connection.
|
||||
*
|
||||
* Sets up protocol-specific transport options based on the profile settings.
|
||||
*
|
||||
* @param streamSettings The stream settings to configure
|
||||
* @param profileItem The profile containing transport configuration
|
||||
* @return The Server Name Indication (SNI) value to use, or null if not applicable
|
||||
*/
|
||||
fun populateTransportSettings(streamSettings: OutboundBean.StreamSettingsBean, profileItem: ProfileItem): String? {
|
||||
val transport = profileItem.network.orEmpty()
|
||||
val headerType = profileItem.headerType
|
||||
val host = profileItem.host
|
||||
val path = profileItem.path
|
||||
val seed = profileItem.seed
|
||||
// val quicSecurity = profileItem.quicSecurity
|
||||
// val key = profileItem.quicKey
|
||||
val mode = profileItem.mode
|
||||
val serviceName = profileItem.serviceName
|
||||
val authority = profileItem.authority
|
||||
val xhttpMode = profileItem.xhttpMode
|
||||
val xhttpExtra = profileItem.xhttpExtra
|
||||
val finalMask = profileItem.finalMask
|
||||
var sni: String? = null
|
||||
streamSettings.network = transport.ifEmpty { NetworkType.TCP.type }
|
||||
when (streamSettings.network) {
|
||||
NetworkType.TCP.type -> {
|
||||
val tcpSetting = OutboundBean.StreamSettingsBean.TcpSettingsBean()
|
||||
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
||||
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
||||
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
||||
val requestObj = OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean()
|
||||
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
tcpSetting.header.request = requestObj
|
||||
sni = requestObj.headers.Host?.getOrNull(0)
|
||||
}
|
||||
} else {
|
||||
tcpSetting.header.type = "none"
|
||||
sni = host
|
||||
}
|
||||
streamSettings.tcpSettings = tcpSetting
|
||||
}
|
||||
|
||||
NetworkType.KCP.type -> {
|
||||
val kcpSetting = OutboundBean.StreamSettingsBean.KcpSettingsBean()
|
||||
profileItem.kcpMtu?.let { kcpSetting.mtu = it }
|
||||
profileItem.kcpTti?.let { kcpSetting.tti = it }
|
||||
streamSettings.kcpSettings = kcpSetting
|
||||
val udpMaskList = mutableListOf<OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean>()
|
||||
if (!headerType.isNullOrEmpty() && headerType != "none") {
|
||||
val kcpHeaderType = when {
|
||||
headerType == "wechat-video" -> "header-wechat"
|
||||
else -> "header-$headerType"
|
||||
}
|
||||
udpMaskList.add(
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = kcpHeaderType,
|
||||
settings = if (headerType == "dns" && !host.isNullOrEmpty()) {
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
domain = host
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
if (seed.isNullOrEmpty()) {
|
||||
udpMaskList.add(
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "mkcp-original"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
udpMaskList.add(
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "mkcp-aes128gcm",
|
||||
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
password = seed
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
streamSettings.finalmask = OutboundBean.StreamSettingsBean.FinalMaskBean(
|
||||
udp = udpMaskList.toList()
|
||||
)
|
||||
}
|
||||
|
||||
NetworkType.WS.type -> {
|
||||
val wssetting = OutboundBean.StreamSettingsBean.WsSettingsBean()
|
||||
wssetting.headers.Host = host.orEmpty()
|
||||
sni = host
|
||||
wssetting.path = path ?: "/"
|
||||
streamSettings.wsSettings = wssetting
|
||||
}
|
||||
|
||||
NetworkType.HTTP_UPGRADE.type -> {
|
||||
val httpupgradeSetting = OutboundBean.StreamSettingsBean.HttpupgradeSettingsBean()
|
||||
httpupgradeSetting.host = host.orEmpty()
|
||||
sni = host
|
||||
httpupgradeSetting.path = path ?: "/"
|
||||
streamSettings.httpupgradeSettings = httpupgradeSetting
|
||||
}
|
||||
|
||||
NetworkType.XHTTP.type -> {
|
||||
val xhttpSetting = OutboundBean.StreamSettingsBean.XhttpSettingsBean()
|
||||
xhttpSetting.host = host.orEmpty()
|
||||
sni = host
|
||||
xhttpSetting.path = path ?: "/"
|
||||
xhttpSetting.mode = xhttpMode
|
||||
xhttpSetting.extra = JsonUtil.parseString(xhttpExtra)
|
||||
streamSettings.xhttpSettings = xhttpSetting
|
||||
}
|
||||
|
||||
NetworkType.H2.type, NetworkType.HTTP.type -> {
|
||||
streamSettings.network = NetworkType.H2.type
|
||||
val h2Setting = OutboundBean.StreamSettingsBean.HttpSettingsBean()
|
||||
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
sni = h2Setting.host.getOrNull(0)
|
||||
h2Setting.path = path ?: "/"
|
||||
streamSettings.httpSettings = h2Setting
|
||||
}
|
||||
|
||||
// "quic" -> {
|
||||
// val quicsetting = QuicSettingBean()
|
||||
// quicsetting.security = quicSecurity ?: "none"
|
||||
// quicsetting.key = key.orEmpty()
|
||||
// quicsetting.header.type = headerType ?: "none"
|
||||
// quicSettings = quicsetting
|
||||
// }
|
||||
|
||||
NetworkType.GRPC.type -> {
|
||||
val grpcSetting = OutboundBean.StreamSettingsBean.GrpcSettingsBean()
|
||||
grpcSetting.multiMode = mode == "multi"
|
||||
grpcSetting.serviceName = serviceName.orEmpty()
|
||||
grpcSetting.authority = authority.orEmpty()
|
||||
grpcSetting.idle_timeout = 60
|
||||
grpcSetting.health_check_timeout = 20
|
||||
sni = authority
|
||||
streamSettings.grpcSettings = grpcSetting
|
||||
}
|
||||
|
||||
NetworkType.HYSTERIA.type -> {
|
||||
val hysteriaSetting = OutboundBean.StreamSettingsBean.HysteriaSettingsBean(
|
||||
version = 2,
|
||||
auth = profileItem.password.orEmpty(),
|
||||
)
|
||||
val quicParams = OutboundBean.StreamSettingsBean.FinalMaskBean.QuicParamsBean(
|
||||
brutalUp = profileItem.bandwidthUp?.nullIfBlank(),
|
||||
brutalDown = profileItem.bandwidthDown?.nullIfBlank(),
|
||||
)
|
||||
quicParams.congestion = if (quicParams.brutalUp != null || quicParams.brutalDown != null) "brutal" else null
|
||||
if (profileItem.portHopping.isNotNullEmpty()) {
|
||||
val rawInterval = profileItem.portHoppingInterval?.trim().nullIfBlank()
|
||||
val interval = if (rawInterval == null) {
|
||||
"30"
|
||||
} else {
|
||||
val singleValue = rawInterval.toIntOrNull()
|
||||
if (singleValue != null) {
|
||||
if (singleValue < 5) {
|
||||
"30"
|
||||
} else {
|
||||
rawInterval
|
||||
}
|
||||
} else {
|
||||
val parts = rawInterval.split('-')
|
||||
if (parts.size == 2) {
|
||||
val start = parts[0].trim().toIntOrNull()
|
||||
val end = parts[1].trim().toIntOrNull()
|
||||
if (start != null && end != null) {
|
||||
val minStart = maxOf(5, start)
|
||||
val minEnd = maxOf(minStart, end)
|
||||
"$minStart-$minEnd"
|
||||
} else {
|
||||
"30"
|
||||
}
|
||||
} else {
|
||||
"30"
|
||||
}
|
||||
}
|
||||
}
|
||||
quicParams.udpHop = OutboundBean.StreamSettingsBean.FinalMaskBean.QuicParamsBean.UdpHopBean(
|
||||
ports = profileItem.portHopping,
|
||||
interval = interval
|
||||
)
|
||||
}
|
||||
val finalmask = OutboundBean.StreamSettingsBean.FinalMaskBean(
|
||||
quicParams = quicParams
|
||||
)
|
||||
if (profileItem.obfsPassword.isNotNullEmpty()) {
|
||||
finalmask.udp = listOf(
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "salamander",
|
||||
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
password = profileItem.obfsPassword.orEmpty()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
streamSettings.hysteriaSettings = hysteriaSetting
|
||||
streamSettings.finalmask = finalmask
|
||||
}
|
||||
}
|
||||
finalMask?.let {
|
||||
val parsedFinalMask = JsonUtil.parseString(finalMask)
|
||||
if (parsedFinalMask != null) {
|
||||
streamSettings.finalmask = parsedFinalMask
|
||||
} else {
|
||||
LogUtil.w("V2rayConfigManager", "Invalid finalMask JSON, keeping previously generated finalmask")
|
||||
}
|
||||
}
|
||||
return sni
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures TLS or REALITY security settings for an outbound connection.
|
||||
*
|
||||
* Sets up security-related parameters like certificates, fingerprints, and SNI.
|
||||
*
|
||||
* @param streamSettings The stream settings to configure
|
||||
* @param profileItem The profile containing security configuration
|
||||
* @param sniExt An external SNI value to use if the profile doesn't specify one
|
||||
*/
|
||||
fun populateTlsSettings(streamSettings: OutboundBean.StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) {
|
||||
val streamSecurity = profileItem.security.orEmpty()
|
||||
val allowInsecure = profileItem.insecure == true
|
||||
val sni = if (profileItem.sni.isNullOrEmpty()) {
|
||||
when {
|
||||
sniExt.isNotNullEmpty() && Utils.isDomainName(sniExt) -> sniExt
|
||||
profileItem.server.isNotNullEmpty() && Utils.isDomainName(profileItem.server) -> profileItem.server
|
||||
else -> sniExt
|
||||
}
|
||||
} else {
|
||||
profileItem.sni
|
||||
}
|
||||
|
||||
streamSettings.security = streamSecurity.nullIfBlank()
|
||||
if (streamSettings.security == null) return
|
||||
val tlsSetting = OutboundBean.StreamSettingsBean.TlsSettingsBean(
|
||||
allowInsecure = allowInsecure,
|
||||
serverName = sni.nullIfBlank(),
|
||||
fingerprint = profileItem.fingerPrint.nullIfBlank(),
|
||||
alpn = profileItem.alpn?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }.takeIf { !it.isNullOrEmpty() },
|
||||
echConfigList = profileItem.echConfigList.nullIfBlank(),
|
||||
pinnedPeerCertSha256 = profileItem.pinnedCA256.nullIfBlank(),
|
||||
publicKey = profileItem.publicKey.nullIfBlank(),
|
||||
shortId = profileItem.shortId.nullIfBlank(),
|
||||
spiderX = profileItem.spiderX.nullIfBlank(),
|
||||
mldsa65Verify = profileItem.mldsa65Verify.nullIfBlank(),
|
||||
)
|
||||
if (streamSettings.security == AppConfig.TLS) {
|
||||
streamSettings.tlsSettings = tlsSetting
|
||||
streamSettings.realitySettings = null
|
||||
} else if (streamSettings.security == AppConfig.REALITY) {
|
||||
streamSettings.tlsSettings = null
|
||||
streamSettings.realitySettings = tlsSetting
|
||||
}
|
||||
|
||||
if (profileItem.finalMask.isNullOrEmpty()) {
|
||||
updateOutboundFragment(streamSettings)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the outbound with fragment settings for traffic optimization.
|
||||
*
|
||||
* Configures packet fragmentation for TLS and REALITY protocols if enabled.
|
||||
*
|
||||
* @param streamSettings The streamSettings object to be modified
|
||||
* @return true if fragment configuration was successful, false otherwise
|
||||
*/
|
||||
private fun updateOutboundFragment(streamSettings: OutboundBean.StreamSettingsBean): Boolean {
|
||||
try {
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
||||
return true
|
||||
}
|
||||
if (streamSettings.security != AppConfig.TLS
|
||||
&& streamSettings.security != AppConfig.REALITY
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (streamSettings.sockopt?.dialerProxy.isNotNullEmpty()) {
|
||||
return true
|
||||
}
|
||||
|
||||
var packets =
|
||||
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
|
||||
if (streamSettings.security == AppConfig.REALITY
|
||||
&& packets == "tlshello"
|
||||
) {
|
||||
packets = "1-3"
|
||||
} else if (streamSettings.security == AppConfig.TLS
|
||||
&& packets != "tlshello"
|
||||
) {
|
||||
packets = "tlshello"
|
||||
}
|
||||
|
||||
val fragmentMask = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "fragment",
|
||||
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
packets = packets,
|
||||
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
|
||||
?: "50-100",
|
||||
delay = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
||||
?: "10-20"
|
||||
)
|
||||
)
|
||||
val noiseMask = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "noise",
|
||||
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
noise = listOf(
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean.NoiseMaskBean(
|
||||
rand = "10-20",
|
||||
delay = "10-16",
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val finalMaskObj = streamSettings.finalmask?.let { existingFinalMask ->
|
||||
JsonUtil.parseString(JsonUtil.toJson(existingFinalMask))
|
||||
} ?: JsonObject()
|
||||
|
||||
// finalmask.tcp / finalmask.udp are arrays; prepend mask at index 0.
|
||||
fun prependMask(scope: String, mask: OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean) {
|
||||
val current = finalMaskObj.get(scope)
|
||||
if (current != null && current.isJsonArray && current.asJsonArray.size() > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
val newArray = JsonArray()
|
||||
newArray.add(JsonUtil.parseString(JsonUtil.toJson(mask)))
|
||||
|
||||
if (current != null && current.isJsonArray) {
|
||||
current.asJsonArray.forEach { newArray.add(it) }
|
||||
}
|
||||
finalMaskObj.add(scope, newArray)
|
||||
}
|
||||
|
||||
prependMask("tcp", fragmentMask)
|
||||
prependMask("udp", noiseMask)
|
||||
streamSettings.finalmask = finalMaskObj
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to update outbound fragment", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getServerAddress(profileItem: ProfileItem): String {
|
||||
if (Utils.isPureIpAddress(profileItem.server.orEmpty())) {
|
||||
return profileItem.server.orEmpty()
|
||||
}
|
||||
|
||||
val domain = HttpUtil.toIdnDomain(profileItem.server.orEmpty())
|
||||
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "2") {
|
||||
return domain
|
||||
}
|
||||
//Resolve and replace domain
|
||||
val resolvedIps = HttpUtil.resolveHostToIP(domain, MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
|
||||
if (resolvedIps.isNullOrEmpty()) {
|
||||
return domain
|
||||
}
|
||||
return resolvedIps.first()
|
||||
}
|
||||
}
|
||||
@@ -172,21 +172,4 @@ open class FmtBase {
|
||||
|
||||
return dicQuery
|
||||
}
|
||||
|
||||
fun getServerAddress(profileItem: ProfileItem): String {
|
||||
if (Utils.isPureIpAddress(profileItem.server.orEmpty())) {
|
||||
return profileItem.server.orEmpty()
|
||||
}
|
||||
|
||||
val domain = HttpUtil.toIdnDomain(profileItem.server.orEmpty())
|
||||
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "2") {
|
||||
return domain
|
||||
}
|
||||
//Resolve and replace domain
|
||||
val resolvedIps = HttpUtil.resolveHostToIP(domain, MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
|
||||
if (resolvedIps.isNullOrEmpty()) {
|
||||
return domain
|
||||
}
|
||||
return resolvedIps.first()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
|
||||
object HttpFmt : FmtBase() {
|
||||
/**
|
||||
* Converts a ProfileItem object to an OutboundBean object.
|
||||
*
|
||||
* @param profileItem the ProfileItem object to convert
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
if (profileItem.username.isNotNullEmpty()) {
|
||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||
socksUsersBean.user = profileItem.username.orEmpty()
|
||||
socksUsersBean.pass = profileItem.password.orEmpty()
|
||||
server.users = listOf(socksUsersBean)
|
||||
}
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,12 @@ package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -110,32 +108,4 @@ object Hysteria2Fmt : FmtBase() {
|
||||
|
||||
return toUri(config, config.password, dicQuery)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a ProfileItem object to an OutboundBean object.
|
||||
*
|
||||
* @param profileItem the ProfileItem object to convert
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2) ?: return null
|
||||
profileItem.network = NetworkType.HYSTERIA.type
|
||||
profileItem.alpn = "h3"
|
||||
|
||||
outboundBean.settings?.let { server ->
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.version = 2
|
||||
}
|
||||
|
||||
val sni = outboundBean.streamSettings?.let {
|
||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean.streamSettings?.let {
|
||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,9 @@ package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
@@ -124,31 +122,4 @@ object ShadowsocksFmt : FmtBase() {
|
||||
|
||||
return toUri(config, Utils.encode(pw, true), null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a ProfileItem object to an OutboundBean object.
|
||||
*
|
||||
* @param profileItem the ProfileItem object to convert
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.password = profileItem.password
|
||||
server.method = profileItem.method
|
||||
}
|
||||
|
||||
val sni = outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -53,27 +51,4 @@ object SocksFmt : FmtBase() {
|
||||
|
||||
return toUri(config, Utils.encode(pw, true), null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a ProfileItem object to an OutboundBean object.
|
||||
*
|
||||
* @param profileItem the ProfileItem object to convert
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
if (profileItem.username.isNotNullEmpty()) {
|
||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||
socksUsersBean.user = profileItem.username.orEmpty()
|
||||
socksUsersBean.pass = profileItem.password.orEmpty()
|
||||
server.users = listOf(socksUsersBean)
|
||||
}
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,10 @@ package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -53,31 +51,4 @@ object TrojanFmt : FmtBase() {
|
||||
|
||||
return toUri(config, config.password, dicQuery)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a ProfileItem object to an OutboundBean object.
|
||||
*
|
||||
* @param profileItem the ProfileItem object to convert
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.password = profileItem.password
|
||||
server.flow = profileItem.flow
|
||||
}
|
||||
|
||||
val sni = outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,9 @@ package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -50,31 +48,4 @@ object VlessFmt : FmtBase() {
|
||||
return toUri(config, config.password, dicQuery)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a ProfileItem object to an OutboundBean object.
|
||||
*
|
||||
* @param profileItem the ProfileItem object to convert
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS)
|
||||
|
||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||
vnext.address = getServerAddress(profileItem)
|
||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||
vnext.users[0].id = profileItem.password.orEmpty()
|
||||
vnext.users[0].encryption = profileItem.method
|
||||
vnext.users[0].flow = profileItem.flow
|
||||
}
|
||||
|
||||
val sni = outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,12 @@ package com.v2ray.ang.fmt
|
||||
import android.text.TextUtils
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.dto.VmessQRCode
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
@@ -174,31 +172,5 @@ object VmessFmt : FmtBase() {
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a ProfileItem object to an OutboundBean object.
|
||||
*
|
||||
* @param profileItem the ProfileItem object to convert
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS)
|
||||
|
||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||
vnext.address = getServerAddress(profileItem)
|
||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||
vnext.users[0].id = profileItem.password.orEmpty()
|
||||
vnext.users[0].security = profileItem.method
|
||||
}
|
||||
|
||||
val sni = outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||
}
|
||||
|
||||
outboundBean?.streamSettings?.let {
|
||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.extension.removeWhiteSpace
|
||||
import com.v2ray.ang.handler.V2rayConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -31,7 +28,7 @@ object WireguardFmt : FmtBase() {
|
||||
config.serverPort = uri.port.toString()
|
||||
|
||||
config.secretKey = uri.userInfo.orEmpty()
|
||||
config.localAddress = queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
|
||||
config.localAddress = queryParam["address"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||
config.publicKey = queryParam["publickey"].orEmpty()
|
||||
config.preSharedKey = queryParam["presharedkey"]?.nullIfBlank()
|
||||
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||
@@ -82,7 +79,7 @@ object WireguardFmt : FmtBase() {
|
||||
|
||||
config.secretKey = interfaceParams["privatekey"].orEmpty()
|
||||
config.remarks = System.currentTimeMillis().toString()
|
||||
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
|
||||
config.localAddress = interfaceParams["address"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||
config.publicKey = peerParams["publickey"].orEmpty()
|
||||
config.preSharedKey = peerParams["presharedkey"]?.nullIfBlank()
|
||||
@@ -100,29 +97,6 @@ object WireguardFmt : FmtBase() {
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a ProfileItem object to an OutboundBean object.
|
||||
*
|
||||
* @param profileItem the ProfileItem object to convert
|
||||
* @return the converted OutboundBean object, or null if conversion fails
|
||||
*/
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.WIREGUARD)
|
||||
|
||||
outboundBean?.settings?.let { wireguard ->
|
||||
wireguard.secretKey = profileItem.secretKey
|
||||
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
||||
wireguard.peers?.firstOrNull()?.let { peer ->
|
||||
peer.publicKey = profileItem.publicKey.orEmpty()
|
||||
peer.preSharedKey = profileItem.preSharedKey?.nullIfBlank()
|
||||
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
||||
}
|
||||
wireguard.mtu = profileItem.mtu
|
||||
wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() }
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a ProfileItem object to a URI string.
|
||||
|
||||
@@ -4,27 +4,14 @@ import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import com.google.gson.JsonArray
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
|
||||
import com.v2ray.ang.core.CoreOutboundBuilder
|
||||
import com.v2ray.ang.dto.ConfigResult
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.RulesetItem
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean
|
||||
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.fmt.HttpFmt
|
||||
import com.v2ray.ang.fmt.Hysteria2Fmt
|
||||
import com.v2ray.ang.fmt.ShadowsocksFmt
|
||||
import com.v2ray.ang.fmt.SocksFmt
|
||||
import com.v2ray.ang.fmt.TrojanFmt
|
||||
import com.v2ray.ang.fmt.VlessFmt
|
||||
import com.v2ray.ang.fmt.VmessFmt
|
||||
import com.v2ray.ang.fmt.WireguardFmt
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
@@ -256,7 +243,7 @@ object V2rayConfigManager {
|
||||
getInbounds(v2rayConfig)
|
||||
|
||||
v2rayConfig.outbounds.removeAt(0)
|
||||
val outboundsList = mutableListOf<OutboundBean>()
|
||||
val outboundsList = mutableListOf<V2rayConfig.OutboundBean>()
|
||||
var index = 0
|
||||
for (config in validConfigs) {
|
||||
index++
|
||||
@@ -484,7 +471,7 @@ object V2rayConfigManager {
|
||||
rulesetItems
|
||||
.filter { it.enabled }
|
||||
.mapNotNull { it.outboundTag.takeIf { tag -> tag.isNotBlank() } }
|
||||
.filter { it !in BUILTIN_OUTBOUND_TAGS }
|
||||
.filter { it !in AppConfig.BUILTIN_OUTBOUND_TAGS }
|
||||
.distinct()
|
||||
.forEach { tag ->
|
||||
if (tag in existingTags) return@forEach
|
||||
@@ -549,7 +536,7 @@ object V2rayConfigManager {
|
||||
return
|
||||
}
|
||||
|
||||
val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return
|
||||
val rule = JsonUtil.fromJson(JsonUtil.toJson(item), V2rayConfig.RoutingBean.RulesBean::class.java) ?: return
|
||||
|
||||
// Replace specific geoip rules with ext versions
|
||||
rule.ip?.let { ipList ->
|
||||
@@ -579,7 +566,7 @@ object V2rayConfigManager {
|
||||
// If the outbound tag is a custom one that failed to inject, fall back to proxy
|
||||
val outboundTag = rule.outboundTag
|
||||
if (!outboundTag.isNullOrBlank()
|
||||
&& outboundTag !in BUILTIN_OUTBOUND_TAGS
|
||||
&& outboundTag !in AppConfig.BUILTIN_OUTBOUND_TAGS
|
||||
&& v2rayConfig.outbounds.none { it.tag == outboundTag }
|
||||
) {
|
||||
LogUtil.w(AppConfig.TAG, "Outbound tag '$outboundTag' not found, falling back to '${AppConfig.TAG_PROXY}'")
|
||||
@@ -648,7 +635,7 @@ object V2rayConfigManager {
|
||||
if (SettingsManager.isUsingHevTun()) {
|
||||
//hev-socks5-tunnel dns routing
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, RulesBean(
|
||||
0, V2rayConfig.RoutingBean.RulesBean(
|
||||
inboundTag = arrayListOf("socks"),
|
||||
outboundTag = "dns-out",
|
||||
port = "53",
|
||||
@@ -656,7 +643,7 @@ object V2rayConfigManager {
|
||||
)
|
||||
} else {
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, RulesBean(
|
||||
0, V2rayConfig.RoutingBean.RulesBean(
|
||||
inboundTag = arrayListOf("tun"),
|
||||
outboundTag = "dns-out",
|
||||
port = "53",
|
||||
@@ -668,7 +655,7 @@ object V2rayConfigManager {
|
||||
// DNS outbound
|
||||
if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
|
||||
v2rayConfig.outbounds.add(
|
||||
OutboundBean(
|
||||
V2rayConfig.OutboundBean(
|
||||
protocol = "dns",
|
||||
tag = "dns-out",
|
||||
settings = null,
|
||||
@@ -771,14 +758,14 @@ object V2rayConfigManager {
|
||||
|
||||
// DNS routing
|
||||
v2rayConfig.routing.rules.add(
|
||||
RulesBean(
|
||||
V2rayConfig.RoutingBean.RulesBean(
|
||||
outboundTag = AppConfig.TAG_DIRECT,
|
||||
inboundTag = arrayListOf(AppConfig.TAG_DOMESTIC_DNS),
|
||||
domain = null
|
||||
)
|
||||
)
|
||||
v2rayConfig.routing.rules.add(
|
||||
RulesBean(
|
||||
V2rayConfig.RoutingBean.RulesBean(
|
||||
outboundTag = AppConfig.TAG_PROXY,
|
||||
inboundTag = arrayListOf(AppConfig.TAG_DNS),
|
||||
domain = null
|
||||
@@ -883,7 +870,7 @@ object V2rayConfigManager {
|
||||
* @param outbound The outbound connection to update
|
||||
* @return true if the update was successful, false otherwise
|
||||
*/
|
||||
private fun updateOutboundWithGlobalSettings(outbound: OutboundBean): Boolean {
|
||||
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean {
|
||||
try {
|
||||
var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
|
||||
val protocol = outbound.protocol
|
||||
@@ -936,7 +923,7 @@ object V2rayConfigManager {
|
||||
}
|
||||
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
|
||||
requestString,
|
||||
StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
|
||||
V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
|
||||
)
|
||||
outbound.streamSettings?.tcpSettings?.header?.request?.path =
|
||||
if (path.isNullOrEmpty()) {
|
||||
@@ -1039,14 +1026,14 @@ object V2rayConfigManager {
|
||||
|
||||
if (v2rayConfig.routing.domainStrategy == "IPIfNonMatch") {
|
||||
v2rayConfig.routing.rules.add(
|
||||
RulesBean(
|
||||
V2rayConfig.RoutingBean.RulesBean(
|
||||
ip = arrayListOf("0.0.0.0/0", "::/0"),
|
||||
balancerTag = AppConfig.TAG_BALANCER,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
v2rayConfig.routing.rules.add(
|
||||
RulesBean(
|
||||
V2rayConfig.RoutingBean.RulesBean(
|
||||
network = "tcp,udp",
|
||||
balancerTag = AppConfig.TAG_BALANCER,
|
||||
)
|
||||
@@ -1057,92 +1044,6 @@ object V2rayConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the outbound with fragment settings for traffic optimization.
|
||||
*
|
||||
* Configures packet fragmentation for TLS and REALITY protocols if enabled.
|
||||
*
|
||||
* @param streamSettings The streamSettings object to be modified
|
||||
* @return true if fragment configuration was successful, false otherwise
|
||||
*/
|
||||
private fun updateOutboundFragment(streamSettings: StreamSettingsBean): Boolean {
|
||||
try {
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
||||
return true
|
||||
}
|
||||
if (streamSettings.security != AppConfig.TLS
|
||||
&& streamSettings.security != AppConfig.REALITY
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (streamSettings.sockopt?.dialerProxy.isNotNullEmpty()) {
|
||||
return true
|
||||
}
|
||||
|
||||
var packets =
|
||||
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
|
||||
if (streamSettings.security == AppConfig.REALITY
|
||||
&& packets == "tlshello"
|
||||
) {
|
||||
packets = "1-3"
|
||||
} else if (streamSettings.security == AppConfig.TLS
|
||||
&& packets != "tlshello"
|
||||
) {
|
||||
packets = "tlshello"
|
||||
}
|
||||
|
||||
val fragmentMask = StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "fragment",
|
||||
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
packets = packets,
|
||||
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
|
||||
?: "50-100",
|
||||
delay = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
||||
?: "10-20"
|
||||
)
|
||||
)
|
||||
val noiseMask = StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "noise",
|
||||
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
noise = listOf(
|
||||
StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean.NoiseMaskBean(
|
||||
rand = "10-20",
|
||||
delay = "10-16",
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val finalMaskObj = streamSettings.finalmask?.let { existingFinalMask ->
|
||||
JsonUtil.parseString(JsonUtil.toJson(existingFinalMask))
|
||||
} ?: com.google.gson.JsonObject()
|
||||
|
||||
// finalmask.tcp / finalmask.udp are arrays; prepend mask at index 0.
|
||||
fun prependMask(scope: String, mask: StreamSettingsBean.FinalMaskBean.MaskBean) {
|
||||
val current = finalMaskObj.get(scope)
|
||||
if (current != null && current.isJsonArray && current.asJsonArray.size() > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
val newArray = JsonArray()
|
||||
newArray.add(JsonUtil.parseString(JsonUtil.toJson(mask)))
|
||||
|
||||
if (current != null && current.isJsonArray) {
|
||||
current.asJsonArray.forEach { newArray.add(it) }
|
||||
}
|
||||
finalMaskObj.add(scope, newArray)
|
||||
}
|
||||
|
||||
prependMask("tcp", fragmentMask)
|
||||
prependMask("udp", noiseMask)
|
||||
streamSettings.finalmask = finalMaskObj
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to update outbound fragment", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves domain names to IP addresses in outbound connections.
|
||||
*
|
||||
@@ -1162,7 +1063,7 @@ object V2rayConfigManager {
|
||||
|
||||
if (newHosts.containsKey(domain)) {
|
||||
item.ensureSockopt().domainStrategy = "UseIP"
|
||||
item.ensureSockopt().happyEyeballs = StreamSettingsBean.HappyEyeballsBean(
|
||||
item.ensureSockopt().happyEyeballs = V2rayConfig.OutboundBean.StreamSettingsBean.HappyEyeballsBean(
|
||||
prioritizeIPv6 = preferIpv6,
|
||||
interleave = 2
|
||||
)
|
||||
@@ -1173,7 +1074,7 @@ object V2rayConfigManager {
|
||||
if (resolvedIps.isNullOrEmpty()) continue
|
||||
|
||||
item.ensureSockopt().domainStrategy = "UseIP"
|
||||
item.ensureSockopt().happyEyeballs = StreamSettingsBean.HappyEyeballsBean(
|
||||
item.ensureSockopt().happyEyeballs = V2rayConfig.OutboundBean.StreamSettingsBean.HappyEyeballsBean(
|
||||
prioritizeIPv6 = preferIpv6,
|
||||
interleave = 2
|
||||
)
|
||||
@@ -1190,350 +1091,15 @@ object V2rayConfigManager {
|
||||
/**
|
||||
* Converts a profile item to an outbound configuration.
|
||||
*
|
||||
* Creates appropriate outbound settings based on the protocol type.
|
||||
* Delegates to [com.v2ray.ang.core.CoreOutboundBuilder] which owns all per-protocol
|
||||
* conversion logic, keeping this manager focused on config orchestration.
|
||||
*
|
||||
* @param profileItem The profile item to convert
|
||||
* @return OutboundBean configuration for the profile, or null if not supported
|
||||
*/
|
||||
private fun convertProfile2Outbound(profileItem: ProfileItem): OutboundBean? {
|
||||
return when (profileItem.configType) {
|
||||
EConfigType.VMESS -> VmessFmt.toOutbound(profileItem)
|
||||
EConfigType.CUSTOM -> null
|
||||
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toOutbound(profileItem)
|
||||
EConfigType.SOCKS -> SocksFmt.toOutbound(profileItem)
|
||||
EConfigType.VLESS -> VlessFmt.toOutbound(profileItem)
|
||||
EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem)
|
||||
EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem)
|
||||
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toOutbound(profileItem)
|
||||
EConfigType.HTTP -> HttpFmt.toOutbound(profileItem)
|
||||
EConfigType.POLICYGROUP -> null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an initial outbound configuration for a specific protocol type.
|
||||
*
|
||||
* Provides a template configuration for different protocol types.
|
||||
*
|
||||
* @param configType The type of configuration to create
|
||||
* @return An initial OutboundBean for the specified configuration type, or null for custom types
|
||||
*/
|
||||
fun createInitOutbound(configType: EConfigType): OutboundBean? {
|
||||
return when (configType) {
|
||||
EConfigType.VMESS,
|
||||
EConfigType.VLESS ->
|
||||
return OutboundBean(
|
||||
protocol = configType.name.lowercase(),
|
||||
settings = OutSettingsBean(
|
||||
vnext = listOf(
|
||||
OutSettingsBean.VnextBean(
|
||||
users = listOf(OutSettingsBean.VnextBean.UsersBean())
|
||||
)
|
||||
)
|
||||
),
|
||||
streamSettings = StreamSettingsBean()
|
||||
)
|
||||
|
||||
EConfigType.SHADOWSOCKS,
|
||||
EConfigType.SOCKS,
|
||||
EConfigType.HTTP,
|
||||
EConfigType.TROJAN ->
|
||||
return OutboundBean(
|
||||
protocol = configType.name.lowercase(),
|
||||
settings = OutSettingsBean(
|
||||
servers = listOf(OutSettingsBean.ServersBean())
|
||||
),
|
||||
streamSettings = StreamSettingsBean()
|
||||
)
|
||||
|
||||
EConfigType.WIREGUARD ->
|
||||
return OutboundBean(
|
||||
protocol = configType.name.lowercase(),
|
||||
settings = OutSettingsBean(
|
||||
secretKey = "",
|
||||
peers = listOf(OutSettingsBean.WireGuardBean())
|
||||
)
|
||||
)
|
||||
|
||||
EConfigType.HYSTERIA,
|
||||
EConfigType.HYSTERIA2 ->
|
||||
return OutboundBean(
|
||||
protocol = EConfigType.HYSTERIA.name.lowercase(),
|
||||
settings = OutSettingsBean(
|
||||
servers = null
|
||||
),
|
||||
streamSettings = StreamSettingsBean()
|
||||
)
|
||||
|
||||
EConfigType.CUSTOM -> null
|
||||
EConfigType.POLICYGROUP -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures transport settings for an outbound connection.
|
||||
*
|
||||
* Sets up protocol-specific transport options based on the profile settings.
|
||||
*
|
||||
* @param streamSettings The stream settings to configure
|
||||
* @param profileItem The profile containing transport configuration
|
||||
* @return The Server Name Indication (SNI) value to use, or null if not applicable
|
||||
*/
|
||||
fun populateTransportSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem): String? {
|
||||
val transport = profileItem.network.orEmpty()
|
||||
val headerType = profileItem.headerType
|
||||
val host = profileItem.host
|
||||
val path = profileItem.path
|
||||
val seed = profileItem.seed
|
||||
// val quicSecurity = profileItem.quicSecurity
|
||||
// val key = profileItem.quicKey
|
||||
val mode = profileItem.mode
|
||||
val serviceName = profileItem.serviceName
|
||||
val authority = profileItem.authority
|
||||
val xhttpMode = profileItem.xhttpMode
|
||||
val xhttpExtra = profileItem.xhttpExtra
|
||||
val finalMask = profileItem.finalMask
|
||||
var sni: String? = null
|
||||
streamSettings.network = transport.ifEmpty { NetworkType.TCP.type }
|
||||
when (streamSettings.network) {
|
||||
NetworkType.TCP.type -> {
|
||||
val tcpSetting = StreamSettingsBean.TcpSettingsBean()
|
||||
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
||||
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
||||
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
||||
val requestObj = StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean()
|
||||
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
tcpSetting.header.request = requestObj
|
||||
sni = requestObj.headers.Host?.getOrNull(0)
|
||||
}
|
||||
} else {
|
||||
tcpSetting.header.type = "none"
|
||||
sni = host
|
||||
}
|
||||
streamSettings.tcpSettings = tcpSetting
|
||||
}
|
||||
|
||||
NetworkType.KCP.type -> {
|
||||
val kcpSetting = StreamSettingsBean.KcpSettingsBean()
|
||||
profileItem.kcpMtu?.let { kcpSetting.mtu = it }
|
||||
profileItem.kcpTti?.let { kcpSetting.tti = it }
|
||||
streamSettings.kcpSettings = kcpSetting
|
||||
val udpMaskList = mutableListOf<StreamSettingsBean.FinalMaskBean.MaskBean>()
|
||||
if (!headerType.isNullOrEmpty() && headerType != "none") {
|
||||
val kcpHeaderType = when {
|
||||
headerType == "wechat-video" -> "header-wechat"
|
||||
else -> "header-$headerType"
|
||||
}
|
||||
udpMaskList.add(
|
||||
StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = kcpHeaderType,
|
||||
settings = if (headerType == "dns" && !host.isNullOrEmpty()) {
|
||||
StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
domain = host
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
if (seed.isNullOrEmpty()) {
|
||||
udpMaskList.add(
|
||||
StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "mkcp-original"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
udpMaskList.add(
|
||||
StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "mkcp-aes128gcm",
|
||||
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
password = seed
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
streamSettings.finalmask = StreamSettingsBean.FinalMaskBean(
|
||||
udp = udpMaskList.toList()
|
||||
)
|
||||
}
|
||||
|
||||
NetworkType.WS.type -> {
|
||||
val wssetting = StreamSettingsBean.WsSettingsBean()
|
||||
wssetting.headers.Host = host.orEmpty()
|
||||
sni = host
|
||||
wssetting.path = path ?: "/"
|
||||
streamSettings.wsSettings = wssetting
|
||||
}
|
||||
|
||||
NetworkType.HTTP_UPGRADE.type -> {
|
||||
val httpupgradeSetting = StreamSettingsBean.HttpupgradeSettingsBean()
|
||||
httpupgradeSetting.host = host.orEmpty()
|
||||
sni = host
|
||||
httpupgradeSetting.path = path ?: "/"
|
||||
streamSettings.httpupgradeSettings = httpupgradeSetting
|
||||
}
|
||||
|
||||
NetworkType.XHTTP.type -> {
|
||||
val xhttpSetting = StreamSettingsBean.XhttpSettingsBean()
|
||||
xhttpSetting.host = host.orEmpty()
|
||||
sni = host
|
||||
xhttpSetting.path = path ?: "/"
|
||||
xhttpSetting.mode = xhttpMode
|
||||
xhttpSetting.extra = JsonUtil.parseString(xhttpExtra)
|
||||
streamSettings.xhttpSettings = xhttpSetting
|
||||
}
|
||||
|
||||
NetworkType.H2.type, NetworkType.HTTP.type -> {
|
||||
streamSettings.network = NetworkType.H2.type
|
||||
val h2Setting = StreamSettingsBean.HttpSettingsBean()
|
||||
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
sni = h2Setting.host.getOrNull(0)
|
||||
h2Setting.path = path ?: "/"
|
||||
streamSettings.httpSettings = h2Setting
|
||||
}
|
||||
|
||||
// "quic" -> {
|
||||
// val quicsetting = QuicSettingBean()
|
||||
// quicsetting.security = quicSecurity ?: "none"
|
||||
// quicsetting.key = key.orEmpty()
|
||||
// quicsetting.header.type = headerType ?: "none"
|
||||
// quicSettings = quicsetting
|
||||
// }
|
||||
|
||||
NetworkType.GRPC.type -> {
|
||||
val grpcSetting = StreamSettingsBean.GrpcSettingsBean()
|
||||
grpcSetting.multiMode = mode == "multi"
|
||||
grpcSetting.serviceName = serviceName.orEmpty()
|
||||
grpcSetting.authority = authority.orEmpty()
|
||||
grpcSetting.idle_timeout = 60
|
||||
grpcSetting.health_check_timeout = 20
|
||||
sni = authority
|
||||
streamSettings.grpcSettings = grpcSetting
|
||||
}
|
||||
|
||||
NetworkType.HYSTERIA.type -> {
|
||||
val hysteriaSetting = StreamSettingsBean.HysteriaSettingsBean(
|
||||
version = 2,
|
||||
auth = profileItem.password.orEmpty(),
|
||||
)
|
||||
val quicParams = StreamSettingsBean.FinalMaskBean.QuicParamsBean(
|
||||
brutalUp = profileItem.bandwidthUp?.nullIfBlank(),
|
||||
brutalDown = profileItem.bandwidthDown?.nullIfBlank(),
|
||||
)
|
||||
quicParams.congestion = if (quicParams.brutalUp != null || quicParams.brutalDown != null) "brutal" else null
|
||||
if (profileItem.portHopping.isNotNullEmpty()) {
|
||||
val rawInterval = profileItem.portHoppingInterval?.trim().nullIfBlank()
|
||||
val interval = if (rawInterval == null) {
|
||||
"30"
|
||||
} else {
|
||||
val singleValue = rawInterval.toIntOrNull()
|
||||
if (singleValue != null) {
|
||||
if (singleValue < 5) {
|
||||
"30"
|
||||
} else {
|
||||
rawInterval
|
||||
}
|
||||
} else {
|
||||
val parts = rawInterval.split('-')
|
||||
if (parts.size == 2) {
|
||||
val start = parts[0].trim().toIntOrNull()
|
||||
val end = parts[1].trim().toIntOrNull()
|
||||
if (start != null && end != null) {
|
||||
val minStart = maxOf(5, start)
|
||||
val minEnd = maxOf(minStart, end)
|
||||
"$minStart-$minEnd"
|
||||
} else {
|
||||
"30"
|
||||
}
|
||||
} else {
|
||||
"30"
|
||||
}
|
||||
}
|
||||
}
|
||||
quicParams.udpHop = StreamSettingsBean.FinalMaskBean.QuicParamsBean.UdpHopBean(
|
||||
ports = profileItem.portHopping,
|
||||
interval = interval
|
||||
)
|
||||
}
|
||||
val finalmask = StreamSettingsBean.FinalMaskBean(
|
||||
quicParams = quicParams
|
||||
)
|
||||
if (profileItem.obfsPassword.isNotNullEmpty()) {
|
||||
finalmask.udp = listOf(
|
||||
StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "salamander",
|
||||
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
password = profileItem.obfsPassword.orEmpty()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
streamSettings.hysteriaSettings = hysteriaSetting
|
||||
streamSettings.finalmask = finalmask
|
||||
}
|
||||
}
|
||||
finalMask?.let {
|
||||
val parsedFinalMask = JsonUtil.parseString(finalMask)
|
||||
if (parsedFinalMask != null) {
|
||||
streamSettings.finalmask = parsedFinalMask
|
||||
} else {
|
||||
LogUtil.w("V2rayConfigManager", "Invalid finalMask JSON, keeping previously generated finalmask")
|
||||
}
|
||||
}
|
||||
return sni
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures TLS or REALITY security settings for an outbound connection.
|
||||
*
|
||||
* Sets up security-related parameters like certificates, fingerprints, and SNI.
|
||||
*
|
||||
* @param streamSettings The stream settings to configure
|
||||
* @param profileItem The profile containing security configuration
|
||||
* @param sniExt An external SNI value to use if the profile doesn't specify one
|
||||
*/
|
||||
fun populateTlsSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) {
|
||||
val streamSecurity = profileItem.security.orEmpty()
|
||||
val allowInsecure = profileItem.insecure == true
|
||||
val sni = if (profileItem.sni.isNullOrEmpty()) {
|
||||
when {
|
||||
sniExt.isNotNullEmpty() && Utils.isDomainName(sniExt) -> sniExt
|
||||
profileItem.server.isNotNullEmpty() && Utils.isDomainName(profileItem.server) -> profileItem.server
|
||||
else -> sniExt
|
||||
}
|
||||
} else {
|
||||
profileItem.sni
|
||||
}
|
||||
|
||||
streamSettings.security = streamSecurity.nullIfBlank()
|
||||
if (streamSettings.security == null) return
|
||||
val tlsSetting = StreamSettingsBean.TlsSettingsBean(
|
||||
allowInsecure = allowInsecure,
|
||||
serverName = sni.nullIfBlank(),
|
||||
fingerprint = profileItem.fingerPrint.nullIfBlank(),
|
||||
alpn = profileItem.alpn?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }.takeIf { !it.isNullOrEmpty() },
|
||||
echConfigList = profileItem.echConfigList.nullIfBlank(),
|
||||
pinnedPeerCertSha256 = profileItem.pinnedCA256.nullIfBlank(),
|
||||
publicKey = profileItem.publicKey.nullIfBlank(),
|
||||
shortId = profileItem.shortId.nullIfBlank(),
|
||||
spiderX = profileItem.spiderX.nullIfBlank(),
|
||||
mldsa65Verify = profileItem.mldsa65Verify.nullIfBlank(),
|
||||
)
|
||||
if (streamSettings.security == AppConfig.TLS) {
|
||||
streamSettings.tlsSettings = tlsSetting
|
||||
streamSettings.realitySettings = null
|
||||
} else if (streamSettings.security == AppConfig.REALITY) {
|
||||
streamSettings.tlsSettings = null
|
||||
streamSettings.realitySettings = tlsSetting
|
||||
}
|
||||
|
||||
if (profileItem.finalMask.isNullOrEmpty()) {
|
||||
updateOutboundFragment(streamSettings)
|
||||
}
|
||||
private fun convertProfile2Outbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
|
||||
return CoreOutboundBuilder.convert(profileItem)
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user