From d9d00f4137a200e1c344971e83e6e4b2a940e7cb Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Fri, 1 May 2026 17:06:53 +0800 Subject: [PATCH] Add CoreOutboundBuilder to build OutboundBeans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../com/v2ray/ang/core/CoreOutboundBuilder.kt | 635 ++++++++++++++++++ .../main/java/com/v2ray/ang/fmt/FmtBase.kt | 17 - .../main/java/com/v2ray/ang/fmt/HttpFmt.kt | 32 - .../java/com/v2ray/ang/fmt/Hysteria2Fmt.kt | 30 - .../java/com/v2ray/ang/fmt/ShadowsocksFmt.kt | 29 - .../main/java/com/v2ray/ang/fmt/SocksFmt.kt | 25 - .../main/java/com/v2ray/ang/fmt/TrojanFmt.kt | 29 - .../main/java/com/v2ray/ang/fmt/VlessFmt.kt | 29 - .../main/java/com/v2ray/ang/fmt/VmessFmt.kt | 28 - .../java/com/v2ray/ang/fmt/WireguardFmt.kt | 30 +- .../v2ray/ang/handler/V2rayConfigManager.kt | 476 +------------ 11 files changed, 658 insertions(+), 702 deletions(-) create mode 100644 V2rayNG/app/src/main/java/com/v2ray/ang/core/CoreOutboundBuilder.kt delete mode 100644 V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/core/CoreOutboundBuilder.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/core/CoreOutboundBuilder.kt new file mode 100644 index 00000000..400ace5b --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/core/CoreOutboundBuilder.kt @@ -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() + 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() + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt index ac636289..f2e19c95 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt @@ -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() - } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt deleted file mode 100644 index d268f2e3..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt index ce5b6fd0..93ebad08 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt @@ -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 - } } \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt index 39ec0109..76cc8083 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt @@ -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 - } } \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt index 40a36ff8..ba54a685 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt @@ -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 - } } \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt index f5de509d..877c360e 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt @@ -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 - } } \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt index ca564a44..07dc0182 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt @@ -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 - } } \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt index 065519f3..093230f0 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt @@ -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 - } } \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt index be27c3d0..cf03d4b8 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt @@ -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. diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt index 43564ce3..2ffb2a90 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt @@ -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() + val outboundsList = mutableListOf() 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() - 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 -} +} \ No newline at end of file