diff --git a/AndroidLibXrayLite b/AndroidLibXrayLite index c53ff63a..8ad3e1dd 160000 --- a/AndroidLibXrayLite +++ b/AndroidLibXrayLite @@ -1 +1 @@ -Subproject commit c53ff63a3be2583d4b90f6819dfb0266cada0e18 +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 7478730e..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 = 650 - versionName = "1.10.0" + 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 649b71c1..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,7 +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://api.ip.sb/geoip" + const val IP_API_URL = "https://speed.cloudflare.com/meta" /** DNS server addresses. */ const val DNS_PROXY = "1.1.1.1" @@ -168,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" @@ -182,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", 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 index 4d29ffc7..97814fbb 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt @@ -2,10 +2,11 @@ package com.v2ray.ang.dto data class IPAPIInfo( var ip: String? = null, - var city: String? = null, - var region: String? = null, - var region_code: 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 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/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 cc9a70ff..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 ) @@ -149,6 +151,20 @@ 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 73def17e..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 @@ -17,7 +17,7 @@ object HttpFmt : FmtBase() { 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 dcc49f06..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 @@ -25,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 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 7a5ac437..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 @@ -36,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() @@ -135,7 +135,7 @@ object ShadowsocksFmt : FmtBase() { 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 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 46046f48..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 @@ -23,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() @@ -64,7 +64,7 @@ object SocksFmt : FmtBase() { 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 8b6c1dec..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 @@ -23,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 @@ -64,7 +64,7 @@ object TrojanFmt : FmtBase() { 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 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 2092843e..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 @@ -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 @@ -60,7 +60,7 @@ object VlessFmt : FmtBase() { 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 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 e3a72e61..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 @@ -151,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 @@ -172,7 +172,7 @@ object VmessFmt : FmtBase() { 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 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 30a33577..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 @@ -25,7 +25,7 @@ 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() 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 1dcd1276..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,7 +415,7 @@ 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 } 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 99898a3a..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 @@ -168,10 +168,13 @@ object SpeedtestManager { fun getRemoteIPInfo(): String? { val httpPort = SettingsManager.getHttpPort() - var content = HttpUtil.getUrlContent(AppConfig.IP_API_Url, 5000, httpPort) ?: return null + var content = HttpUtil.getUrlContent(AppConfig.IP_API_URL, 5000, httpPort) ?: return null var ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null - return "(${ipInfo.country_code}) ${ipInfo.ip}" + var ip = ipInfo.ip ?: ipInfo.clientIp ?: ipInfo.ip_addr ?: ipInfo.query + var country = ipInfo.country_code ?: ipInfo.country ?: ipInfo.countryCode + + return "(${country ?: "unknown"}) $ip" } /** 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 3ba5f0a8..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 @@ -16,7 +16,6 @@ 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 @@ -98,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 @@ -132,7 +131,10 @@ object V2rayConfigManager { v2rayConfig.policy = null } - resolveOutboundDomainsToHosts(v2rayConfig) + //Resolve and add to DNS Hosts + if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") == "1") { + resolveOutboundDomainsToHosts(v2rayConfig) + } result.status = true result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: "" @@ -152,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 @@ -286,7 +288,7 @@ object V2rayConfigManager { v2rayConfig.routing.domainStrategy = MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) - ?: "IPIfNonMatch" + ?: "AsIs" val rulesetItems = MmkvManager.decodeRoutingRulesets() rulesetItems?.forEach { key -> @@ -479,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) @@ -493,24 +514,6 @@ object V2rayConfigManager { Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e) } - //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_DOMAIN] = AppConfig.DNS_CLOUDFLARE_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 - - // DNS dns v2rayConfig.dns = V2rayConfig.DnsBean( servers = servers, @@ -828,7 +831,11 @@ object V2rayConfigManager { for (item in proxyOutboundList) { val domain = item.getServerAddress() if (domain.isNullOrEmpty()) continue - if (newHosts.containsKey(domain)) continue + + if (newHosts.containsKey(domain)) { + item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6" + continue + } val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6) if (resolvedIps.isNullOrEmpty()) continue @@ -861,7 +868,7 @@ 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) } } @@ -1045,7 +1052,15 @@ object V2rayConfigManager { fun populateTlsSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) { val streamSecurity = profileItem.security.orEmpty() val allowInsecure = profileItem.insecure == true - val sni = if (profileItem.sni.isNullOrEmpty()) sniExt else profileItem.sni + 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 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/V2RayServiceManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt index 659158a5..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 @@ -102,7 +102,7 @@ object V2RayServiceManager { 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 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 9fc24d56..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 @@ -160,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()) } @@ -176,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) } @@ -259,9 +256,10 @@ class V2RayVpnService : VpnService(), ServiceControl { 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(), @@ -272,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()) 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/SettingsActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt index 4ec2294a..6af64e3a 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt @@ -44,6 +44,7 @@ class SettingsActivity : BaseActivity() { private val localDnsPort by lazy { findPreference(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 fff8f42d..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 @@ -109,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 9e08f663..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 @@ -18,12 +18,14 @@ 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) @@ -34,6 +36,28 @@ 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 * 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 48e04b6d..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 @@ -28,13 +28,17 @@ object PluginUtil { fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) { Log.i(AppConfig.TAG, "Starting plugin execution") - if (config == null || socksPort == null) { + 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, socksPort) ?: return val cmd = genCmdHy2(context, configFile) 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/item_recycler_sub_setting.xml b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml index aa8d9812..47c2b4c5 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml @@ -15,97 +15,144 @@ android:clickable="true" android:focusable="true" android:gravity="center" - android:nextFocusRight="@+id/layout_edit" + android:nextFocusRight="@+id/layout_share" android:orientation="horizontal" android:padding="@dimen/padding_spacing_dp8"> - - - - - - - - + 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 الوضع انقر هنا للحصول على مزيد من المساعدة اللغة @@ -316,6 +320,7 @@ New version found: %s Update now Check Pre-release + Checking for update… رمز استجابة سريعة (QRcode) @@ -353,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 4520f2e2..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 মোড আরো সাহায্যের জন্য ক্লিক করুন ভাষা @@ -315,6 +319,7 @@ New version found: %s Update now Check Pre-release + Checking for update… QR কোড @@ -358,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 45dc5158..90e5cc26 100644 --- a/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml +++ b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml @@ -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,7 +168,7 @@ ز نوم دامنه sniffed تینا سی تور جوستن استفاڌه کۊنین وو نشۊوی مۉرد نزرن و عونوان نشۊوی IP ووردارین. ر وندن DNS مهلی - DNS پردازشت وابیڌه و دس هسته ماژول DNS (پؽشنهاڌ ابۊ، ٱر نیاز هڌ ک جوستن تور وو ولات ٱسلین دور زنی) + درخاستا DNS و هسته و من ایان وو و دست ماژول DNS پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین فعال بۊ) ر وندن DNS جئلی DNS مهلی نشۊویا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من یقرد ز برنومه یل کار نکونه) @@ -175,16 +176,18 @@ ترجی 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 دؽقه بۊ) سئت داسووا + بارت پؽش هل دامنه دری هالت سی دووسمندیا وو هیاری بیشتر، ری ای هؽل بزݩ زۉݩ @@ -324,6 +328,7 @@ نوسخه نۊ ن جوست: %s سکو ورۊ رسۊوی کۊنین واجۊری نوسخه یل پؽش ز تیجنیڌن + ورۊ رسۊوی ن هونی واجۊری اکونه... QRcode @@ -368,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 1341f182..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انتخاب خودکار برنامه های پراکسی در منو امکان پذیر است. @@ -179,6 +180,8 @@ VPN DNS (فقط IPv4/v6) آیا VPN از شبکه محلی عبور می کند؟ + VPN Interface Address + DNS داخلی (اختیاری) DNS @@ -235,6 +238,7 @@ اشتراک های خود را به طور خودکار با فاصله زمانی در پس زمینه به روز کنید. بسته به دستگاه، این ویژگی ممکن است همیشه کار نکند. فاصله به‌ روزرسانی خودکار ( حداقل مقدار ، 15 دقیقه ) سطح گزارشات + Outbound domain pre-resolve method حالت برای اطلاعات و راهنمایی بیشتر، روی این متن کلیک کنید زبان @@ -321,6 +325,7 @@ نسخه جدید پیدا شد: %s اکنون به روز رسانی کنید بررسی نسخه پیش از انتشار + در حال بررسی برای به‌روزرسانی… QRcode @@ -367,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 cc56161b..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Есть возможность автоматического выбора проксируемых приложений в меню. @@ -180,6 +181,8 @@ VPN DNS (только IPv4/v6) VPN пропускает LAN + VPN частный IP + Внутренняя DNS (необязательно) DNS @@ -237,6 +240,7 @@ Интервал автообновления (минут, не менее 15) Подробность ведения журнала + Outbound domain pre-resolve method Режим Нажмите для получения дополнительной информации Язык @@ -323,6 +327,7 @@ Найдена новая версия: %s Обновить Искать предварительный выпуск + Проверка обновления… QR-код @@ -367,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 ac247ce0..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ữ @@ -317,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) @@ -355,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 38303d49..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 域名预解析方式 模式 点此查看更多帮助 语言 @@ -315,6 +319,7 @@ 发现新版本: %s 立即更新 检查 Pre-release + 正在检查更新中… 二维码 @@ -359,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 959312e9..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 網域預解析方式 模式 輕觸以檢視說明 語言 @@ -315,6 +319,7 @@ 發現新版本: %s 立即更新 檢查 Pre-release + 正在檢查更新中… QR Code @@ -359,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 069dcfc7..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 @@ -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 @@ -325,6 +329,7 @@ New version found: %s Update now Check Pre-release + Checking for update… QRcode @@ -369,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 75cad848..b5ee7aab 100644 --- a/V2rayNG/app/src/main/res/xml/pref_settings.xml +++ b/V2rayNG/app/src/main/res/xml/pref_settings.xml @@ -20,9 +20,9 @@ + android:key="pref_prefer_ipv6" + android:summary="@string/summary_pref_prefer_ipv6" + android:title="@string/title_pref_prefer_ipv6" /> + + @@ -171,9 +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 2c4a5d98..04900e8c 100644 --- a/V2rayNG/gradle/libs.versions.toml +++ b/V2rayNG/gradle/libs.versions.toml @@ -1,13 +1,13 @@ [versions] -agp = "8.9.2" +agp = "8.10.1" desugarJdkLibs = "2.1.5" gradleLicensePlugin = "0.9.8" -kotlin = "2.1.20" +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" @@ -20,8 +20,8 @@ swiperefreshlayout = "1.1.0" toasty = "1.5.2" editorkit = "2.9.0" core = "3.5.3" -workRuntimeKtx = "2.10.1" -lifecycleViewmodelKtx = "2.8.7" +workRuntimeKtx = "2.10.2" +lifecycleViewmodelKtx = "2.9.1" multidex = "2.0.1" mockitoMockitoInline = "5.2.0" flexbox = "3.0.0" 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