diff --git a/AndroidLibXrayLite b/AndroidLibXrayLite index 2715a3b1..8ad3e1dd 160000 --- a/AndroidLibXrayLite +++ b/AndroidLibXrayLite @@ -1 +1 @@ -Subproject commit 2715a3b110f64e039a4b8d2c8ca0cb5a9a6b0958 +Subproject commit 8ad3e1ddf165d8d67e488346b2faa9153d3e33a4 diff --git a/README.md b/README.md index 83214e02..4bd6f8ec 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,12 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core) [![API](https://img.shields.io/badge/API-21%2B-yellow.svg?style=flat)](https://developer.android.com/about/versions/lollipop) -[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.1.20-blue.svg)](https://kotlinlang.org) +[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.1.21-blue.svg)](https://kotlinlang.org) [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master) [![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayng/badge)](https://www.codefactor.io/repository/github/2dust/v2rayng) [![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases) [![Chat on Telegram](https://img.shields.io/badge/Chat%20on-Telegram-brightgreen.svg)](https://t.me/v2rayn) - -Get it on Google Play - - ### Telegram Channel [github_2dust](https://t.me/github_2dust) diff --git a/V2rayNG/app/build.gradle.kts b/V2rayNG/app/build.gradle.kts index d97ae358..1624786c 100644 --- a/V2rayNG/app/build.gradle.kts +++ b/V2rayNG/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.v2ray.ang" minSdk = 21 targetSdk = 35 - versionCode = 646 - versionName = "1.9.46" + versionCode = 658 + versionName = "1.10.8" multiDexEnabled = true val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml index 4ff08af9..00e4b747 100644 --- a/V2rayNG/app/src/main/AndroidManifest.xml +++ b/V2rayNG/app/src/main/AndroidManifest.xml @@ -144,6 +144,9 @@ + diff --git a/V2rayNG/app/src/main/assets/v2ray_config.json b/V2rayNG/app/src/main/assets/v2ray_config.json index 90abcee0..4f8c3d7e 100644 --- a/V2rayNG/app/src/main/assets/v2ray_config.json +++ b/V2rayNG/app/src/main/assets/v2ray_config.json @@ -97,7 +97,7 @@ } ], "routing": { - "domainStrategy": "IPIfNonMatch", + "domainStrategy": "AsIs", "rules": [] }, "dns": { diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt index 0a2a2946..09e3a9d5 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt @@ -26,6 +26,7 @@ object AppConfig { const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port" const val PREF_VPN_DNS = "pref_vpn_dns" const val PREF_VPN_BYPASS_LAN = "pref_vpn_bypass_lan" + const val PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX = "pref_vpn_interface_address_config_index" const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy" const val PREF_ROUTING_RULESET = "pref_routing_ruleset" const val PREF_MUX_ENABLED = "pref_mux_enabled" @@ -55,6 +56,7 @@ object AppConfig { const val PREF_DNS_HOSTS = "pref_dns_hosts" const val PREF_DELAY_TEST_URL = "pref_delay_test_url" const val PREF_LOGLEVEL = "pref_core_loglevel" + const val PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD = "pref_outbound_domain_resolve_method" const val PREF_MODE = "pref_mode" const val PREF_IS_BOOTED = "pref_is_booted" const val PREF_CHECK_UPDATE_PRE_RELEASE = "pref_check_update_pre_release" @@ -103,6 +105,7 @@ object AppConfig { const val TG_CHANNEL_URL = "https://t.me/github_2dust" const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204" const val DELAY_TEST_URL2 = "https://www.google.com/generate_204" + const val IP_API_URL = "https://speed.cloudflare.com/meta" /** DNS server addresses. */ const val DNS_PROXY = "1.1.1.1" @@ -167,7 +170,9 @@ object AppConfig { // Android Private DNS constants const val DNS_DNSPOD_DOMAIN = "dot.pub" const val DNS_ALIDNS_DOMAIN = "dns.alidns.com" - const val DNS_CLOUDFLARE_DOMAIN = "one.one.one.one" + const val DNS_CLOUDFLARE_ONE_DOMAIN = "one.one.one.one" + const val DNS_CLOUDFLARE_DNS_COM_DOMAIN = "dns.cloudflare.com" + const val DNS_CLOUDFLARE_DNS_DOMAIN = "cloudflare-dns.com" const val DNS_GOOGLE_DOMAIN = "dns.google" const val DNS_QUAD9_DOMAIN = "dns.quad9.net" const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net" @@ -181,14 +186,16 @@ object AppConfig { const val HEADER_TYPE_HTTP = "http" val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1") - val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001") + val DNS_CLOUDFLARE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001") + val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5") + val DNS_CLOUDFLARE_DNS_ADDRESSES = arrayListOf("104.16.248.249", "104.16.249.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9") val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53") val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844") val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9") val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff") //minimum list https://serverfault.com/a/304791 - val BYPASS_PRIVATE_IP_LIST = arrayListOf( + val ROUTED_IP_LIST = arrayListOf( "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", @@ -223,7 +230,9 @@ object AppConfig { ) val PRIVATE_IP_LIST = arrayListOf( + "0.0.0.0/8", "10.0.0.0/8", + "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt index c0b70c6a..c8870248 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt @@ -4,6 +4,6 @@ data class ConfigResult( var status: Boolean, var guid: String? = null, var content: String = "", - var domainPort: String? = null, + var socksPort: Int? = null, ) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt new file mode 100644 index 00000000..97814fbb --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt @@ -0,0 +1,12 @@ +package com.v2ray.ang.dto + +data class IPAPIInfo( + var ip: String? = null, + var clientIp: String? = null, + var ip_addr: String? = null, + var query: String? = null, + var country: String? = null, + var country_name: String? = null, + var country_code: String? = null, + var countryCode: String? = null +) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileLiteItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileLiteItem.kt deleted file mode 100644 index 12995abd..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileLiteItem.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.v2ray.ang.dto - -data class ProfileLiteItem( - val configType: EConfigType, - var subscriptionId: String = "", - var remarks: String = "", - var server: String?, - var serverPort: Int?, -) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt index 8e8c66a4..8957df78 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt @@ -11,5 +11,6 @@ data class SubscriptionItem( var prevProfile: String? = null, var nextProfile: String? = null, var filter: String? = null, + var allowInsecureUrl: Boolean = false, ) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt index 692caf69..155be104 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt @@ -1,25 +1,14 @@ package com.v2ray.ang.dto -import android.text.TextUtils -import com.google.gson.GsonBuilder -import com.google.gson.JsonPrimitive -import com.google.gson.JsonSerializationContext -import com.google.gson.JsonSerializer import com.google.gson.annotations.SerializedName -import com.google.gson.reflect.TypeToken import com.v2ray.ang.AppConfig -import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.ServersBean -import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean -import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean -import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean import com.v2ray.ang.util.Utils -import java.lang.reflect.Type data class V2rayConfig( var remarks: String? = null, var stats: Any? = null, val log: LogBean, - var policy: PolicyBean?, + var policy: PolicyBean? = null, val inbounds: ArrayList, var outbounds: ArrayList, var dns: DnsBean? = null, @@ -34,9 +23,9 @@ data class V2rayConfig( ) { data class LogBean( - val access: String, - val error: String, - var loglevel: String?, + val access: String? = null, + val error: String? = null, + var loglevel: String? = null, val dnsLog: Boolean? = null ) @@ -46,7 +35,7 @@ data class V2rayConfig( var protocol: String, var listen: String? = null, val settings: Any? = null, - val sniffing: SniffingBean?, + val sniffing: SniffingBean? = null, val streamSettings: Any? = null, val allocate: Any? = null ) { @@ -77,50 +66,6 @@ data class V2rayConfig( val sendThrough: String? = null, var mux: MuxBean? = MuxBean(false) ) { - companion object { - fun create(configType: EConfigType): OutboundBean? { - return when (configType) { - EConfigType.VMESS, - EConfigType.VLESS -> - return OutboundBean( - protocol = configType.name.lowercase(), - settings = OutSettingsBean( - vnext = listOf( - VnextBean( - users = listOf(UsersBean()) - ) - ) - ), - streamSettings = StreamSettingsBean() - ) - - EConfigType.SHADOWSOCKS, - EConfigType.SOCKS, - EConfigType.HTTP, - EConfigType.TROJAN, - EConfigType.HYSTERIA2 -> - return OutboundBean( - protocol = configType.name.lowercase(), - settings = OutSettingsBean( - servers = listOf(ServersBean()) - ), - streamSettings = StreamSettingsBean() - ) - - EConfigType.WIREGUARD -> - return OutboundBean( - protocol = configType.name.lowercase(), - settings = OutSettingsBean( - secretKey = "", - peers = listOf(WireGuardBean()) - ) - ) - - EConfigType.CUSTOM -> null - } - } - } - data class OutSettingsBean( var vnext: List? = null, var fragment: FragmentBean? = null, @@ -197,7 +142,7 @@ data class V2rayConfig( data class WireGuardBean( var publicKey: String = "", - var preSharedKey: String = "", + var preSharedKey: String? = null, var endpoint: String = "" ) } @@ -299,7 +244,8 @@ data class V2rayConfig( var tcpFastOpen: Boolean? = null, var tproxy: String? = null, var mark: Int? = null, - var dialerProxy: String? = null + var dialerProxy: String? = null, + var domainStrategy: String? = null ) data class TlsSettingsBean( @@ -349,139 +295,6 @@ data class V2rayConfig( ) } - fun populateTransportSettings( - transport: String, - headerType: String?, - host: String?, - path: String?, - seed: String?, - quicSecurity: String?, - key: String?, - mode: String?, - serviceName: String?, - authority: String? - ): String? { - var sni: String? = null - network = if (transport.isEmpty()) NetworkType.TCP.type else transport - when (network) { - NetworkType.TCP.type -> { - val tcpSetting = TcpSettingsBean() - if (headerType == AppConfig.HEADER_TYPE_HTTP) { - tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP - if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) { - val requestObj = 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 - } - tcpSettings = tcpSetting - } - - NetworkType.KCP.type -> { - val kcpsetting = KcpSettingsBean() - kcpsetting.header.type = headerType ?: "none" - if (seed.isNullOrEmpty()) { - kcpsetting.seed = null - } else { - kcpsetting.seed = seed - } - if (host.isNullOrEmpty()) { - kcpsetting.header.domain = null - } else { - kcpsetting.header.domain = host - } - kcpSettings = kcpsetting - } - - NetworkType.WS.type -> { - val wssetting = WsSettingsBean() - wssetting.headers.Host = host.orEmpty() - sni = host - wssetting.path = path ?: "/" - wsSettings = wssetting - } - - NetworkType.HTTP_UPGRADE.type -> { - val httpupgradeSetting = HttpupgradeSettingsBean() - httpupgradeSetting.host = host.orEmpty() - sni = host - httpupgradeSetting.path = path ?: "/" - httpupgradeSettings = httpupgradeSetting - } - - NetworkType.XHTTP.type -> { - val xhttpSetting = XhttpSettingsBean() - xhttpSetting.host = host.orEmpty() - sni = host - xhttpSetting.path = path ?: "/" - xhttpSettings = xhttpSetting - } - - NetworkType.H2.type, NetworkType.HTTP.type -> { - network = NetworkType.H2.type - val h2Setting = HttpSettingsBean() - h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() } - sni = h2Setting.host.getOrNull(0) - h2Setting.path = path ?: "/" - 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 = GrpcSettingsBean() - grpcSetting.multiMode = mode == "multi" - grpcSetting.serviceName = serviceName.orEmpty() - grpcSetting.authority = authority.orEmpty() - grpcSetting.idle_timeout = 60 - grpcSetting.health_check_timeout = 20 - sni = authority - grpcSettings = grpcSetting - } - } - return sni - } - - fun populateTlsSettings( - streamSecurity: String, - allowInsecure: Boolean, - sni: String?, - fingerprint: String?, - alpns: String?, - publicKey: String?, - shortId: String?, - spiderX: String? - ) { - security = if (streamSecurity.isEmpty()) null else streamSecurity - if (security == null) return - val tlsSetting = TlsSettingsBean( - allowInsecure = allowInsecure, - serverName = if (sni.isNullOrEmpty()) null else sni, - fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint, - alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() }, - publicKey = if (publicKey.isNullOrEmpty()) null else publicKey, - shortId = if (shortId.isNullOrEmpty()) null else shortId, - spiderX = if (spiderX.isNullOrEmpty()) null else spiderX, - ) - if (security == AppConfig.TLS) { - tlsSettings = tlsSetting - realitySettings = null - } else if (security == AppConfig.REALITY) { - tlsSettings = null - realitySettings = tlsSetting - } - } } data class MuxBean( @@ -647,6 +460,18 @@ data class V2rayConfig( } return null } + + fun ensureSockopt(): V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean { + val stream = streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean().also { + streamSettings = it + } + + val sockopt = stream.sockopt ?: V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean().also { + stream.sockopt = it + } + + return sockopt + } } data class DnsBean( @@ -723,15 +548,9 @@ data class V2rayConfig( return null } - fun toPrettyPrinting(): String { - return GsonBuilder() - .setPrettyPrinting() - .disableHtmlEscaping() - .registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start - object : TypeToken() {}.type, - JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) } - ) - .create() - .toJson(this) + fun getAllProxyOutbound(): List { + return outbounds.filter { outbound -> + EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) } + } } -} \ No newline at end of file +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt new file mode 100644 index 00000000..6b7bc379 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt @@ -0,0 +1,39 @@ +package com.v2ray.ang.dto + +/** + * VPN interface address configuration enum class + * Defines predefined IPv4 and IPv6 address pairs for VPN TUN interface configuration. + * Each option provides client and router addresses to establish point-to-point VPN tunnels. + */ +enum class VpnInterfaceAddressConfig( + val displayName: String, + val ipv4Client: String, + val ipv4Router: String, + val ipv6Client: String, + val ipv6Router: String +) { + OPTION_1("10.10.14.x", "10.10.14.1", "10.10.14.2", "fc00::10:10:14:1", "fc00::10:10:14:2"), + OPTION_2("10.1.0.x", "10.1.0.1", "10.1.0.2", "fc00::10:1:0:1", "fc00::10:1:0:2"), + OPTION_3("10.0.0.x", "10.0.0.1", "10.0.0.2", "fc00::10:0:0:1", "fc00::10:0:0:2"), + OPTION_4("172.31.0.x", "172.31.0.1", "172.31.0.2", "fc00::172:31:0:1", "fc00::172:31:0:2"), + OPTION_5("172.20.0.x", "172.20.0.1", "172.20.0.2", "fc00::172:20:0:1", "fc00::172:20:0:2"), + OPTION_6("172.16.0.x", "172.16.0.1", "172.16.0.2", "fc00::172:16:0:1", "fc00::172:16:0:2"), + OPTION_7("192.168.100.x", "192.168.100.1", "192.168.100.2", "fc00::192:168:100:1", "fc00::192:168:100:2"); + + companion object { + /** + * Retrieves the VPN interface address configuration based on the specified index. + * + * @param index The configuration index (0-based) corresponding to user selection + * @return The VpnInterfaceAddressConfig instance at the specified index, + * or OPTION_1 (default) if the index is out of bounds + */ + fun getConfigByIndex(index: Int): VpnInterfaceAddressConfig { + return if (index in values().indices) { + values()[index] + } else { + OPTION_1 // Default to the first configuration + } + } + } +} 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 407e5c37..73cdf958 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 @@ -4,6 +4,8 @@ import com.v2ray.ang.AppConfig import com.v2ray.ang.dto.NetworkType import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.util.HttpUtil import com.v2ray.ang.util.Utils import java.net.URI @@ -26,7 +28,7 @@ open class FmtBase { val url = String.format( "%s@%s:%s", Utils.urlEncode(userInfo ?: ""), - Utils.getIpv6Address(config.server), + Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())), config.serverPort ) @@ -148,4 +150,21 @@ 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 index faac0230..8c641f24 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt @@ -4,6 +4,7 @@ import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.handler.V2rayConfigManager object HttpFmt : FmtBase() { /** @@ -13,10 +14,10 @@ object HttpFmt : FmtBase() { * @return the converted OutboundBean object, or null if conversion fails */ fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = OutboundBean.create(EConfigType.HTTP) + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP) outboundBean?.settings?.servers?.first()?.let { server -> - server.address = profileItem.server.orEmpty() + server.address = getServerAddress(profileItem) server.port = profileItem.serverPort.orEmpty().toInt() if (profileItem.username.isNotNullEmpty()) { val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() 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 7a6abe08..3b3dc88c 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 @@ -9,6 +9,7 @@ import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.V2rayConfigManager import com.v2ray.ang.util.Utils import java.net.URI @@ -24,7 +25,7 @@ object Hysteria2Fmt : FmtBase() { val config = ProfileItem.create(EConfigType.HYSTERIA2) val uri = URI(Utils.fixIllegalUrl(str)) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } config.server = uri.idnHost config.serverPort = uri.port.toString() config.password = uri.userInfo @@ -144,7 +145,7 @@ object Hysteria2Fmt : FmtBase() { * @return the converted OutboundBean object, or null if conversion fails */ fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2) + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2) 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 fb383d25..87ba74f8 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 @@ -7,6 +7,7 @@ import com.v2ray.ang.dto.NetworkType import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.handler.V2rayConfigManager import com.v2ray.ang.util.Utils import java.net.URI @@ -35,7 +36,7 @@ object ShadowsocksFmt : FmtBase() { if (uri.port <= 0) return null if (uri.userInfo.isNullOrEmpty()) return null - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } config.server = uri.idnHost config.serverPort = uri.port.toString() @@ -131,38 +132,22 @@ object ShadowsocksFmt : FmtBase() { * @return the converted OutboundBean object, or null if conversion fails */ fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS) + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS) outboundBean?.settings?.servers?.first()?.let { server -> - server.address = profileItem.server.orEmpty() + server.address = getServerAddress(profileItem) server.port = profileItem.serverPort.orEmpty().toInt() server.password = profileItem.password server.method = profileItem.method } - val sni = outboundBean?.streamSettings?.populateTransportSettings( - profileItem.network.orEmpty(), - profileItem.headerType, - profileItem.host, - profileItem.path, - profileItem.seed, - profileItem.quicSecurity, - profileItem.quicKey, - profileItem.mode, - profileItem.serviceName, - profileItem.authority, - ) + val sni = outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTransportSettings(it, profileItem) + } - outboundBean?.streamSettings?.populateTlsSettings( - profileItem.security.orEmpty(), - profileItem.insecure == true, - if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni, - profileItem.fingerPrint, - profileItem.alpn, - profileItem.publicKey, - profileItem.shortId, - profileItem.spiderX, - ) + outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTlsSettings(it, profileItem, sni) + } return outboundBean } 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 f37030c2..30bc08e4 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 @@ -5,6 +5,7 @@ import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.V2rayConfig.OutboundBean 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 @@ -22,7 +23,7 @@ object SocksFmt : FmtBase() { if (uri.idnHost.isEmpty()) return null if (uri.port <= 0) return null - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } config.server = uri.idnHost config.serverPort = uri.port.toString() @@ -60,10 +61,10 @@ object SocksFmt : FmtBase() { * @return the converted OutboundBean object, or null if conversion fails */ fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = OutboundBean.create(EConfigType.SOCKS) + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS) outboundBean?.settings?.servers?.first()?.let { server -> - server.address = profileItem.server.orEmpty() + server.address = getServerAddress(profileItem) server.port = profileItem.serverPort.orEmpty().toInt() if (profileItem.username.isNotNullEmpty()) { val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() 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 56883cd8..446ef99c 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 @@ -7,6 +7,7 @@ import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.V2rayConfig.OutboundBean 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 @@ -22,7 +23,7 @@ object TrojanFmt : FmtBase() { val config = ProfileItem.create(EConfigType.TROJAN) val uri = URI(Utils.fixIllegalUrl(str)) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } config.server = uri.idnHost config.serverPort = uri.port.toString() config.password = uri.userInfo @@ -60,38 +61,22 @@ object TrojanFmt : FmtBase() { * @return the converted OutboundBean object, or null if conversion fails */ fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = OutboundBean.create(EConfigType.TROJAN) + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN) outboundBean?.settings?.servers?.first()?.let { server -> - server.address = profileItem.server.orEmpty() + server.address = getServerAddress(profileItem) server.port = profileItem.serverPort.orEmpty().toInt() server.password = profileItem.password server.flow = profileItem.flow } - val sni = outboundBean?.streamSettings?.populateTransportSettings( - profileItem.network.orEmpty(), - profileItem.headerType, - profileItem.host, - profileItem.path, - profileItem.seed, - profileItem.quicSecurity, - profileItem.quicKey, - profileItem.mode, - profileItem.serviceName, - profileItem.authority, - ) + val sni = outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTransportSettings(it, profileItem) + } - outboundBean?.streamSettings?.populateTlsSettings( - profileItem.security.orEmpty(), - profileItem.insecure == true, - if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni, - profileItem.fingerPrint, - profileItem.alpn, - profileItem.publicKey, - profileItem.shortId, - profileItem.spiderX, - ) + outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTlsSettings(it, profileItem, sni) + } return outboundBean } 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 77d2f2a3..9242f0ec 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 @@ -6,7 +6,7 @@ import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.extension.idnHost import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.handler.V2rayConfigManager import com.v2ray.ang.util.Utils import java.net.URI @@ -26,7 +26,7 @@ object VlessFmt : FmtBase() { if (uri.rawQuery.isNullOrEmpty()) return null val queryParam = getQueryParam(uri) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } config.server = uri.idnHost config.serverPort = uri.port.toString() config.password = uri.userInfo @@ -57,41 +57,23 @@ object VlessFmt : FmtBase() { * @return the converted OutboundBean object, or null if conversion fails */ fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = OutboundBean.create(EConfigType.VLESS) + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS) outboundBean?.settings?.vnext?.first()?.let { vnext -> - vnext.address = profileItem.server.orEmpty() + 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?.populateTransportSettings( - profileItem.network.orEmpty(), - profileItem.headerType, - profileItem.host, - profileItem.path, - profileItem.seed, - profileItem.quicSecurity, - profileItem.quicKey, - profileItem.mode, - profileItem.serviceName, - profileItem.authority, - ) - outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode - outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra) + val sni = outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTransportSettings(it, profileItem) + } - outboundBean?.streamSettings?.populateTlsSettings( - profileItem.security.orEmpty(), - profileItem.insecure == true, - if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni, - profileItem.fingerPrint, - profileItem.alpn, - profileItem.publicKey, - profileItem.shortId, - profileItem.spiderX, - ) + outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTlsSettings(it, profileItem, sni) + } return outboundBean } 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 bbc913d3..4201f4dc 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 @@ -11,6 +11,7 @@ import com.v2ray.ang.dto.VmessQRCode import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.V2rayConfigManager import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.Utils import java.net.URI @@ -150,7 +151,7 @@ object VmessFmt : FmtBase() { if (uri.rawQuery.isNullOrEmpty()) return null val queryParam = getQueryParam(uri) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } config.server = uri.idnHost config.serverPort = uri.port.toString() config.password = uri.userInfo @@ -168,38 +169,22 @@ object VmessFmt : FmtBase() { * @return the converted OutboundBean object, or null if conversion fails */ fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = OutboundBean.create(EConfigType.VMESS) + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS) outboundBean?.settings?.vnext?.first()?.let { vnext -> - vnext.address = profileItem.server.orEmpty() + 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?.populateTransportSettings( - profileItem.network.orEmpty(), - profileItem.headerType, - profileItem.host, - profileItem.path, - profileItem.seed, - profileItem.quicSecurity, - profileItem.quicKey, - profileItem.mode, - profileItem.serviceName, - profileItem.authority, - ) + val sni = outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTransportSettings(it, profileItem) + } - outboundBean?.streamSettings?.populateTlsSettings( - profileItem.security.orEmpty(), - profileItem.insecure == true, - if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni, - profileItem.fingerPrint, - profileItem.alpn, - null, - null, - null - ) + outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTlsSettings(it, profileItem, sni) + } return outboundBean } 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 3d58ad40..8f1cec84 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 @@ -7,6 +7,7 @@ import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.removeWhiteSpace +import com.v2ray.ang.handler.V2rayConfigManager import com.v2ray.ang.util.Utils import java.net.URI @@ -24,14 +25,14 @@ object WireguardFmt : FmtBase() { if (uri.rawQuery.isNullOrEmpty()) return null val queryParam = getQueryParam(uri) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } config.server = uri.idnHost config.serverPort = uri.port.toString() config.secretKey = uri.userInfo.orEmpty() config.localAddress = queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4 config.publicKey = queryParam["publickey"].orEmpty() - config.preSharedKey = queryParam["presharedkey"].orEmpty() + config.preSharedKey = queryParam["presharedkey"]?.takeIf { it.isNotEmpty() } config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) config.reserved = queryParam["reserved"] ?: "0,0,0" @@ -83,7 +84,7 @@ object WireguardFmt : FmtBase() { config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4 config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) config.publicKey = peerParams["publickey"].orEmpty() - config.preSharedKey = peerParams["presharedkey"].orEmpty() + config.preSharedKey = peerParams["presharedkey"]?.takeIf { it.isNotEmpty() } val endpoint = peerParams["endpoint"].orEmpty() val endpointParts = endpoint.split(":", limit = 2) if (endpointParts.size == 2) { @@ -105,18 +106,18 @@ object WireguardFmt : FmtBase() { * @return the converted OutboundBean object, or null if conversion fails */ fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = OutboundBean.create(EConfigType.WIREGUARD) + 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.orEmpty() + peer.preSharedKey = profileItem.preSharedKey?.takeIf { it.isNotEmpty() } peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}" } wireguard.mtu = profileItem.mtu - wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() } + wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() } } return outboundBean diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt index 449a05d0..d24ae0c2 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt @@ -415,12 +415,14 @@ object AngConfigManager { if (!it.second.enabled) { return 0 } - val url = HttpUtil.idnToASCII(it.second.url) + val url = HttpUtil.toIdnUrl(it.second.url) if (!Utils.isValidUrl(url)) { return 0 } - if (!Utils.isValidSubUrl(url)) { - return 0 + if (!it.second.allowInsecureUrl) { + if (!Utils.isValidSubUrl(url)) { + return 0 + } } Log.i(AppConfig.TAG, url) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt index cb364531..b2e23f7f 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt @@ -16,6 +16,7 @@ import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.RoutingType import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.dto.VpnInterfaceAddressConfig import com.v2ray.ang.handler.MmkvManager.decodeServerConfig import com.v2ray.ang.handler.MmkvManager.decodeServerList import com.v2ray.ang.util.JsonUtil @@ -159,7 +160,7 @@ object SettingsManager { * @return True if bypassing LAN, false otherwise. */ fun routingRulesetsBypassLan(): Boolean { - val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "0" + val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "1" if (vpnBypassLan == "1") { return true } else if (vpnBypassLan == "2") { @@ -216,7 +217,7 @@ object SettingsManager { * @return The ProfileItem. */ fun getServerViaRemarks(remarks: String?): ProfileItem? { - if (remarks == null) { + if (remarks.isNullOrEmpty()) { return null } val serverList = decodeServerList() @@ -356,4 +357,17 @@ object SettingsManager { "2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) } } + + /** + * Retrieves the currently selected VPN interface address configuration. + * This method reads the user's preference for VPN interface addressing and returns + * the corresponding configuration containing IPv4 and IPv6 addresses. + * + * @return The selected VpnInterfaceAddressConfig instance, or the default configuration + * if no valid selection is found or if the stored index is invalid. + */ + fun getCurrentVpnInterfaceAddressConfig(): VpnInterfaceAddressConfig { + val selectedIndex = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, "0")?.toInt() + return VpnInterfaceAddressConfig.getConfigByIndex(selectedIndex ?: 0) + } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt index bab1f1d4..e547c378 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt @@ -6,8 +6,10 @@ import android.text.TextUtils import android.util.Log import com.v2ray.ang.AppConfig import com.v2ray.ang.R +import com.v2ray.ang.dto.IPAPIInfo import com.v2ray.ang.extension.responseLength import com.v2ray.ang.util.HttpUtil +import com.v2ray.ang.util.JsonUtil import kotlinx.coroutines.isActive import libv2ray.Libv2ray import java.io.IOException @@ -164,6 +166,17 @@ object SpeedtestManager { return Pair(elapsed, result) } + fun getRemoteIPInfo(): String? { + val httpPort = SettingsManager.getHttpPort() + var content = HttpUtil.getUrlContent(AppConfig.IP_API_URL, 5000, httpPort) ?: return null + + var ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null + var ip = ipInfo.ip ?: ipInfo.clientIp ?: ipInfo.ip_addr ?: ipInfo.query + var country = ipInfo.country_code ?: ipInfo.country ?: ipInfo.countryCode + + return "(${country ?: "unknown"}) $ip" + } + /** * Gets the version of the V2Ray library. * diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt index e152002f..37b55c2e 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt @@ -17,7 +17,6 @@ import java.io.FileOutputStream object UpdateCheckerManager { suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) { - try { val url = if (includePreRelease) { AppConfig.APP_API_URL } else { @@ -53,10 +52,6 @@ object UpdateCheckerManager { } else { CheckUpdateResult(hasUpdate = false) } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}") - return@withContext CheckUpdateResult(hasUpdate = false, error = e.message) - } } suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) { 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 0a08b1ec..f53697bb 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,55 +4,33 @@ import android.content.Context import android.text.TextUtils import android.util.Log import com.v2ray.ang.AppConfig -import com.v2ray.ang.AppConfig.DEFAULT_NETWORK -import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES -import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN -import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_ADDRESSES -import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_DOMAIN -import com.v2ray.ang.AppConfig.DNS_DNSPOD_ADDRESSES -import com.v2ray.ang.AppConfig.DNS_DNSPOD_DOMAIN -import com.v2ray.ang.AppConfig.DNS_GOOGLE_ADDRESSES -import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN -import com.v2ray.ang.AppConfig.DNS_QUAD9_ADDRESSES -import com.v2ray.ang.AppConfig.DNS_QUAD9_DOMAIN -import com.v2ray.ang.AppConfig.DNS_YANDEX_ADDRESSES -import com.v2ray.ang.AppConfig.DNS_YANDEX_DOMAIN -import com.v2ray.ang.AppConfig.GEOIP_CN -import com.v2ray.ang.AppConfig.GEOSITE_CN -import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE -import com.v2ray.ang.AppConfig.GOOGLEAPIS_CN_DOMAIN -import com.v2ray.ang.AppConfig.GOOGLEAPIS_COM_DOMAIN -import com.v2ray.ang.AppConfig.HEADER_TYPE_HTTP -import com.v2ray.ang.AppConfig.LOOPBACK -import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM -import com.v2ray.ang.AppConfig.TAG_BLOCKED -import com.v2ray.ang.AppConfig.TAG_DIRECT -import com.v2ray.ang.AppConfig.TAG_FRAGMENT -import com.v2ray.ang.AppConfig.TAG_PROXY -import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 -import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6 import com.v2ray.ang.dto.ConfigResult import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.NetworkType 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.extension.isNotNullEmpty 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.Utils object V2rayConfigManager { private var initConfigCache: String? = null + //region get config function + /** * Retrieves the V2ray configuration for the given GUID. * @@ -104,8 +82,7 @@ object V2rayConfigManager { */ private fun getV2rayCustomConfig(guid: String, config: ProfileItem): ConfigResult { val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false) - val domainPort = config.getServerAddressAndPort() - return ConfigResult(true, guid, raw, domainPort) + return ConfigResult(true, guid, raw) } /** @@ -120,7 +97,7 @@ object V2rayConfigManager { val result = ConfigResult(false) val address = config.server ?: return result - if (!Utils.isIpAddress(address)) { + if (!Utils.isPureIpAddress(address)) { if (!Utils.isValidUrl(address)) { Log.w(AppConfig.TAG, "$address is an invalid ip or domain") return result @@ -131,29 +108,36 @@ object V2rayConfigManager { v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning" v2rayConfig.remarks = config.remarks - inbounds(v2rayConfig) + getInbounds(v2rayConfig) - val isPlugin = config.configType == EConfigType.HYSTERIA2 - val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result - val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin) + if (config.configType == EConfigType.HYSTERIA2) { + result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result + } else { + getOutbounds(v2rayConfig, config) ?: return result + getMoreOutbounds(v2rayConfig, config.subscriptionId) + } - routing(v2rayConfig) + getRouting(v2rayConfig) - fakedns(v2rayConfig) + getFakeDns(v2rayConfig) - dns(v2rayConfig) + getDns(v2rayConfig) if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { - customLocalDns(v2rayConfig) + getCustomLocalDns(v2rayConfig) } if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) { v2rayConfig.stats = null v2rayConfig.policy = null } + //Resolve and add to DNS Hosts + if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") == "1") { + resolveOutboundDomainsToHosts(v2rayConfig) + } + result.status = true - result.content = v2rayConfig.toPrettyPrinting() - result.domainPort = if (retMore.first) retMore.second else retOut.second + result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: "" result.guid = guid return result } @@ -170,7 +154,7 @@ object V2rayConfigManager { val result = ConfigResult(false) val address = config.server ?: return result - if (!Utils.isIpAddress(address)) { + if (!Utils.isPureIpAddress(address)) { if (!Utils.isValidUrl(address)) { Log.w(AppConfig.TAG, "$address is an invalid ip or domain") return result @@ -179,9 +163,12 @@ object V2rayConfigManager { val v2rayConfig = initV2rayConfig(context) ?: return result - val isPlugin = config.configType == EConfigType.HYSTERIA2 - val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result - val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin) + if (config.configType == EConfigType.HYSTERIA2) { + result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result + } else { + getOutbounds(v2rayConfig, config) ?: return result + getMoreOutbounds(v2rayConfig, config.subscriptionId) + } v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning" v2rayConfig.inbounds.clear() @@ -196,8 +183,7 @@ object V2rayConfigManager { } result.status = true - result.content = v2rayConfig.toPrettyPrinting() - result.domainPort = if (retMore.first) retMore.second else retOut.second + result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: "" result.guid = guid return result } @@ -212,7 +198,6 @@ object V2rayConfigManager { * @param context Android context used to access application assets * @return V2rayConfig object parsed from the JSON configuration, or null if the configuration is empty */ - private fun initV2rayConfig(context: Context): V2rayConfig? { val assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json") if (TextUtils.isEmpty(assets)) { @@ -223,14 +208,28 @@ object V2rayConfigManager { return config } - private fun inbounds(v2rayConfig: V2rayConfig): Boolean { + + //endregion + + + //region some sub function + + /** + * Configures the inbound settings for V2ray. + * + * This function sets up the listening ports, sniffing options, and other inbound-related configurations. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @return true if inbound configuration was successful, false otherwise + */ + private fun getInbounds(v2rayConfig: V2rayConfig): Boolean { try { val socksPort = SettingsManager.getSocksPort() v2rayConfig.inbounds.forEach { curInbound -> if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) { //bind all inbounds to localhost if the user requests - curInbound.listen = LOOPBACK + curInbound.listen = AppConfig.LOOPBACK } } v2rayConfig.inbounds[0].port = socksPort @@ -261,44 +260,14 @@ object V2rayConfigManager { return true } - private fun outbounds(v2rayConfig: V2rayConfig, config: ProfileItem, isPlugin: Boolean): Pair? { - if (isPlugin) { - val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0)) - val outboundNew = V2rayConfig.OutboundBean( - mux = null, - protocol = EConfigType.SOCKS.name.lowercase(), - settings = V2rayConfig.OutboundBean.OutSettingsBean( - servers = listOf( - V2rayConfig.OutboundBean.OutSettingsBean.ServersBean( - address = LOOPBACK, - port = socksPort - ) - ) - ) - ) - if (v2rayConfig.outbounds.isNotEmpty()) { - v2rayConfig.outbounds[0] = outboundNew - } else { - v2rayConfig.outbounds.add(outboundNew) - } - return Pair(true, outboundNew.getServerAddressAndPort()) - } - - val outbound = getProxyOutbound(config) ?: return null - val ret = updateOutboundWithGlobalSettings(outbound) - if (!ret) return null - - if (v2rayConfig.outbounds.isNotEmpty()) { - v2rayConfig.outbounds[0] = outbound - } else { - v2rayConfig.outbounds.add(outbound) - } - - updateOutboundFragment(v2rayConfig) - return Pair(true, config.getServerAddressAndPort()) - } - - private fun fakedns(v2rayConfig: V2rayConfig) { + /** + * Configures the fake DNS settings if enabled. + * + * Adds FakeDNS configuration to v2rayConfig if both local DNS and fake DNS are enabled. + * + * @param v2rayConfig The V2ray configuration object to be modified + */ + private fun getFakeDns(v2rayConfig: V2rayConfig) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true && MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true ) { @@ -306,16 +275,24 @@ object V2rayConfigManager { } } - private fun routing(v2rayConfig: V2rayConfig): Boolean { + /** + * Configures routing settings for V2ray. + * + * Sets up the domain strategy and adds routing rules from saved rulesets. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @return true if routing configuration was successful, false otherwise + */ + private fun getRouting(v2rayConfig: V2rayConfig): Boolean { try { v2rayConfig.routing.domainStrategy = MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) - ?: "IPIfNonMatch" + ?: "AsIs" val rulesetItems = MmkvManager.decodeRoutingRulesets() rulesetItems?.forEach { key -> - routingUserRule(key, v2rayConfig) + getRoutingUserRule(key, v2rayConfig) } } catch (e: Exception) { Log.e(AppConfig.TAG, "Failed to configure routing", e) @@ -324,7 +301,13 @@ object V2rayConfigManager { return true } - private fun routingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) { + /** + * Adds a specific ruleset item to the routing configuration. + * + * @param item The ruleset item to add + * @param v2rayConfig The V2ray configuration object to be modified + */ + private fun getRoutingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) { try { if (item == null || !item.enabled) { return @@ -339,14 +322,22 @@ object V2rayConfigManager { } } - private fun userRule2Domain(tag: String): ArrayList { + /** + * Retrieves domain rules for a specific outbound tag. + * + * Searches through all rulesets to find domains targeting the specified tag. + * + * @param tag The outbound tag to search for + * @return ArrayList of domain rules matching the tag + */ + private fun getUserRule2Domain(tag: String): ArrayList { val domain = ArrayList() val rulesetItems = MmkvManager.decodeRoutingRulesets() rulesetItems?.forEach { key -> if (key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) { key.domain?.forEach { - if (it != GEOSITE_PRIVATE + if (it != AppConfig.GEOSITE_PRIVATE && (it.startsWith("geosite:") || it.startsWith("domain:")) ) { domain.add(it) @@ -358,12 +349,20 @@ object V2rayConfigManager { return domain } - private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean { + /** + * Configures custom local DNS settings. + * + * Sets up DNS inbound, outbound, and routing rules for local DNS resolution. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @return true if custom local DNS configuration was successful, false otherwise + */ + private fun getCustomLocalDns(v2rayConfig: V2rayConfig): Boolean { try { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) { - val geositeCn = arrayListOf(GEOSITE_CN) - val proxyDomain = userRule2Domain(TAG_PROXY) - val directDomain = userRule2Domain(TAG_DIRECT) + val geositeCn = arrayListOf(AppConfig.GEOSITE_CN) + val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY) + val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT) // fakedns with all domains to make it always top priority v2rayConfig.dns?.servers?.add( 0, @@ -391,7 +390,7 @@ object V2rayConfigManager { V2rayConfig.InboundBean( tag = "dns-in", port = localDnsPort, - listen = LOOPBACK, + listen = AppConfig.LOOPBACK, protocol = "dokodemo-door", settings = dnsInboundSettings, sniffing = null @@ -427,14 +426,22 @@ object V2rayConfigManager { return true } - private fun dns(v2rayConfig: V2rayConfig): Boolean { + /** + * Configures the DNS settings for V2ray. + * + * Sets up DNS servers, hosts, and routing rules for DNS resolution. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @return true if DNS configuration was successful, false otherwise + */ + private fun getDns(v2rayConfig: V2rayConfig): Boolean { try { val hosts = mutableMapOf() val servers = ArrayList() //remote Dns val remoteDns = SettingsManager.getRemoteDnsServers() - val proxyDomain = userRule2Domain(TAG_PROXY) + val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY) remoteDns.forEach { servers.add(it) } @@ -449,9 +456,9 @@ object V2rayConfigManager { // domestic DNS val domesticDns = SettingsManager.getDomesticDnsServers() - val directDomain = userRule2Domain(TAG_DIRECT) - val isCnRoutingMode = directDomain.contains(GEOSITE_CN) - val geoipCn = arrayListOf(GEOIP_CN) + val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT) + val isCnRoutingMode = directDomain.contains(AppConfig.GEOSITE_CN) + val geoipCn = arrayListOf(AppConfig.GEOIP_CN) if (directDomain.isNotEmpty()) { servers.add( V2rayConfig.DnsBean.ServersBean( @@ -466,7 +473,7 @@ object V2rayConfigManager { if (Utils.isPureIpAddress(domesticDns.first())) { v2rayConfig.routing.rules.add( 0, RulesBean( - outboundTag = TAG_DIRECT, + outboundTag = AppConfig.TAG_DIRECT, port = "53", ip = arrayListOf(domesticDns.first()), domain = null @@ -474,6 +481,25 @@ object V2rayConfigManager { ) } + //block dns + val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED) + if (blkDomain.isNotEmpty()) { + hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK }) + } + + // hardcode googleapi rule to fix play store problems + hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN + + // hardcode popular Android Private DNS rule to fix localhost DNS problem + hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES + hosts[AppConfig.DNS_CLOUDFLARE_ONE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONE_ADDRESSES + hosts[AppConfig.DNS_CLOUDFLARE_DNS_COM_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_COM_ADDRESSES + hosts[AppConfig.DNS_CLOUDFLARE_DNS_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_ADDRESSES + hosts[AppConfig.DNS_DNSPOD_DOMAIN] = AppConfig.DNS_DNSPOD_ADDRESSES + hosts[AppConfig.DNS_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES + hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES + hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES + //User DNS hosts try { val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS) @@ -488,24 +514,6 @@ object V2rayConfigManager { Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e) } - //block dns - val blkDomain = userRule2Domain(TAG_BLOCKED) - if (blkDomain.isNotEmpty()) { - hosts.putAll(blkDomain.map { it to LOOPBACK }) - } - - // hardcode googleapi rule to fix play store problems - hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN - - // hardcode popular Android Private DNS rule to fix localhost DNS problem - hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES - hosts[DNS_CLOUDFLARE_DOMAIN] = DNS_CLOUDFLARE_ADDRESSES - hosts[DNS_DNSPOD_DOMAIN] = DNS_DNSPOD_ADDRESSES - hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES - hosts[DNS_QUAD9_DOMAIN] = DNS_QUAD9_ADDRESSES - hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES - - // DNS dns v2rayConfig.dns = V2rayConfig.DnsBean( servers = servers, @@ -516,7 +524,7 @@ object V2rayConfigManager { if (Utils.isPureIpAddress(remoteDns.first())) { v2rayConfig.routing.rules.add( 0, RulesBean( - outboundTag = TAG_PROXY, + outboundTag = AppConfig.TAG_PROXY, port = "53", ip = arrayListOf(remoteDns.first()), domain = null @@ -530,6 +538,138 @@ object V2rayConfigManager { return true } + + //endregion + + + //region outbound related functions + + /** + * Configures the primary outbound connection. + * + * Converts the profile to an outbound configuration and applies global settings. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @param config The profile item containing connection details + * @return true if outbound configuration was successful, null if there was an error + */ + private fun getOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Boolean? { + val outbound = convertProfile2Outbound(config) ?: return null + val ret = updateOutboundWithGlobalSettings(outbound) + if (!ret) return null + + if (v2rayConfig.outbounds.isNotEmpty()) { + v2rayConfig.outbounds[0] = outbound + } else { + v2rayConfig.outbounds.add(outbound) + } + + updateOutboundFragment(v2rayConfig) + return true + } + + /** + * Configures special outbound settings for Hysteria2 protocol. + * + * Creates a SOCKS outbound connection on a free port for protocols requiring special handling. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @param config The profile item containing connection details + * @return The port number for the SOCKS connection, or null if there was an error + */ + private fun getPlusOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Int? { + try { + val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0)) + + val outboundNew = OutboundBean( + mux = null, + protocol = EConfigType.SOCKS.name.lowercase(), + settings = OutSettingsBean( + servers = listOf( + OutSettingsBean.ServersBean( + address = AppConfig.LOOPBACK, + port = socksPort + ) + ) + ) + ) + if (v2rayConfig.outbounds.isNotEmpty()) { + v2rayConfig.outbounds[0] = outboundNew + } else { + v2rayConfig.outbounds.add(outboundNew) + } + + return socksPort + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to configure plusOutbound", e) + return null + } + } + + /** + * Configures additional outbound connections for proxy chaining. + * + * Sets up previous and next proxies in a subscription for advanced routing capabilities. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @param subscriptionId The subscription ID to look up related proxies + * @return true if additional outbounds were configured successfully, false otherwise + */ + private fun getMoreOutbounds(v2rayConfig: V2rayConfig, subscriptionId: String): Boolean { + //fragment proxy + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) { + return false + } + + if (subscriptionId.isEmpty()) { + return false + } + try { + val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return false + + //current proxy + val outbound = v2rayConfig.outbounds[0] + + //Previous proxy + val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile) + if (prevNode != null) { + val prevOutbound = convertProfile2Outbound(prevNode) + if (prevOutbound != null) { + updateOutboundWithGlobalSettings(prevOutbound) + prevOutbound.tag = AppConfig.TAG_PROXY + "2" + v2rayConfig.outbounds.add(prevOutbound) + outbound.ensureSockopt().dialerProxy = prevOutbound.tag + } + } + + //Next proxy + val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile) + if (nextNode != null) { + val nextOutbound = convertProfile2Outbound(nextNode) + if (nextOutbound != null) { + updateOutboundWithGlobalSettings(nextOutbound) + nextOutbound.tag = AppConfig.TAG_PROXY + v2rayConfig.outbounds.add(0, nextOutbound) + outbound.tag = AppConfig.TAG_PROXY + "1" + nextOutbound.ensureSockopt().dialerProxy = outbound.tag + } + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to configure more outbounds", e) + return false + } + + return true + } + + /** + * Updates outbound settings based on global preferences. + * + * Applies multiplexing and protocol-specific settings to an outbound connection. + * + * @param outbound The outbound connection to update + * @return true if the update was successful, false otherwise + */ private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean { try { var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false) @@ -561,7 +701,7 @@ object V2rayConfigManager { if (protocol.equals(EConfigType.WIREGUARD.name, true)) { var localTunAddr = if (outbound.settings?.address == null) { - listOf(WIREGUARD_LOCAL_ADDRESS_V4, WIREGUARD_LOCAL_ADDRESS_V6) + listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4) } else { outbound.settings?.address as List<*> } @@ -571,8 +711,8 @@ object V2rayConfigManager { outbound.settings?.address = localTunAddr } - if (outbound.streamSettings?.network == DEFAULT_NETWORK - && outbound.streamSettings?.tcpSettings?.header?.type == HEADER_TYPE_HTTP + if (outbound.streamSettings?.network == AppConfig.DEFAULT_NETWORK + && outbound.streamSettings?.tcpSettings?.header?.type == AppConfig.HEADER_TYPE_HTTP ) { val path = outbound.streamSettings?.tcpSettings?.header?.request?.path val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host @@ -582,7 +722,7 @@ object V2rayConfigManager { } outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson( requestString, - V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java + StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java ) outbound.streamSettings?.tcpSettings?.header?.request?.path = if (path.isNullOrEmpty()) { @@ -601,6 +741,14 @@ object V2rayConfigManager { return true } + /** + * Updates the outbound with fragment settings for traffic optimization. + * + * Configures packet fragmentation for TLS and REALITY protocols if enabled. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @return true if fragment configuration was successful, false otherwise + */ private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean { try { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) { @@ -614,8 +762,8 @@ object V2rayConfigManager { val fragmentOutbound = V2rayConfig.OutboundBean( - protocol = PROTOCOL_FREEDOM, - tag = TAG_FRAGMENT, + protocol = AppConfig.PROTOCOL_FREEDOM, + tag = AppConfig.TAG_FRAGMENT, mux = null ) @@ -631,8 +779,8 @@ object V2rayConfigManager { packets = "tlshello" } - fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean( - fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean( + fragmentOutbound.settings = OutboundBean.OutSettingsBean( + fragment = OutboundBean.OutSettingsBean.FragmentBean( packets = packets, length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH) ?: "50-100", @@ -640,15 +788,15 @@ object V2rayConfigManager { ?: "10-20" ), noises = listOf( - V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean( + OutboundBean.OutSettingsBean.NoiseBean( type = "rand", packet = "10-20", delay = "10-16", ) ), ) - fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean( - sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean( + fragmentOutbound.streamSettings = StreamSettingsBean( + sockopt = StreamSettingsBean.SockoptBean( TcpNoDelay = true, mark = 255 ) @@ -657,8 +805,8 @@ object V2rayConfigManager { //proxy chain v2rayConfig.outbounds[0].streamSettings?.sockopt = - V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean( - dialerProxy = TAG_FRAGMENT + StreamSettingsBean.SockoptBean( + dialerProxy = AppConfig.TAG_FRAGMENT ) } catch (e: Exception) { Log.e(AppConfig.TAG, "Failed to update outbound fragment", e) @@ -667,80 +815,51 @@ object V2rayConfigManager { return true } - private fun moreOutbounds( - v2rayConfig: V2rayConfig, - subscriptionId: String, - isPlugin: Boolean - ): Pair { - val returnPair = Pair(false, "") - var domainPort: String = "" + /** + * Resolves domain names to IP addresses in outbound connections. + * + * Pre-resolves domains to improve connection speed and reliability. + * + * @param v2rayConfig The V2ray configuration object to be modified + */ + private fun resolveOutboundDomainsToHosts(v2rayConfig: V2rayConfig) { + val proxyOutboundList = v2rayConfig.getAllProxyOutbound() + val dns = v2rayConfig.dns ?: return + val newHosts = dns.hosts?.toMutableMap() ?: mutableMapOf() + val preferIpv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true - if (isPlugin) { - return returnPair - } - //fragment proxy - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) { - return returnPair - } + for (item in proxyOutboundList) { + val domain = item.getServerAddress() + if (domain.isNullOrEmpty()) continue - if (subscriptionId.isEmpty()) { - return returnPair - } - try { - val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return returnPair - - //current proxy - val outbound = v2rayConfig.outbounds[0] - - //Previous proxy - val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile) - if (prevNode != null) { - val prevOutbound = getProxyOutbound(prevNode) - if (prevOutbound != null) { - updateOutboundWithGlobalSettings(prevOutbound) - prevOutbound.tag = TAG_PROXY + "2" - v2rayConfig.outbounds.add(prevOutbound) - outbound.streamSettings?.sockopt = - V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean( - dialerProxy = prevOutbound.tag - ) - domainPort = prevNode.getServerAddressAndPort() - } + if (newHosts.containsKey(domain)) { + item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6" + continue } - //Next proxy - val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile) - if (nextNode != null) { - val nextOutbound = getProxyOutbound(nextNode) - if (nextOutbound != null) { - updateOutboundWithGlobalSettings(nextOutbound) - nextOutbound.tag = TAG_PROXY - v2rayConfig.outbounds.add(0, nextOutbound) - outbound.tag = TAG_PROXY + "1" - nextOutbound.streamSettings?.sockopt = - V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean( - dialerProxy = outbound.tag - ) - } + val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6) + if (resolvedIps.isNullOrEmpty()) continue + + item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6" + newHosts[domain] = if (resolvedIps.size == 1) { + resolvedIps[0] + } else { + resolvedIps } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to configure more outbounds", e) - return returnPair } - if (domainPort.isNotEmpty()) { - return Pair(true, domainPort) - } - return returnPair + dns.hosts = newHosts } /** - * Retrieves the proxy outbound configuration for the given profile item. + * Converts a profile item to an outbound configuration. * - * @param profileItem The profile item for which to get the proxy outbound configuration. - * @return The proxy outbound configuration as a V2rayConfig.OutboundBean, or null if not found. + * Creates appropriate outbound settings based on the protocol type. + * + * @param profileItem The profile item to convert + * @return OutboundBean configuration for the profile, or null if not supported */ - fun getProxyOutbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? { + private fun convertProfile2Outbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? { return when (profileItem.configType) { EConfigType.VMESS -> VmessFmt.toOutbound(profileItem) EConfigType.CUSTOM -> null @@ -749,10 +868,224 @@ object V2rayConfigManager { EConfigType.VLESS -> VlessFmt.toOutbound(profileItem) EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem) EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem) - EConfigType.HYSTERIA2 -> Hysteria2Fmt.toOutbound(profileItem) + EConfigType.HYSTERIA2 -> null EConfigType.HTTP -> HttpFmt.toOutbound(profileItem) } - } + /** + * 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, + EConfigType.HYSTERIA2 -> + 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.CUSTOM -> 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 + + var sni: String? = null + streamSettings.network = if (transport.isEmpty()) NetworkType.TCP.type else transport + 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() + kcpsetting.header.type = headerType ?: "none" + if (seed.isNullOrEmpty()) { + kcpsetting.seed = null + } else { + kcpsetting.seed = seed + } + if (host.isNullOrEmpty()) { + kcpsetting.header.domain = null + } else { + kcpsetting.header.domain = host + } + streamSettings.kcpSettings = kcpsetting + } + + 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 + } + } + 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 + } + val fingerprint = profileItem.fingerPrint + val alpns = profileItem.alpn + val publicKey = profileItem.publicKey + val shortId = profileItem.shortId + val spiderX = profileItem.spiderX + + streamSettings.security = if (streamSecurity.isEmpty()) null else streamSecurity + if (streamSettings.security == null) return + val tlsSetting = StreamSettingsBean.TlsSettingsBean( + allowInsecure = allowInsecure, + serverName = if (sni.isNullOrEmpty()) null else sni, + fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint, + alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() }, + publicKey = if (publicKey.isNullOrEmpty()) null else publicKey, + shortId = if (shortId.isNullOrEmpty()) null else shortId, + spiderX = if (spiderX.isNullOrEmpty()) null else spiderX, + ) + if (streamSettings.security == AppConfig.TLS) { + streamSettings.tlsSettings = tlsSetting + streamSettings.realitySettings = null + } else if (streamSettings.security == AppConfig.REALITY) { + streamSettings.tlsSettings = null + streamSettings.realitySettings = tlsSetting + } + } + + //endregion } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt index cb9ad811..92c551a6 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt @@ -158,6 +158,7 @@ object NotificationService { mBuilder = null speedNotificationJob?.cancel() speedNotificationJob = null + mNotificationManager = null } /** diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt index db12287d..7aecf634 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt @@ -25,14 +25,13 @@ class QSTileService : TileService() { * @param state The state to set. */ fun setState(state: Int) { + qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name) if (state == Tile.STATE_INACTIVE) { qsTile?.state = Tile.STATE_INACTIVE qsTile?.label = getString(R.string.app_name) - qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name) } else if (state == Tile.STATE_ACTIVE) { qsTile?.state = Tile.STATE_ACTIVE qsTile?.label = V2RayServiceManager.getRunningServerName() - qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name) } qsTile?.updateTile() @@ -45,7 +44,11 @@ class QSTileService : TileService() { override fun onStartListening() { super.onStartListening() - setState(Tile.STATE_INACTIVE) + if (V2RayServiceManager.isRunning()) { + setState(Tile.STATE_ACTIVE) + } else { + setState(Tile.STATE_INACTIVE) + } mMsgReceive = ReceiveMessageHandler(this) val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY) ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags()) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt index 66d74e86..25fcd1a6 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt @@ -27,7 +27,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl { * @return The start mode. */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - V2RayServiceManager.startV2rayPoint() + V2RayServiceManager.startCoreLoop() return START_STICKY } @@ -36,7 +36,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl { */ override fun onDestroy() { super.onDestroy() - V2RayServiceManager.stopV2rayPoint() + V2RayServiceManager.stopCoreLoop() } /** diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt index d1c17cf1..4f42ca23 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt @@ -15,6 +15,7 @@ import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.extension.toast import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.handler.SpeedtestManager import com.v2ray.ang.handler.V2rayConfigManager import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.PluginUtil @@ -23,14 +24,14 @@ import go.Seq import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import libv2ray.CoreCallbackHandler +import libv2ray.CoreController import libv2ray.Libv2ray -import libv2ray.V2RayPoint -import libv2ray.V2RayVPNServiceSupportsSet import java.lang.ref.SoftReference object V2RayServiceManager { - private val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) + private val coreController: CoreController = Libv2ray.newCoreController(CoreCallback()) private val mMsgReceive = ReceiveMessageHandler() private var currentConfig: ProfileItem? = null @@ -38,7 +39,7 @@ object V2RayServiceManager { set(value) { field = value Seq.setContext(value?.get()?.getService()?.applicationContext) - Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey()) + Libv2ray.initCoreEnv(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey()) } /** @@ -80,7 +81,7 @@ object V2RayServiceManager { * Checks if the V2Ray service is running. * @return True if the service is running, false otherwise. */ - fun isRunning() = v2rayPoint.isRunning + fun isRunning() = coreController.isRunning /** * Gets the name of the currently running server. @@ -90,15 +91,18 @@ object V2RayServiceManager { /** * Starts the context service for V2Ray. + * Chooses between VPN service or Proxy-only service based on user settings. * @param context The context from which the service is started. */ private fun startContextService(context: Context) { - if (v2rayPoint.isRunning) return + if (coreController.isRunning) { + return + } val guid = MmkvManager.getSelectServer() ?: return val config = MmkvManager.decodeServerConfig(guid) ?: return if (config.configType != EConfigType.CUSTOM && !Utils.isValidUrl(config.server) - && !Utils.isIpAddress(config.server) + && !Utils.isPureIpAddress(config.server.orEmpty()) ) return // val result = V2rayConfigUtil.getV2rayConfig(context, guid) // if (!result.status) return @@ -123,18 +127,19 @@ object V2RayServiceManager { /** * Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int): * `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`. - * Starts the V2Ray point. + * Starts the V2Ray core service. */ - fun startV2rayPoint() { - val service = getService() ?: return - val guid = MmkvManager.getSelectServer() ?: return - val config = MmkvManager.decodeServerConfig(guid) ?: return - if (v2rayPoint.isRunning) { - return + fun startCoreLoop(): Boolean { + if (coreController.isRunning) { + return false } + + val service = getService() ?: return false + val guid = MmkvManager.getSelectServer() ?: return false + val config = MmkvManager.decodeServerConfig(guid) ?: return false val result = V2rayConfigManager.getV2rayConfig(service, guid) if (!result.status) - return + return false try { val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE) @@ -144,39 +149,49 @@ object V2RayServiceManager { ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags()) } catch (e: Exception) { Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e) + return false } - v2rayPoint.configureFileContent = result.content - v2rayPoint.domainName = result.domainPort currentConfig = config try { - v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) + coreController.startLoop(result.content) } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to start V2Ray loop", e) + Log.e(AppConfig.TAG, "Failed to start Core loop", e) + return false } - if (v2rayPoint.isRunning) { - MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "") - NotificationService.showNotification(currentConfig) - - PluginUtil.runPlugin(service, config, result.domainPort) - } else { + if (coreController.isRunning == false) { MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "") NotificationService.cancelNotification() + return false } + + try { + MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "") + NotificationService.showNotification(currentConfig) + NotificationService.startSpeedNotification(currentConfig) + + PluginUtil.runPlugin(service, config, result.socksPort) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to startup service", e) + return false + } + return true } /** - * Stops the V2Ray point. + * Stops the V2Ray core service. + * Unregisters broadcast receivers, stops notifications, and shuts down plugins. + * @return True if the core was stopped successfully, false otherwise. */ - fun stopV2rayPoint() { - val service = getService() ?: return + fun stopCoreLoop(): Boolean { + val service = getService() ?: return false - if (v2rayPoint.isRunning) { + if (coreController.isRunning) { CoroutineScope(Dispatchers.IO).launch { try { - v2rayPoint.stopLoop() + coreController.stopLoop() } catch (e: Exception) { Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e) } @@ -192,6 +207,8 @@ object V2RayServiceManager { Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e) } PluginUtil.stopPlugin() + + return true } /** @@ -201,40 +218,52 @@ object V2RayServiceManager { * @return The statistics value. */ fun queryStats(tag: String, link: String): Long { - return v2rayPoint.queryStats(tag, link) + return coreController.queryStats(tag, link) } /** - * Measures the delay for V2Ray. + * Measures the connection delay for the current V2Ray configuration. + * Tests with primary URL first, then falls back to alternative URL if needed. + * Also fetches remote IP information if the delay test was successful. */ private fun measureV2rayDelay() { + if (coreController.isRunning == false) { + return + } + CoroutineScope(Dispatchers.IO).launch { val service = getService() ?: return@launch var time = -1L - var errstr = "" - if (v2rayPoint.isRunning) { - try { - time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl()) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e) - errstr = e.message?.substringAfter("\":") ?: "empty message" - } - if (time == -1L) { - try { - time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl(true)) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e) - errstr = e.message?.substringAfter("\":") ?: "empty message" - } - } + var errorStr = "" + + try { + time = coreController.measureDelay(SettingsManager.getDelayTestUrl()) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e) + errorStr = e.message?.substringAfter("\":") ?: "empty message" } - val result = if (time == -1L) { - service.getString(R.string.connection_test_error, errstr) - } else { - service.getString(R.string.connection_test_available, time) + if (time == -1L) { + try { + time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true)) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e) + errorStr = e.message?.substringAfter("\":") ?: "empty message" + } } + val result = if (time >= 0) { + service.getString(R.string.connection_test_available, time) + } else { + service.getString(R.string.connection_test_error, errorStr) + } MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result) + + // Only fetch IP info if the delay test was successful + if (time >= 0) { + SpeedtestManager.getRemoteIPInfo()?.let { ip -> + MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip") + } + } } } @@ -246,10 +275,25 @@ object V2RayServiceManager { return serviceControl?.get()?.getService() } - private class V2RayCallback : V2RayVPNServiceSupportsSet { + /** + * Core callback handler implementation for handling V2Ray core events. + * Handles startup, shutdown, socket protection, and status emission. + */ + private class CoreCallback : CoreCallbackHandler { + /** + * Called when V2Ray core starts up. + * @return 0 for success, any other value for failure. + */ + override fun startup(): Long { + return 0 + } + + /** + * Called when V2Ray core shuts down. + * @return 0 for success, any other value for failure. + */ override fun shutdown(): Long { val serviceControl = serviceControl?.get() ?: return -1 - // called by go return try { serviceControl.stopService() 0 @@ -259,46 +303,25 @@ object V2RayServiceManager { } } - override fun prepare(): Long { - return 0 - } - - override fun protect(l: Long): Boolean { - val serviceControl = serviceControl?.get() ?: return true - return serviceControl.vpnProtect(l.toInt()) - } - /** - * Called by Go to emit status. - * @param l The status code. - * @param s The status message. - * @return The status code. + * Called when V2Ray core emits status information. + * @param l Status code. + * @param s Status message. + * @return Always returns 0. */ override fun onEmitStatus(l: Long, s: String?): Long { return 0 } - - /** - * Called by Go to set up the service. - * @param s The setup string. - * @return The status code. - */ - override fun setup(s: String): Long { - val serviceControl = serviceControl?.get() ?: return -1 - return try { - serviceControl.startService() - NotificationService.startSpeedNotification(currentConfig) - 0 - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to setup service in callback", e) - -1 - } - } } + /** + * Broadcast receiver for handling messages sent to the service. + * Handles registration, service control, and screen events. + */ private class ReceiveMessageHandler : BroadcastReceiver() { /** * Handles received broadcast messages. + * Processes service control messages and screen state changes. * @param ctx The context in which the receiver is running. * @param intent The intent being received. */ @@ -306,7 +329,7 @@ object V2RayServiceManager { val serviceControl = serviceControl?.get() ?: return when (intent?.getIntExtra("key", 0)) { AppConfig.MSG_REGISTER_CLIENT -> { - if (v2rayPoint.isRunning) { + if (coreController.isRunning) { MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "") } else { MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "") diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt index 180dc24b..3fef1ae1 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt @@ -32,7 +32,7 @@ class V2RayTestService : Service() { override fun onCreate() { super.onCreate() Seq.setContext(this) - Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey()) + Libv2ray.initCoreEnv(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey()) } /** diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt index 5a3b67f5..d734c299 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt @@ -33,12 +33,7 @@ import java.lang.ref.SoftReference class V2RayVpnService : VpnService(), ServiceControl { companion object { private const val VPN_MTU = 1500 - private const val PRIVATE_VLAN4_CLIENT = "10.10.14.1" - private const val PRIVATE_VLAN4_ROUTER = "10.10.14.2" - private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:14:1" - private const val PRIVATE_VLAN6_ROUTER = "fc00::10:10:14:2" private const val TUN2SOCKS = "libtun2socks.so" - } private lateinit var mInterface: ParcelFileDescriptor @@ -104,7 +99,9 @@ class V2RayVpnService : VpnService(), ServiceControl { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - V2RayServiceManager.startV2rayPoint() + if (V2RayServiceManager.startCoreLoop()) { + startService() + } return START_STICKY //return super.onStartCommand(intent, flags, startId) } @@ -158,14 +155,15 @@ class V2RayVpnService : VpnService(), ServiceControl { // If the old interface has exactly the same parameters, use it! // Configure a builder while parsing the parameters. val builder = Builder() + val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig() //val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false) builder.setMtu(VPN_MTU) - builder.addAddress(PRIVATE_VLAN4_CLIENT, 30) + builder.addAddress(vpnConfig.ipv4Client, 30) //builder.addDnsServer(PRIVATE_VLAN4_ROUTER) val bypassLan = SettingsManager.routingRulesetsBypassLan() if (bypassLan) { - AppConfig.BYPASS_PRIVATE_IP_LIST.forEach { + AppConfig.ROUTED_IP_LIST.forEach { val addr = it.split('/') builder.addRoute(addr[0], addr[1].toInt()) } @@ -174,9 +172,10 @@ class V2RayVpnService : VpnService(), ServiceControl { } if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) { - builder.addAddress(PRIVATE_VLAN6_CLIENT, 126) + builder.addAddress(vpnConfig.ipv6Client, 126) if (bypassLan) { builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use + builder.addRoute("fc00::", 18) //Xray-core default FakeIPv6 Pool } else { builder.addRoute("::", 0) } @@ -255,10 +254,12 @@ class V2RayVpnService : VpnService(), ServiceControl { * Starts the tun2socks process with the appropriate parameters. */ private fun runTun2socks() { + Log.i(AppConfig.TAG, "Start run $TUN2SOCKS") val socksPort = SettingsManager.getSocksPort() + val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig() val cmd = arrayListOf( File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath, - "--netif-ipaddr", PRIVATE_VLAN4_ROUTER, + "--netif-ipaddr", vpnConfig.ipv4Router, "--netif-netmask", "255.255.255.252", "--socks-server-addr", "$LOOPBACK:${socksPort}", "--tunmtu", VPN_MTU.toString(), @@ -269,7 +270,7 @@ class V2RayVpnService : VpnService(), ServiceControl { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) { cmd.add("--netif-ip6addr") - cmd.add(PRIVATE_VLAN6_ROUTER) + cmd.add(vpnConfig.ipv6Router) } if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) { val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt()) @@ -293,11 +294,11 @@ class V2RayVpnService : VpnService(), ServiceControl { runTun2socks() } }.start() - Log.i(AppConfig.TAG, process.toString()) + Log.i(AppConfig.TAG, "$TUN2SOCKS process info : ${process.toString()}") sendFd() } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to start tun2socks process", e) + Log.e(AppConfig.TAG, "Failed to start $TUN2SOCKS process", e) } } @@ -308,13 +309,13 @@ class V2RayVpnService : VpnService(), ServiceControl { private fun sendFd() { val fd = mInterface.fileDescriptor val path = File(applicationContext.filesDir, "sock_path").absolutePath - Log.i(AppConfig.TAG, path) + Log.i(AppConfig.TAG, "LocalSocket path : $path") CoroutineScope(Dispatchers.IO).launch { var tries = 0 while (true) try { Thread.sleep(50L shl tries) - Log.i(AppConfig.TAG, "sendFd tries: $tries") + Log.i(AppConfig.TAG, "LocalSocket sendFd tries: $tries") LocalSocket().use { localSocket -> localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM)) localSocket.setFileDescriptorsForSend(arrayOf(fd)) @@ -348,13 +349,13 @@ class V2RayVpnService : VpnService(), ServiceControl { } try { - Log.i(AppConfig.TAG, "tun2socks destroy") + Log.i(AppConfig.TAG, "$TUN2SOCKS destroy") process.destroy() } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to destroy tun2socks process", e) + Log.e(AppConfig.TAG, "Failed to destroy $TUN2SOCKS process", e) } - V2RayServiceManager.stopV2rayPoint() + V2RayServiceManager.stopCoreLoop() if (isForced) { //stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt index 7c2ffdba..1931cb45 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt @@ -5,28 +5,21 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.util.Log -import android.view.View import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.content.FileProvider -import androidx.lifecycle.lifecycleScope import com.tencent.mmkv.MMKV import com.v2ray.ang.AppConfig import com.v2ray.ang.BuildConfig import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityAboutBinding -import com.v2ray.ang.dto.CheckUpdateResult import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SpeedtestManager -import com.v2ray.ang.handler.UpdateCheckerManager -import com.v2ray.ang.util.AppManagerUtil import com.v2ray.ang.util.Utils import com.v2ray.ang.util.ZipUtil -import kotlinx.coroutines.launch import java.io.File import java.text.SimpleDateFormat import java.util.Locale @@ -105,23 +98,6 @@ class AboutActivity : BaseActivity() { } } - //If it is the Google Play version, not be displayed within 1 days after update - if (Utils.isGoogleFlavor()) { - val lastUpdateTime = AppManagerUtil.getLastUpdateTime(this) - val currentTime = System.currentTimeMillis() - if ((currentTime - lastUpdateTime) < 1 * 24 * 60 * 60 * 1000L) { - binding.layoutCheckUpdate.visibility = View.GONE - } - } - binding.layoutCheckUpdate.setOnClickListener { - checkForUpdates(binding.checkPreRelease.isChecked) - } - - binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked -> - MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked) - } - binding.checkPreRelease.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false) - binding.layoutSoureCcode.setOnClickListener { Utils.openUri(this, AppConfig.APP_URL) } @@ -222,28 +198,4 @@ class AboutActivity : BaseActivity() { } } } - - private fun checkForUpdates(includePreRelease: Boolean) { - lifecycleScope.launch { - val result = UpdateCheckerManager.checkForUpdate(includePreRelease) - if (result.hasUpdate) { - showUpdateDialog(result) - } else { - toast(R.string.update_already_latest_version) - } - } - } - - private fun showUpdateDialog(result: CheckUpdateResult) { - AlertDialog.Builder(this) - .setTitle(getString(R.string.update_new_version_found, result.latestVersion)) - .setMessage(result.releaseNotes) - .setPositiveButton(R.string.update_now) { _, _ -> - result.downloadUrl?.let { - Utils.openUri(this, it) - } - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } } \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt new file mode 100644 index 00000000..a9b698c5 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt @@ -0,0 +1,77 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope +import com.v2ray.ang.AppConfig +import com.v2ray.ang.BuildConfig +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityCheckUpdateBinding +import com.v2ray.ang.dto.CheckUpdateResult +import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SpeedtestManager +import com.v2ray.ang.handler.UpdateCheckerManager +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.launch + +class CheckUpdateActivity : BaseActivity() { + + private val binding by lazy { ActivityCheckUpdateBinding.inflate(layoutInflater) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + title = getString(R.string.update_check_for_update) + + binding.layoutCheckUpdate.setOnClickListener { + checkForUpdates(binding.checkPreRelease.isChecked) + } + + binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked -> + MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked) + } + binding.checkPreRelease.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false) + + "v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also { + binding.tvVersion.text = it + } + + checkForUpdates(binding.checkPreRelease.isChecked) + } + + private fun checkForUpdates(includePreRelease: Boolean) { + toast(R.string.update_checking_for_update) + + lifecycleScope.launch { + try { + val result = UpdateCheckerManager.checkForUpdate(includePreRelease) + if (result.hasUpdate) { + showUpdateDialog(result) + } else { + toastSuccess(R.string.update_already_latest_version) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}") + toastError(e.message ?: getString(R.string.toast_failure)) + } + } + } + + private fun showUpdateDialog(result: CheckUpdateResult) { + AlertDialog.Builder(this) + .setTitle(getString(R.string.update_new_version_found, result.latestVersion)) + .setMessage(result.releaseNotes) + .setPositiveButton(R.string.update_now) { _, _ -> + result.downloadUrl?.let { + Utils.openUri(this, it) + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt index bb9abc68..0c7584d8 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt @@ -685,6 +685,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.APP_PROMOTION_URL)}?t=${System.currentTimeMillis()}") R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java)) + R.id.check_for_update -> startActivity(Intent(this, CheckUpdateActivity::class.java)) R.id.about -> startActivity(Intent(this, AboutActivity::class.java)) } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt index 179f70ae..e7ea6211 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt @@ -27,6 +27,7 @@ import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.helper.ItemTouchHelperAdapter import com.v2ray.ang.helper.ItemTouchHelperViewHolder import com.v2ray.ang.service.V2RayServiceManager +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -220,10 +221,15 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter(AppConfig.PREF_LOCAL_DNS_PORT) } private val vpnDns by lazy { findPreference(AppConfig.PREF_VPN_DNS) } private val vpnBypassLan by lazy { findPreference(AppConfig.PREF_VPN_BYPASS_LAN) } + private val vpnInterfaceAddress by lazy { findPreference(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) } private val mux by lazy { findPreference(AppConfig.PREF_MUX_ENABLED) } private val muxConcurrency by lazy { findPreference(AppConfig.PREF_MUX_CONCURRENCY) } @@ -249,12 +250,14 @@ class SettingsActivity : BaseActivity() { listOf( AppConfig.PREF_VPN_BYPASS_LAN, + AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, AppConfig.PREF_MUX_XUDP_QUIC, AppConfig.PREF_FRAGMENT_PACKETS, AppConfig.PREF_LANGUAGE, AppConfig.PREF_UI_MODE_NIGHT, AppConfig.PREF_LOGLEVEL, + AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, AppConfig.PREF_MODE ).forEach { key -> if (MmkvManager.decodeSettingsString(key) != null) { @@ -273,7 +276,7 @@ class SettingsActivity : BaseActivity() { localDnsPort?.isEnabled = vpn vpnDns?.isEnabled = vpn vpnBypassLan?.isEnabled = vpn - vpn + vpnInterfaceAddress?.isEnabled = vpn if (vpn) { updateLocalDns( MmkvManager.decodeSettingsBool( diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt index 649156fb..f85382f1 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt @@ -6,6 +6,7 @@ import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope +import com.v2ray.ang.AppConfig import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivitySubEditBinding import com.v2ray.ang.dto.SubscriptionItem @@ -46,6 +47,7 @@ class SubEditActivity : BaseActivity() { binding.etFilter.text = Utils.getEditable(subItem.filter) binding.chkEnable.isChecked = subItem.enabled binding.autoUpdateCheck.isChecked = subItem.autoUpdate + binding.allowInsecureUrl.isChecked = subItem.allowInsecureUrl binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile) binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile) return true @@ -77,6 +79,7 @@ class SubEditActivity : BaseActivity() { subItem.autoUpdate = binding.autoUpdateCheck.isChecked subItem.prevProfile = binding.etPreProfile.text.toString() subItem.nextProfile = binding.etNextProfile.text.toString() + subItem.allowInsecureUrl = binding.allowInsecureUrl.isChecked if (TextUtils.isEmpty(subItem.remarks)) { toast(R.string.sub_setting_remarks) @@ -90,7 +93,9 @@ class SubEditActivity : BaseActivity() { if (!Utils.isValidSubUrl(subItem.url)) { toast(R.string.toast_insecure_url_protocol) - return false + if (!subItem.allowInsecureUrl) { + return false + } } } @@ -105,19 +110,28 @@ class SubEditActivity : BaseActivity() { */ private fun deleteServer(): Boolean { if (editSubId.isNotEmpty()) { - AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) - .setPositiveButton(android.R.string.ok) { _, _ -> - lifecycleScope.launch(Dispatchers.IO) { - MmkvManager.removeSubscription(editSubId) - launch(Dispatchers.Main) { - finish() + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { + AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + lifecycleScope.launch(Dispatchers.IO) { + MmkvManager.removeSubscription(editSubId) + launch(Dispatchers.Main) { + finish() + } } } + .setNegativeButton(android.R.string.cancel) { _, _ -> + // do nothing + } + .show() + } else { + lifecycleScope.launch(Dispatchers.IO) { + MmkvManager.removeSubscription(editSubId) + launch(Dispatchers.Main) { + finish() + } } - .setNegativeButton(android.R.string.cancel) { _, _ -> - // do nothing - } - .show() + } } return true } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt index bb364c1b..cc2d5404 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.v2ray.ang.AppConfig import com.v2ray.ang.R @@ -20,6 +21,8 @@ import com.v2ray.ang.helper.ItemTouchHelperAdapter import com.v2ray.ang.helper.ItemTouchHelperViewHolder import com.v2ray.ang.util.QRCodeDecoder import com.v2ray.ang.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter(), ItemTouchHelperAdapter { @@ -46,6 +49,10 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView ) } + holder.itemSubSettingBinding.layoutRemove.setOnClickListener { + removeSubscription(subId, position) + } + holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked -> if (!it.isPressed) return@setOnCheckedChangeListener subItem.enabled = isChecked @@ -54,9 +61,11 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView } if (TextUtils.isEmpty(subItem.url)) { + holder.itemSubSettingBinding.layoutUrl.visibility = View.GONE holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE } else { + holder.itemSubSettingBinding.layoutUrl.visibility = View.VISIBLE holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE holder.itemSubSettingBinding.layoutShare.setOnClickListener { @@ -90,6 +99,32 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView } } + private fun removeSubscription(subId: String, position: Int) { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { + AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + removeSubscriptionSub(subId, position) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do noting + } + .show() + } else { + removeSubscriptionSub(subId, position) + } + } + + private fun removeSubscriptionSub(subId: String, position: Int) { + mActivity.lifecycleScope.launch(Dispatchers.IO) { + MmkvManager.removeSubscription(subId) + launch(Dispatchers.Main) { + notifyItemRemoved(position) + notifyItemRangeChanged(position, mActivity.subscriptions.size) + mActivity.refreshData() + } + } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder { return MainViewHolder( ItemRecyclerSubSettingBinding.inflate( diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt index 202af8be..7172728e 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt @@ -9,20 +9,23 @@ import com.v2ray.ang.util.Utils.urlDecode import java.io.IOException import java.net.HttpURLConnection import java.net.IDN +import java.net.Inet6Address +import java.net.InetAddress import java.net.InetSocketAddress import java.net.Proxy import java.net.URL object HttpUtil { - /** - * Converts a URL string to its ASCII representation. + * Converts the domain part of a URL string to its IDN (Punycode, ASCII Compatible Encoding) format. * - * @param str The URL string to convert. - * @return The ASCII representation of the URL. + * For example, a URL like "https://例子.中国/path" will be converted to "https://xn--fsqu00a.xn--fiqs8s/path". + * + * @param str The URL string to convert (can contain non-ASCII characters in the domain). + * @return The URL string with the domain part converted to ASCII-compatible (Punycode) format. */ - fun idnToASCII(str: String): String { + fun toIdnUrl(str: String): String { val url = URL(str) val host = url.host val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED) @@ -33,6 +36,67 @@ object HttpUtil { } } + /** + * Converts a Unicode domain name to its IDN (Punycode, ASCII Compatible Encoding) format. + * If the input is an IP address or already an ASCII domain, returns the original string. + * + * @param domain The domain string to convert (can include non-ASCII internationalized characters). + * @return The domain in ASCII-compatible (Punycode) format, or the original string if input is an IP or already ASCII. + */ + fun toIdnDomain(domain: String): String { + // Return as is if it's a pure IP address (IPv4 or IPv6) + if (Utils.isPureIpAddress(domain)) { + return domain + } + + // Return as is if already ASCII (English domain or already punycode) + if (domain.all { it.code < 128 }) { + return domain + } + + // Otherwise, convert to ASCII using IDN + return IDN.toASCII(domain, IDN.ALLOW_UNASSIGNED) + } + + /** + * Resolves a hostname to an IP address, returns original input if it's already an IP + * + * @param host The hostname or IP address to resolve + * @param ipv6Preferred Whether to prefer IPv6 addresses, defaults to false + * @return The resolved IP address or the original input (if it's already an IP or resolution fails) + */ + fun resolveHostToIP(host: String, ipv6Preferred: Boolean = false): List? { + try { + // If it's already an IP address, return it as a list + if (Utils.isPureIpAddress(host)) { + return null + } + + // Get all IP addresses + val addresses = InetAddress.getAllByName(host) + if (addresses.isEmpty()) { + return null + } + + // Sort addresses based on preference + val sortedAddresses = if (ipv6Preferred) { + addresses.sortedWith(compareByDescending { it is Inet6Address }) + } else { + addresses.sortedWith(compareBy { it is Inet6Address }) + } + + val ipList = sortedAddresses.mapNotNull { it.hostAddress } + + Log.i(AppConfig.TAG, "Resolved IPs for $host: ${ipList.joinToString()}") + + return ipList + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to resolve host to IP", e) + return null + } + } + + /** * Retrieves the content of a URL as a string. * diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt index bcf30a3b..2b9f71aa 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt @@ -23,20 +23,24 @@ object PluginUtil { * * @param context The context to use. * @param config The profile configuration. - * @param domainPort The domain and port information. + * @param socksPort The port information. */ - fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) { + fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) { Log.i(AppConfig.TAG, "Starting plugin execution") if (config == null) { Log.w(AppConfig.TAG, "Cannot run plugin: config is null") return } - + try { if (config.configType == EConfigType.HYSTERIA2) { + if (socksPort == null) { + Log.w(AppConfig.TAG, "Cannot run plugin: socksPort is null") + return + } Log.i(AppConfig.TAG, "Running Hysteria2 plugin") - val configFile = genConfigHy2(context, config, domainPort) ?: return + val configFile = genConfigHy2(context, config, socksPort) ?: return val cmd = genCmdHy2(context, configFile) procService.runProcess(context, cmd) @@ -66,7 +70,7 @@ object PluginUtil { if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) { val socksPort = Utils.findFreePort(listOf(0)) - val configFile = genConfigHy2(context, config, "0:${socksPort}") ?: return retFailure + val configFile = genConfigHy2(context, config, socksPort) ?: return retFailure val cmd = genCmdHy2(context, configFile) val proc = ProcessService() @@ -85,14 +89,12 @@ object PluginUtil { * * @param context The context to use. * @param config The profile configuration. - * @param domainPort The domain and port information. + * @param socksPort The port information. * @return The generated configuration file. */ - private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? { + private fun genConfigHy2(context: Context, config: ProfileItem, socksPort: Int): File? { Log.i(AppConfig.TAG, "runPlugin $HYSTERIA2") - val socksPort = domainPort?.split(":")?.last() - .let { if (it.isNullOrEmpty()) return null else it.toInt() } val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json") diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt index 6a61afe5..148ce4ec 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt @@ -198,6 +198,21 @@ object Utils { return isIpv4Address(value) || isIpv6Address(value) } + /** + * Check if a string is a valid domain name. + * + * A valid domain name must not be an IP address and must be a valid URL format. + * + * @param input The string to check. + * @return True if the string is a valid domain name, false otherwise. + */ + fun isDomainName(input: String?): Boolean { + if (input.isNullOrEmpty()) return false + + // Must not be an IP address and must be a valid URL format + return !isPureIpAddress(input) && isValidUrl(input) + } + /** * Check if a string is a valid IPv4 address. * diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt index d78b1307..7ac5d60f 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt @@ -41,6 +41,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application AppConfig.PREF_MODE, AppConfig.PREF_VPN_DNS, AppConfig.PREF_VPN_BYPASS_LAN, + AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, AppConfig.PREF_REMOTE_DNS, AppConfig.PREF_DOMESTIC_DNS, AppConfig.PREF_DNS_HOSTS, @@ -48,6 +49,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PREF_SOCKS_PORT, AppConfig.PREF_LOGLEVEL, + AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, AppConfig.PREF_LANGUAGE, AppConfig.PREF_UI_MODE_NIGHT, AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, diff --git a/V2rayNG/app/src/main/res/layout/activity_about.xml b/V2rayNG/app/src/main/res/layout/activity_about.xml index d4596963..62053559 100644 --- a/V2rayNG/app/src/main/res/layout/activity_about.xml +++ b/V2rayNG/app/src/main/res/layout/activity_about.xml @@ -111,49 +111,6 @@ android:orientation="vertical" android:paddingTop="@dimen/padding_spacing_dp16"> - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml index 28575fd1..af5d70cd 100644 --- a/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml +++ b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml @@ -138,6 +138,28 @@ + + + + + + + + - - - - - - - - + android:orientation="vertical"> + android:paddingStart="@dimen/padding_spacing_dp8"> - + + android:orientation="horizontal"> + + + + + + + + + + + + + + + + + + - + + android:orientation="horizontal" + android:paddingStart="@dimen/padding_spacing_dp8" + android:paddingEnd="@dimen/padding_spacing_dp8"> - + + + + + + + android:layout_marginTop="@dimen/padding_spacing_dp8" + android:orientation="horizontal"> + + + + + + - + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/menu_drawer.xml b/V2rayNG/app/src/main/res/menu/menu_drawer.xml index c134759d..4e204e62 100644 --- a/V2rayNG/app/src/main/res/menu/menu_drawer.xml +++ b/V2rayNG/app/src/main/res/menu/menu_drawer.xml @@ -35,6 +35,10 @@ android:id="@+id/logcat" android:icon="@drawable/ic_logcat_24dp" android:title="@string/title_logcat" /> + الإعدادات إعدادات متقدمة + إعدادات النواة إعدادات VPN الوكيل لكل تطبيق عام: التطبيق المحدد هو وكيل، غير المحدد اتصال مباشر؛ \nوضع التجاوز: التطبيق المحدد متصل مباشرة، غير المحدد وكيل. \nخيار تحديد تطبيق الوكيل تلقائيًا في القائمة @@ -181,6 +182,8 @@ VPN DNS (IPv4/v6 فقط) Does VPN bypass LAN + VPN Interface Address + DNS المحلي (اختياري) DNS @@ -238,6 +241,7 @@ فاصل التحديث التلقائي (بالدقائق، الحد الأدنى للقيمة 15) مستوى السجل + Outbound domain pre-resolve method الوضع انقر هنا للحصول على مزيد من المساعدة اللغة @@ -258,6 +262,7 @@ Remarks regular filter تفعيل التحديث تفعيل التحديث التلقائي + Allow insecure HTTP address Previous proxy configuration remarks Next proxy configuration remarks The configuration remarks exists and is unique @@ -315,6 +320,7 @@ New version found: %s Update now Check Pre-release + Checking for update… رمز استجابة سريعة (QRcode) @@ -352,4 +358,10 @@ Not Bypass + + Do not resolve + Resolve and add to DNS Hosts + Resolve and replace domain + + diff --git a/V2rayNG/app/src/main/res/values-bn/strings.xml b/V2rayNG/app/src/main/res/values-bn/strings.xml index 58f22b17..f36c9d3a 100644 --- a/V2rayNG/app/src/main/res/values-bn/strings.xml +++ b/V2rayNG/app/src/main/res/values-bn/strings.xml @@ -139,6 +139,7 @@ সেটিংস এডভান্সড সেটিংস + কোর সেটিংস VPN সেটিংস প্রতি-অ্যাপ প্রক্সি সাধারণ: চেকড অ্যাপ প্রক্সি, আনচেকড সরাসরি সংযোগ; \nবাইপাস মোড: চেকড অ্যাপ সরাসরি সংযুক্ত, আনচেকড প্রক্সি। \nমেনুতে প্রক্সি অ্যাপ্লিকেশন স্বয়ংক্রিয়ভাবে নির্বাচন করার বিকল্প @@ -181,6 +182,8 @@ VPN DNS (শুধুমাত্র IPv4/v6) Does VPN bypass LAN + VPN Interface Address + ঘরোয়া DNS (ঐচ্ছিক) DNS @@ -238,6 +241,7 @@ অটো আপডেট ইন্টারভ্যাল (মিনিট, সর্বনিম্ন মান ১৫) লগ স্তর + Outbound domain pre-resolve method মোড আরো সাহায্যের জন্য ক্লিক করুন ভাষা @@ -258,6 +262,7 @@ Remarks regular filter আপডেট সক্রিয় করুন স্বয়ংক্রিয় আপডেট সক্রিয় করুন + Allow insecure HTTP address Previous proxy configuration remarks Next proxy configuration remarks The configuration remarks exists and is unique @@ -314,6 +319,7 @@ New version found: %s Update now Check Pre-release + Checking for update… QR কোড @@ -357,4 +363,10 @@ Not Bypass + + Do not resolve + Resolve and add to DNS Hosts + Resolve and replace domain + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml index 0275de46..90e5cc26 100644 --- a/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml +++ b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml @@ -40,7 +40,7 @@ نیشتنا نشۊوی پورت - نوم من توری + نوم منتوری شناسه جایگۊزین ٱمنیت شبکه @@ -73,7 +73,7 @@ رزم ٱمنیت رزم (اختیاری) - نوم من توری (اختیاری) + نوم منتوری (اختیاری) رزم نگاری جریان کیلیت پوی وولاتی @@ -140,6 +140,7 @@ سامووا سامووا پؽش رئڌه + سامووا هسته سامووا VPN پروکسی و ری برنومه پوی وولاتی: برنومه واجۊری بیڌه پروکسی هڌ، منپیز موستقیم بؽ نشووه هڌ. هالت دور زیڌن: برنومه نشووک ناڌه موستقیمن منپیز هڌ، پروکسی نشووک زیڌه نؽڌ. گۊزینه پسند خوتکار برنومه پروکسی من نومگه @@ -148,7 +149,7 @@ سامووا Mux ر وندن Mux - زل تر، ٱما گاشڌ منپیز زی قت بۊ بارت دؽوۉداری، TCP، UDP و QUIC ن ای لم سفارشی کۊنین. + زل تر، ٱما گاشڌ منپیز زی قت بۊ\nمخزن ترافیک TCP وا 8 منپیز پؽش فرز، بارت دؽوۉداری UDP وو QUIC ن ای لم سفارشی کۊنین. منپیزا TCP (تلایه منجا 1-1024) منپیزا XUDP (تلایه منجا 1-1024) دؽوۉداری QUIC من تۊنل mux @@ -167,24 +168,26 @@ ز نوم دامنه sniffed تینا سی تور جوستن استفاڌه کۊنین وو نشۊوی مۉرد نزرن و عونوان نشۊوی IP ووردارین. ر وندن DNS مهلی - DNS پردازشت وابیڌه و دس هسته ماژول DNS (پؽشنهاڌ ابۊ، ٱر نیاز هڌ ک جوستن تور وو ولات ٱسلین دور زنی) + درخاستا DNS و هسته و من ایان وو و دست ماژول DNS پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین فعال بۊ) ر وندن DNS جئلی DNS مهلی نشۊویا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من یقرد ز برنومه یل کار نکونه) ترجی IPv6 - ترجی داڌن نشۊوی وو تورا IPv6 + تورا IPv6 ن فعال کۊنین وو نشۊویا IPv6 ن ترجی بڌین - DNS ز ر دیر (اختیاری) (udp/tcp/https/quic) (اختیاری) + ز ر دیر (اختیاری) DNS (udp/tcp/https/quic) (اختیاری) DNS VPN DNS (تینا IPv4/v6) - VPN ز شبکه مهلی اگوڌرته؟ + ز شبکه مهلی اگوڌرته؟ VPN - DNS منی (اختیاری) + نشۊوی رابت VPN + + منی (اختیاری) DNS DNS - DNS هاست موستقیم (قالوو: دامنه: نشۊوی،...) + هاست موستقیم (قالوو: دامنه: نشۊوی،...) DNS دامنه:نشۊوی،... نشۊوی اینترنتی آزمایش تئخیر واقعی (http/https) @@ -204,20 +207,20 @@ پورت DNS مهلی قوۊل کردن پاک کردن کانفیگ - سی پاک وابیڌن فایل کانفیگ نیاز به قوۊل کردن دووارته ز سمت منتور هڌ + سی پاک وابیڌن فایل کانفیگ نیاز به قوۊل کردن دووارته ز سمت منتور هڌ. زی اسکنن ر ون - شؽواتگرن سی اسکن، زی مجال ر وندن بۊگۊشین، اندی ترین کودن اسکن کۊنین یا شؽواتی ن منه نوار ٱوزار پسند کۊنین. + شؽواتگرن سی اسکن، زی مجال ر وندن بۊگۊشین، ٱندی ترین کودن اسکن کۊنین یا شؽواتی ن منه نوار ٱوزار پسند کۊنین. پروکسی HTTP ن و VPN ازاف کۊنین پروکسی HTTP ن موسقیمن ز (مۊرۊرگر/ی قرد ز برنومه یل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ. ر وندن نشۉݩ داڌن دو سۊتۊنی - نومگه نمایه یل من دو سۊتۊن نشۉݩ داڌه ابۊن وو چینۉ ترین موئتوا بیشتری ن سیل کۊنین. سی ر وستن وا برنومه ن ز نۊ ر ونین. + نومگه نمایه یل من دو سۊتۊن نشۉݩ داڌه ابۊن وو چینۉ ترین موئتوا بیشتری ن سیل کۊنین. سی ر وستن، وا برنومه ن ز نۊ ر ونین. فشناڌن منشڌ - فشناڌن منشڌ یا داسوو موشکلا من Github + فشناڌن منشڌ یا داسووݩ موشکلا من Github ٱووڌن من جرگه تلگرام برنومه تلگرامن نجوست هریم سیخومی @@ -238,6 +241,7 @@ فاسله ورۊ کردن خوتکار (اقلن وا 15 دؽقه بۊ) سئت داسووا + بارت پؽش هل دامنه دری هالت سی دووسمندیا وو هیاری بیشتر، ری ای هؽل بزݩ زۉݩ @@ -258,6 +262,7 @@ نوم موستعار فیلتر فعال بیڌن ورۊ کردن فعال بیڌن ورۊ کردن خوتکار + موجاز کردن نشۊوی HTTP نا ٱمن نوم موستعار پروکسی دیندایی نوم موستعار پروکسی نیایی موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی @@ -303,7 +308,7 @@ منپیزن واجۊری کوݩ هونی آزمایش ابۊ… %d کانفیگ هونی آزمایش ابۊ... - مووفق بی: منپیز HTTP %dms تۊل کشی + مووفق بی: منپیز %dms تۊل کشی منپیز و اینترنتن نجوست: %s اینترنت من دسرس نؽ کود ختا: #%d @@ -322,7 +327,8 @@ سکو نوسخه دیندایی پۊرنیڌه هڌ نوسخه نۊ ن جوست: %s سکو ورۊ رسۊوی کۊنین - نوسخیل پؽش ز تیجنیڌنن واجۊری کۊنین + واجۊری نوسخه یل پؽش ز تیجنیڌن + ورۊ رسۊوی ن هونی واجۊری اکونه... QRcode @@ -367,4 +373,10 @@ دور زیڌه نبۊ + + هل وو فسل مکۊنین + هل وو ٱووردن و میزبووݩ یل دامنه DNS + هل وو جایونی دامنه + + diff --git a/V2rayNG/app/src/main/res/values-fa/strings.xml b/V2rayNG/app/src/main/res/values-fa/strings.xml index c739fd86..081571a3 100644 --- a/V2rayNG/app/src/main/res/values-fa/strings.xml +++ b/V2rayNG/app/src/main/res/values-fa/strings.xml @@ -137,6 +137,7 @@ تنظیمات تنظیمات پیشرفته + تنظیمات هسته تنظیمات VPN پروکسی به تفکیک برنامه عمومی: برنامه انتخاب شده از طریق یک پروکسی متصل می شود، برنامه انتخاب نشده مستقیماً متصل می شود. \nحالت دور زدن: برنامه انتخاب شده مستقیماً متصل می شود، برنامه انتخاب نشده از طریق یک پروکسی متصل می شود. \nانتخاب خودکار برنامه های پراکسی در منو امکان پذیر است. @@ -171,7 +172,7 @@ دی ان اس محلی آدرس های آیپی جعلی را بر می گرداند (سریع تر می باشد و تاخیر را کاهش می دهد اما ممکن است برای برخی از برنامه ها کار نکند) ترجیح دادن IPV6 - ترجیح دادن نشانی و مسیر های IPv6 + مسیرهای IPv6 را فعال کنید و آدرس‌های IPv6 را ترجیح دهید DNS از راه دور (اختیاری) (udp/tcp/https/quic) DNS @@ -179,6 +180,8 @@ VPN DNS (فقط IPv4/v6) آیا VPN از شبکه محلی عبور می کند؟ + VPN Interface Address + DNS داخلی (اختیاری) DNS @@ -235,6 +238,7 @@ اشتراک های خود را به طور خودکار با فاصله زمانی در پس زمینه به روز کنید. بسته به دستگاه، این ویژگی ممکن است همیشه کار نکند. فاصله به‌ روزرسانی خودکار ( حداقل مقدار ، 15 دقیقه ) سطح گزارشات + Outbound domain pre-resolve method حالت برای اطلاعات و راهنمایی بیشتر، روی این متن کلیک کنید زبان @@ -255,6 +259,7 @@ نام مستعار فیلتر فعال کردن به‌روزرسانی فعال سازی به‌روزرسانی خودکار + مجاز کردن آدرس HTTP ناامن نام مستعار پروکسی قبلی نام مستعار پروکسی بعدی لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است @@ -300,7 +305,7 @@ اتصال را بررسی کنید در حال آزمایش... تست کردن %d کانفیگ… - موفقیت: اتصال HTTP %dms طول کشید + موفقیت: اتصال %dms طول کشید اتصال به اینترنت شناسایی نشد: %s اینترنت در دسترس نیست کد خطا: #%d @@ -320,6 +325,7 @@ نسخه جدید پیدا شد: %s اکنون به روز رسانی کنید بررسی نسخه پیش از انتشار + در حال بررسی برای به‌روزرسانی… QRcode @@ -366,4 +372,10 @@ دور زده نشود + + Do not resolve + Resolve and add to DNS Hosts + Resolve and replace domain + + diff --git a/V2rayNG/app/src/main/res/values-ru/strings.xml b/V2rayNG/app/src/main/res/values-ru/strings.xml index 9cf4fd3e..c71054a0 100644 --- a/V2rayNG/app/src/main/res/values-ru/strings.xml +++ b/V2rayNG/app/src/main/res/values-ru/strings.xml @@ -139,6 +139,7 @@ Настройки Расширенные настройки + Настройки ядра Настройки VPN Прокси для выбранных приложений Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню. @@ -172,7 +173,7 @@ Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями) Предпочитать IPv6 - Предпочитать IPv6-адреса и маршрутизацию + Использовать маршрутизацию IPv6 предпочитать IPv6-адреса Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно) DNS @@ -180,6 +181,8 @@ VPN DNS (только IPv4/v6) VPN пропускает LAN + VPN частный IP + Внутренняя DNS (необязательно) DNS @@ -237,6 +240,7 @@ Интервал автообновления (минут, не менее 15) Подробность ведения журнала + Outbound domain pre-resolve method Режим Нажмите для получения дополнительной информации Язык @@ -257,9 +261,10 @@ Название фильтра Использовать обновление Использовать автообновление - Название предыдущего прокси - Название следующего прокси - Название должно существовать и быть уникальным + Разрешать незащищённые HTTP-адреса + Предыдущая конфигурация прокси + Следующая конфигурация прокси + Конфигурация должна быть уникальной Обновить подписку группы Проверка профилей группы Время отклика профилей группы @@ -302,7 +307,7 @@ Проверить подключение Проверка… Проверка профилей (%d) - Успешно: HTTP-соединение заняло %d мс + Успешно: соединение заняло %d мс Сбой проверки интернет-соединения: %s Интернет недоступен Код ошибки: #%d @@ -321,7 +326,8 @@ Установлена последняя версия Найдена новая версия: %s Обновить - Проверить предварительный выпуск + Искать предварительный выпуск + Проверка обновления… QR-код @@ -366,4 +372,10 @@ Не пропускает + + Do not resolve + Resolve and add to DNS Hosts + Resolve and replace domain + + diff --git a/V2rayNG/app/src/main/res/values-vi/strings.xml b/V2rayNG/app/src/main/res/values-vi/strings.xml index 296f039e..86238b79 100644 --- a/V2rayNG/app/src/main/res/values-vi/strings.xml +++ b/V2rayNG/app/src/main/res/values-vi/strings.xml @@ -138,6 +138,7 @@ Cài đặt Cài đặt nâng cao + Cài đặt lõi Cài đặt VPN Proxy theo Ứng dụng - Bình thường: Ứng dụng đã chọn sẽ kết nối thông qua Proxy, chưa chọn sẽ kết nối trực tiếp. \n- Chế độ Bypass: Ứng dụng đã chọn sẽ kết nối trực tiếp, chưa chọn sẽ kết nối qua Proxy. \n- Nếu bạn đang ở Trung Quốc thì vào Menu, chọn Tự động chọn ứng dụng Proxy. @@ -181,6 +182,8 @@ VPN DNS (Chỉ IPv4 / IPv6) Does VPN bypass LAN + VPN Interface Address + DNS nội địa (Không bắt buộc) DNS @@ -238,6 +241,7 @@ Thời gian cập nhật tự động (Phút, giá trị tối thiểu là 15) Cấp độ nhật ký + Outbound domain pre-resolve method Chế độ kết nối Nhấn vào đây nếu bạn cần trợ giúp! Ngôn ngữ @@ -258,6 +262,7 @@ Remarks regular filter Sử dụng gói đăng ký này Bật tự động cập nhật + Allow insecure HTTP address Previous proxy configuration remarks Next proxy configuration remarks The configuration remarks exists and is unique @@ -316,6 +321,7 @@ New version found: %s Update now Check Pre-release + Checking for update… Xuất ra mã QR (Chụp màn hình để lưu) @@ -354,4 +360,10 @@ Not Bypass + + Do not resolve + Resolve and add to DNS Hosts + Resolve and replace domain + + diff --git a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml index b7bef7c3..a8eec856 100644 --- a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml @@ -137,6 +137,7 @@ 设置 进阶设置 + 核心设置 VPN 设置 分应用 常规: 勾选的 App 被代理, 未勾选的直连;\n绕行模式: 勾选的 App 直连, 未勾选的被代理.\n不明白者在菜单中选择自动选中需代理应用 @@ -178,6 +179,8 @@ VPN DNS (仅支持 IPv4/v6) VPN 是否绕过局域网 + VPN 接口地址 + 境内 DNS (可选) DNS @@ -235,6 +238,7 @@ 自动更新间隔(分钟,最小值 15) 日志级别 + Outbound 域名预解析方式 模式 点此查看更多帮助 语言 @@ -255,6 +259,7 @@ 别名正则过滤 启用更新 启用自动更新 + 允许不安全的 HTTP 地址 前置代理配置文件别名 落地代理配置文件別名 请确保配置文件别名存在并唯一 @@ -314,6 +319,7 @@ 发现新版本: %s 立即更新 检查 Pre-release + 正在检查更新中… 二维码 @@ -358,4 +364,10 @@ 不绕过 + + 不解析 + 解析后添加至 DNS Hosts + 解析后替换原域名 + + diff --git a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml index bb7403df..f8b938c5 100644 --- a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml @@ -138,6 +138,7 @@ 設定 進階 + 核心設定 VPN 設定 Proxy 個別應用程式 常規:勾選的 App 啟用 Proxy,未勾選的直接連線;\n繞行模式:勾選的 App 直接連線,未勾選的啟用 Proxy。\n可在選單中選擇自動選中需 Proxy 應用 @@ -180,6 +181,8 @@ VPN DNS (僅支援 IPv4/v6) VPN 是否繞過區域網 + VPN 介面位址 + DNS 境内 DNS (可选) DNS hosts (格式: 網域:位址,…) @@ -236,6 +239,7 @@ 自動更新間隔(分鐘,最小值 15) 記錄層級 + Outbound 網域預解析方式 模式 輕觸以檢視說明 語言 @@ -256,6 +260,7 @@ 別名正規過濾 啟用更新 啟用自動更新 + 允許不安全的 HTTP 位址 前置代理設定檔别名 落地代理設定檔別名 请确保設定檔别名存在并唯一 @@ -314,6 +319,7 @@ 發現新版本: %s 立即更新 檢查 Pre-release + 正在檢查更新中… QR Code @@ -358,4 +364,10 @@ 不繞過 + + 不解析 + 解析後加入 DNS Hosts + 解析後替換原網域名稱 + + diff --git a/V2rayNG/app/src/main/res/values/arrays.xml b/V2rayNG/app/src/main/res/values/arrays.xml index 1863ccdd..27f0846e 100644 --- a/V2rayNG/app/src/main/res/values/arrays.xml +++ b/V2rayNG/app/src/main/res/values/arrays.xml @@ -182,4 +182,30 @@ 2 + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + + + + 10.10.14.x + 10.1.0.x + 10.0.0.x + 172.31.0.x + 172.20.0.x + 172.16.0.x + 192.168.100.x + + + + 0 + 1 + 2 + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml index ced93fa0..57106306 100644 --- a/V2rayNG/app/src/main/res/values/strings.xml +++ b/V2rayNG/app/src/main/res/values/strings.xml @@ -140,6 +140,7 @@ Settings Advanced Settings + Core Settings VPN Settings Per-app proxy General: Checked apps use proxy, unchecked apps connect directly; \nBypass mode: checked apps connect directly, unchecked apps use proxy. \nThe option to automatically select proxy applications is in the menu @@ -174,7 +175,7 @@ Local DNS returns fake IP addresses (faster, but it may not work for some apps) Prefer IPv6 - Prefer IPv6 addresses and routes + Enable IPv6 routes and Prefer IPv6 addresses Remote DNS (udp/tcp/https/quic)(Optional) DNS @@ -182,6 +183,8 @@ VPN DNS (only IPv4/v6) Does VPN bypass LAN + VPN Interface Address + Domestic DNS (Optional) DNS @@ -239,6 +242,7 @@ Auto Update Interval (Minutes, Min value 15) Log Level + Outbound domain pre-resolve method Mode Click me for more help Language @@ -259,6 +263,7 @@ Remarks regular filter Enable update Enable automatic update + Allow insecure HTTP address Previous proxy configuration remarks Next proxy configuration remarks The configuration remarks exists and is unique @@ -304,7 +309,7 @@ Check Connectivity Testing… Testing %d configurations… - Success: HTTP connection took %dms + Success: Connection took %dms Fail to detect internet connection: %s Internet Unavailable Error code: #%d @@ -324,6 +329,7 @@ New version found: %s Update now Check Pre-release + Checking for update… QRcode @@ -368,4 +374,10 @@ Not Bypass + + Do not resolve + Resolve and add to DNS Hosts + Resolve and replace domain + + diff --git a/V2rayNG/app/src/main/res/xml/pref_settings.xml b/V2rayNG/app/src/main/res/xml/pref_settings.xml index 8a5f9040..b5ee7aab 100644 --- a/V2rayNG/app/src/main/res/xml/pref_settings.xml +++ b/V2rayNG/app/src/main/res/xml/pref_settings.xml @@ -19,6 +19,11 @@ android:title="@string/title_pref_is_booted" /> + + + + @@ -166,14 +179,7 @@ android:title="@string/title_pref_auto_update_interval" /> - - - + - - + + + + + + + + - + + \ No newline at end of file diff --git a/V2rayNG/app/src/test/java/com/v2ray/ang/HttpUtilTest.kt b/V2rayNG/app/src/test/java/com/v2ray/ang/HttpUtilTest.kt index 207215e5..07d87f4d 100644 --- a/V2rayNG/app/src/test/java/com/v2ray/ang/HttpUtilTest.kt +++ b/V2rayNG/app/src/test/java/com/v2ray/ang/HttpUtilTest.kt @@ -10,31 +10,31 @@ class HttpUtilTest { fun testIdnToASCII() { // Regular URL remains unchanged val regularUrl = "https://example.com/path" - assertEquals(regularUrl, HttpUtil.idnToASCII(regularUrl)) + assertEquals(regularUrl, HttpUtil.toIdnUrl(regularUrl)) // Non-ASCII URL converts to ASCII (Punycode) val nonAsciiUrl = "https://例子.测试/path" val expectedNonAscii = "https://xn--fsqu00a.xn--0zwm56d/path" - assertEquals(expectedNonAscii, HttpUtil.idnToASCII(nonAsciiUrl)) + assertEquals(expectedNonAscii, HttpUtil.toIdnUrl(nonAsciiUrl)) // Mixed URL only converts the host part val mixedUrl = "https://例子.com/测试" val expectedMixed = "https://xn--fsqu00a.com/测试" - assertEquals(expectedMixed, HttpUtil.idnToASCII(mixedUrl)) + assertEquals(expectedMixed, HttpUtil.toIdnUrl(mixedUrl)) // URL with Basic Authentication using regular domain val basicAuthUrl = "https://user:password@example.com/path" - assertEquals(basicAuthUrl, HttpUtil.idnToASCII(basicAuthUrl)) + assertEquals(basicAuthUrl, HttpUtil.toIdnUrl(basicAuthUrl)) // URL with Basic Authentication using non-ASCII domain val basicAuthNonAscii = "https://user:password@例子.测试/path" val expectedBasicAuthNonAscii = "https://user:password@xn--fsqu00a.xn--0zwm56d/path" - assertEquals(expectedBasicAuthNonAscii, HttpUtil.idnToASCII(basicAuthNonAscii)) + assertEquals(expectedBasicAuthNonAscii, HttpUtil.toIdnUrl(basicAuthNonAscii)) // URL with non-ASCII username and password val nonAsciiAuth = "https://用户:密码@example.com/path" // Basic auth credentials should remain unchanged as they're percent-encoded separately - assertEquals(nonAsciiAuth, HttpUtil.idnToASCII(nonAsciiAuth)) + assertEquals(nonAsciiAuth, HttpUtil.toIdnUrl(nonAsciiAuth)) } diff --git a/V2rayNG/gradle/libs.versions.toml b/V2rayNG/gradle/libs.versions.toml index fb625399..04900e8c 100644 --- a/V2rayNG/gradle/libs.versions.toml +++ b/V2rayNG/gradle/libs.versions.toml @@ -1,27 +1,27 @@ [versions] -agp = "8.9.1" -desugar_jdk_libs = "2.1.5" +agp = "8.10.1" +desugarJdkLibs = "2.1.5" gradleLicensePlugin = "0.9.8" -kotlin = "2.1.20" -coreKtx = "1.15.0" +kotlin = "2.1.21" +coreKtx = "1.16.0" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" -appcompat = "1.7.0" +appcompat = "1.7.1" material = "1.12.0" activity = "1.10.1" constraintlayout = "2.2.1" mmkvStatic = "1.3.12" gson = "2.12.1" quickieFoss = "1.14.0" -kotlinx-coroutines-android = "1.10.1" -kotlinx-coroutines-core = "1.10.1" +kotlinxCoroutinesAndroid = "1.10.2" +kotlinxCoroutinesCore = "1.10.2" swiperefreshlayout = "1.1.0" toasty = "1.5.2" editorkit = "2.9.0" core = "3.5.3" -workRuntimeKtx = "2.10.0" -lifecycleViewmodelKtx = "2.8.7" +workRuntimeKtx = "2.10.2" +lifecycleViewmodelKtx = "2.9.1" multidex = "2.0.1" mockitoMockitoInline = "5.2.0" flexbox = "3.0.0" @@ -30,7 +30,7 @@ recyclerview = "1.4.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" } -desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } +desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" } gradle-license-plugin = { module = "com.jaredsburrows:gradle-license-plugin", version.ref = "gradleLicensePlugin" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } @@ -42,8 +42,8 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const mmkv-static = { module = "com.tencent:mmkv-static", version.ref = "mmkvStatic" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } quickie-foss = { module = "com.github.T8RIN.QuickieExtended:quickie-foss", version.ref = "quickieFoss" } -kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version = "kotlinx-coroutines-android" } -kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = "kotlinx-coroutines-core" } +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } toasty = { module = "com.github.GrenderG:Toasty", version.ref = "toasty" } editorkit = { module = "com.blacksquircle.ui:editorkit", version.ref = "editorkit" } language-base = { module = "com.blacksquircle.ui:language-base", version.ref = "editorkit" } diff --git a/V2rayNG/gradle/wrapper/gradle-wrapper.properties b/V2rayNG/gradle/wrapper/gradle-wrapper.properties index f221584f..b2eeb9db 100644 --- a/V2rayNG/gradle/wrapper/gradle-wrapper.properties +++ b/V2rayNG/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Nov 14 12:42:51 BDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/hysteria b/hysteria index 245c6e9b..2adeec29 160000 --- a/hysteria +++ b/hysteria @@ -1 +1 @@ -Subproject commit 245c6e9bd17b1ef644f81fc4dafd0a1e1933da85 +Subproject commit 2adeec2900a7a0e3689f118580174cc528f9995a