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 c64f0dcb..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 = 645 - versionName = "1.9.45" + 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/AngApplication.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt index 16e02f16..44f680b3 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt @@ -39,5 +39,9 @@ class AngApplication : MultiDexApplication() { WorkManager.initialize(this, workManagerConfiguration) SettingsManager.initRoutingRulesets(this) + + es.dmoral.toasty.Toasty.Config.getInstance() + .setGravity(android.view.Gravity.BOTTOM, 0, 200) + .apply() } } 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 7613c318..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,15 +56,18 @@ 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" + const val PREF_GEO_FILES_SOURCES = "pref_geo_files_sources" /** Cache keys. */ const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id" const val CACHE_KEYWORD_FILTER = "cache_keyword_filter" /** Protocol identifiers. */ - const val PROTOCOL_FREEDOM: String = "freedom" + const val PROTOCOL_FREEDOM = "freedom" /** Broadcast actions. */ const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service" @@ -88,19 +92,20 @@ object AppConfig { const val DOWNLINK = "downlink" /** URLs for various resources. */ - const val androidpackagenamelistUrl = - "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt" - const val v2rayCustomRoutingListUrl = - "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/" - const val v2rayNGUrl = "https://github.com/2dust/v2rayNG" - const val v2rayNGIssues = "$v2rayNGUrl/issues" - const val v2rayNGWikiMode = "$v2rayNGUrl/wiki/Mode" - const val v2rayNGPrivacyPolicy = "https://raw.githubusercontent.com/2dust/v2rayNG/master/CR.md" - const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw=" - const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/" - const val TgChannelUrl = "https://t.me/github_2dust" - const val DelayTestUrl = "https://www.gstatic.com/generate_204" - const val DelayTestUrl2 = "https://www.google.com/generate_204" + const val GITHUB_URL = "https://github.com" + const val GITHUB_RAW_URL = "https://raw.githubusercontent.com" + const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download" + const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt" + const val APP_URL = "$GITHUB_URL/2dust/v2rayNG" + const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases" + const val APP_ISSUES_URL = "$APP_URL/issues" + const val APP_WIKI_MODE = "$APP_URL/wiki/Mode" + const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md" + const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw=" + 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" @@ -165,19 +170,13 @@ 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" - - 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_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") - const val DEFAULT_PORT = 443 const val DEFAULT_SECURITY = "auto" const val DEFAULT_LEVEL = 8 @@ -186,4 +185,64 @@ object AppConfig { const val REALITY = "reality" 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_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 ROUTED_IP_LIST = arrayListOf( + "0.0.0.0/5", + "8.0.0.0/7", + "11.0.0.0/8", + "12.0.0.0/6", + "16.0.0.0/4", + "32.0.0.0/3", + "64.0.0.0/2", + "128.0.0.0/3", + "160.0.0.0/5", + "168.0.0.0/6", + "172.0.0.0/12", + "172.32.0.0/11", + "172.64.0.0/10", + "172.128.0.0/9", + "173.0.0.0/8", + "174.0.0.0/7", + "176.0.0.0/4", + "192.0.0.0/9", + "192.128.0.0/11", + "192.160.0.0/13", + "192.169.0.0/16", + "192.170.0.0/15", + "192.172.0.0/14", + "192.176.0.0/12", + "192.192.0.0/10", + "193.0.0.0/8", + "194.0.0.0/7", + "196.0.0.0/6", + "200.0.0.0/5", + "208.0.0.0/4", + "240.0.0.0/4" + ) + + 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", + "224.0.0.0/4" + ) + + val GEO_FILES_SOURCES = arrayListOf( + "Loyalsoldier/v2ray-rules-dat", + "runetfreedom/russia-v2ray-rules-dat", + "Chocolate4U/Iran-v2ray-rules" + ) + } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt index d0703f09..5a8d1e60 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt @@ -4,5 +4,6 @@ data class AssetUrlItem( var remarks: String = "", var url: String = "", val addedTime: Long = System.currentTimeMillis(), - var lastUpdated: Long = -1 + var lastUpdated: Long = -1, + var locked: Boolean? = false, ) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt new file mode 100644 index 00000000..be4f62e5 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt @@ -0,0 +1,10 @@ +package com.v2ray.ang.dto + +data class CheckUpdateResult( + val hasUpdate: Boolean, + val latestVersion: String? = null, + val releaseNotes: String? = null, + val downloadUrl: String? = null, + val error: String? = null, + val isPreRelease: Boolean = false +) \ No newline at end of file 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/GitHubRelease.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt new file mode 100644 index 00000000..0a7dce56 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt @@ -0,0 +1,23 @@ +package com.v2ray.ang.dto + +import com.google.gson.annotations.SerializedName + +data class GitHubRelease( + @SerializedName("tag_name") + val tagName: String, + @SerializedName("body") + val body: String, + @SerializedName("assets") + val assets: List, + @SerializedName("prerelease") + val prerelease: Boolean = false, + @SerializedName("published_at") + val publishedAt: String = "" +) { + data class Asset( + @SerializedName("name") + val name: String, + @SerializedName("browser_download_url") + val browserDownloadUrl: String + ) +} \ No newline at end of file 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/extension/_Ext.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt index 3064535f..6e5c6bb4 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt @@ -196,4 +196,17 @@ inline fun Intent.serializable(key: String): T? = whe * * @return True if the CharSequence is not null and not empty, false otherwise. */ -fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty()) \ No newline at end of file +fun CharSequence?.isNotNullEmpty(): Boolean = this != null && this.isNotEmpty() + +fun String.concatUrl(vararg paths: String): String { + val builder = StringBuilder(this.trimEnd('/')) + + paths.forEach { path -> + val trimmedPath = path.trim('/') + if (trimmedPath.isNotEmpty()) { + builder.append('/').append(trimmedPath) + } + } + + return builder.toString() +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt index bd0e638d..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 @@ -18,15 +20,15 @@ open class FmtBase { */ fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap?): String { val query = if (dicQuery != null) - ("?" + dicQuery.toList().joinToString( + "?" + dicQuery.toList().joinToString( separator = "&", - transform = { it.first + "=" + Utils.urlEncode(it.second) })) + transform = { it.first + "=" + Utils.urlEncode(it.second) }) else "" 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 } -} \ No newline at end of file + + 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 90736c06..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,16 +25,16 @@ 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.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") + config.reserved = queryParam["reserved"] ?: "0,0,0" return config } @@ -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 c40a61c7..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 @@ -7,7 +7,9 @@ import android.util.Log import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.HY2 import com.v2ray.ang.R -import com.v2ray.ang.dto.* +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.fmt.CustomFmt import com.v2ray.ang.fmt.Hysteria2Fmt import com.v2ray.ang.fmt.ShadowsocksFmt @@ -413,10 +415,15 @@ 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 (!it.second.allowInsecureUrl) { + if (!Utils.isValidSubUrl(url)) { + return 0 + } + } Log.i(AppConfig.TAG, url) var configText = try { @@ -430,7 +437,7 @@ object AngConfigManager { configText = try { HttpUtil.getUrlContentWithUserAgent(url) } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to get URL content with user agent", e) + Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e) "" } } 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 3b934b2a..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() @@ -242,7 +243,7 @@ object SettingsManager { * @return The HTTP port. */ fun getHttpPort(): Int { - return getSocksPort() + (if (Utils.isXray()) 0 else 1) + return getSocksPort() + if (Utils.isXray()) 0 else 1 } /** @@ -316,10 +317,10 @@ object SettingsManager { */ fun getDelayTestUrl(second: Boolean = false): String { return if (second) { - AppConfig.DelayTestUrl2 + AppConfig.DELAY_TEST_URL2 } else { MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) - ?: AppConfig.DelayTestUrl + ?: AppConfig.DELAY_TEST_URL } } @@ -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 new file mode 100644 index 00000000..37b55c2e --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt @@ -0,0 +1,107 @@ +package com.v2ray.ang.handler + +import android.content.Context +import android.os.Build +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.BuildConfig +import com.v2ray.ang.dto.CheckUpdateResult +import com.v2ray.ang.dto.GitHubRelease +import com.v2ray.ang.extension.concatUrl +import com.v2ray.ang.util.HttpUtil +import com.v2ray.ang.util.JsonUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream + +object UpdateCheckerManager { + suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) { + val url = if (includePreRelease) { + AppConfig.APP_API_URL + } else { + AppConfig.APP_API_URL.concatUrl("latest") + } + + var response = HttpUtil.getUrlContent(url, 5000) + if (response.isNullOrEmpty()) { + val httpPort = SettingsManager.getHttpPort() + response = HttpUtil.getUrlContent(url, 5000, httpPort) ?: throw IllegalStateException("Failed to get response") + } + + val latestRelease = if (includePreRelease) { + JsonUtil.fromJson(response, Array::class.java) + .firstOrNull() + ?: throw IllegalStateException("No pre-release found") + } else { + JsonUtil.fromJson(response, GitHubRelease::class.java) + } + + val latestVersion = latestRelease.tagName.removePrefix("v") + Log.i(AppConfig.TAG, "Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})") + + return@withContext if (compareVersions(latestVersion, BuildConfig.VERSION_NAME) > 0) { + val downloadUrl = getDownloadUrl(latestRelease, Build.SUPPORTED_ABIS[0]) + CheckUpdateResult( + hasUpdate = true, + latestVersion = latestVersion, + releaseNotes = latestRelease.body, + downloadUrl = downloadUrl, + isPreRelease = latestRelease.prerelease + ) + } else { + CheckUpdateResult(hasUpdate = false) + } + } + + suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) { + try { + val httpPort = SettingsManager.getHttpPort() + val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true) + ?: throw IllegalStateException("Failed to create connection") + + try { + val apkFile = File(context.cacheDir, "update.apk") + Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}") + + FileOutputStream(apkFile).use { outputStream -> + connection.inputStream.use { inputStream -> + inputStream.copyTo(outputStream) + } + } + Log.i(AppConfig.TAG, "APK download completed") + return@withContext apkFile + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to download APK: ${e.message}") + return@withContext null + } finally { + try { + connection.disconnect() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Error closing connection: ${e.message}") + } + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to initiate download: ${e.message}") + return@withContext null + } + } + + private fun compareVersions(version1: String, version2: String): Int { + val v1 = version1.split(".") + val v2 = version2.split(".") + + for (i in 0 until maxOf(v1.size, v2.size)) { + val num1 = if (i < v1.size) v1[i].toInt() else 0 + val num2 = if (i < v2.size) v2[i].toInt() else 0 + if (num1 != num2) return num1 - num2 + } + return 0 + } + + private fun getDownloadUrl(release: GitHubRelease, abi: String): String { + return release.assets.find { it.name.contains(abi) }?.browserDownloadUrl + ?: release.assets.firstOrNull()?.browserDownloadUrl + ?: throw IllegalStateException("No compatible APK found") + } +} 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/helper/SimpleItemTouchHelperCallback.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt index 44a9853b..b98129a7 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt @@ -107,7 +107,7 @@ class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter addUpdateListener { animation -> val value = animation.animatedValue as Float viewHolder.itemView.translationX = value - viewHolder.itemView.alpha = (1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD)) + viewHolder.itemView.alpha = 1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD) } interpolator = DecelerateInterpolator() duration = ANIMATION_DURATION @@ -144,4 +144,4 @@ class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter private const val SWIPE_THRESHOLD = 0.25f private const val ANIMATION_DURATION: Long = 200 } -} \ No newline at end of file +} 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 6bed1c1e..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 @@ -20,7 +20,6 @@ import androidx.annotation.RequiresApi import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.BuildConfig -import com.v2ray.ang.R import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.util.MyContextWrapper @@ -34,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 @@ -105,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) } @@ -159,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) { - resources.getStringArray(R.array.bypass_private_ip_address).forEach { + AppConfig.ROUTED_IP_LIST.forEach { val addr = it.split('/') builder.addRoute(addr[0], addr[1].toInt()) } @@ -175,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) } @@ -256,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(), @@ -270,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()) @@ -294,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) } } @@ -309,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)) @@ -349,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 648996bc..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 @@ -16,6 +16,7 @@ import com.v2ray.ang.databinding.ActivityAboutBinding 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.util.Utils import com.v2ray.ang.util.ZipUtil @@ -98,11 +99,11 @@ class AboutActivity : BaseActivity() { } binding.layoutSoureCcode.setOnClickListener { - Utils.openUri(this, AppConfig.v2rayNGUrl) + Utils.openUri(this, AppConfig.APP_URL) } binding.layoutFeedback.setOnClickListener { - Utils.openUri(this, AppConfig.v2rayNGIssues) + Utils.openUri(this, AppConfig.APP_ISSUES_URL) } binding.layoutOssLicenses.setOnClickListener { @@ -116,11 +117,11 @@ class AboutActivity : BaseActivity() { } binding.layoutTgChannel.setOnClickListener { - Utils.openUri(this, AppConfig.TgChannelUrl) + Utils.openUri(this, AppConfig.TG_CHANNEL_URL) } binding.layoutPrivacyPolicy.setOnClickListener { - Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy) + Utils.openUri(this, AppConfig.APP_PRIVACY_POLICY) } "v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also { 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 e6abbba3..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 @@ -87,9 +87,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList Action.IMPORT_QR_CODE_CONFIG -> scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) -// Action.IMPORT_QR_CODE_URL -> -// scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java)) - Action.READ_CONTENT_FROM_URI -> chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply { type = "*/*" @@ -110,8 +107,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList enum class Action { NONE, IMPORT_QR_CODE_CONFIG, - - //IMPORT_QR_CODE_URL, READ_CONTENT_FROM_URI, POST_NOTIFICATIONS } @@ -129,12 +124,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } } -// private val scanQRCodeForUrlToCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { -// if (it.resultCode == RESULT_OK) { -// importConfigCustomUrl(it.data?.getStringExtra("SCAN_RESULT")) -// } -// } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) @@ -325,7 +314,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { R.id.import_qrcode -> { - importQRcode(true) + importQRcode() true } @@ -379,26 +368,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList true } -// R.id.import_config_custom_clipboard -> { -// importConfigCustomClipboard() -// true -// } -// -// R.id.import_config_custom_local -> { -// importConfigCustomLocal() -// true -// } -// -// R.id.import_config_custom_url -> { -// importConfigCustomUrlClipboard() -// true -// } -// -// R.id.import_config_custom_url_scan -> { -// importQRcode(false) -// true -// } - R.id.export_all -> { exportAll() true @@ -462,16 +431,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList /** * import config from qrcode */ - private fun importQRcode(forConfig: Boolean): Boolean { + private fun importQRcode(): Boolean { val permission = Manifest.permission.CAMERA if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { - if (forConfig) { - scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) - } else { - //scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java)) - } + scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) } else { - pendingAction = Action.IMPORT_QR_CODE_CONFIG//if (forConfig) Action.IMPORT_QR_CODE_CONFIG else Action.IMPORT_QR_CODE_URL + pendingAction = Action.IMPORT_QR_CODE_CONFIG requestPermissionLauncher.launch(permission) } return true @@ -535,77 +500,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } -// private fun importConfigCustomClipboard() -// : Boolean { -// try { -// val configText = Utils.getClipboard(this) -// if (TextUtils.isEmpty(configText)) { -// toast(R.string.toast_none_data_clipboard) -// return false -// } -// importCustomizeConfig(configText) -// return true -// } catch (e: Exception) { -// e.printStackTrace() -// return false -// } -// } - - /** - * import config from local config file - */ -// private fun importConfigCustomLocal(): Boolean { -// try { -// showFileChooser() -// } catch (e: Exception) { -// e.printStackTrace() -// return false -// } -// return true -// } -// -// private fun importConfigCustomUrlClipboard() -// : Boolean { -// try { -// val url = Utils.getClipboard(this) -// if (TextUtils.isEmpty(url)) { -// toast(R.string.toast_none_data_clipboard) -// return false -// } -// return importConfigCustomUrl(url) -// } catch (e: Exception) { -// e.printStackTrace() -// return false -// } -// } - - /** - * import config from url - */ -// private fun importConfigCustomUrl(url: String?): Boolean { -// try { -// if (!Utils.isValidUrl(url)) { -// toast(R.string.toast_invalid_url) -// return false -// } -// lifecycleScope.launch(Dispatchers.IO) { -// val configText = try { -// HttpUtil.getUrlContentWithUserAgent(url) -// } catch (e: Exception) { -// e.printStackTrace() -// "" -// } -// launch(Dispatchers.Main) { -// importCustomizeConfig(configText) -// } -// } -// } catch (e: Exception) { -// e.printStackTrace() -// return false -// } -// return true -// } - /** * import config from sub */ @@ -755,29 +649,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } } -// /** -// * import customize config -// */ -// private fun importCustomizeConfig(server: String?) { -// try { -// if (server == null || TextUtils.isEmpty(server)) { -// toast(R.string.toast_none_data) -// return -// } -// if (mainViewModel.appendCustomConfigServer(server)) { -// mainViewModel.reloadServerList() -// toastSuccess(R.string.toast_success) -// } else { -// toastError(R.string.toast_failure) -// } -// //adapter.notifyItemInserted(mainViewModel.serverList.lastIndex) -// } catch (e: Exception) { -// ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show() -// e.printStackTrace() -// return -// } -// } - private fun setTestState(content: String?) { binding.tvTestState.text = content } @@ -812,8 +683,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList .putExtra("isRunning", mainViewModel.isRunning.value == true) ) - R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}") + 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? = null @@ -86,6 +85,10 @@ class PerAppProxyActivity : BaseActivity() { MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked) } binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false) + + binding.layoutSwitchBypassAppsTips.setOnClickListener { + Toasty.info(this, R.string.summary_pref_per_app_proxy, Toast.LENGTH_LONG, true).show() + } } override fun onPause() { @@ -157,7 +160,7 @@ class PerAppProxyActivity : BaseActivity() { toast(R.string.msg_downloading_content) binding.pbWaiting.show() - val url = AppConfig.androidpackagenamelistUrl + val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL lifecycleScope.launch(Dispatchers.IO) { var content = HttpUtil.getUrlContent(url, 5000) if (content.isNullOrEmpty()) { diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt index 104e5049..e585cff8 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt @@ -7,8 +7,6 @@ import android.os.Bundle import android.util.Log import android.view.Menu import android.view.MenuItem -import android.view.View -import android.widget.AdapterView import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope @@ -67,15 +65,9 @@ class RoutingSettingActivity : BaseActivity() { mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) - val found = Utils.arrayFind(routing_domain_strategy, MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "") - found.let { binding.spDomainStrategy.setSelection(if (it >= 0) it else 0) } - binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onNothingSelected(parent: AdapterView<*>?) { - } - - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, routing_domain_strategy[position]) - } + binding.tvDomainStrategySummary.text = getDomainStrategy() + binding.layoutDomainStrategy.setOnClickListener { + setDomainStrategy() } } @@ -98,6 +90,22 @@ class RoutingSettingActivity : BaseActivity() { else -> super.onOptionsItemSelected(item) } + private fun getDomainStrategy(): String { + return MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: routing_domain_strategy.first() + } + + private fun setDomainStrategy() { + android.app.AlertDialog.Builder(this).setItems(routing_domain_strategy.asList().toTypedArray()) { _, i -> + try { + val value = routing_domain_strategy[i] + MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value) + binding.tvDomainStrategySummary.text = value + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to set domain strategy", e) + } + }.show() + } + private fun importPredefined() { AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i -> AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt index 58b80d29..e9bdad66 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt @@ -2,7 +2,6 @@ package com.v2ray.ang.ui import android.os.Bundle import android.text.TextUtils -import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View @@ -19,7 +18,6 @@ import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE import com.v2ray.ang.AppConfig.REALITY import com.v2ray.ang.AppConfig.TLS import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 -import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6 import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU import com.v2ray.ang.R import com.v2ray.ang.dto.EConfigType @@ -328,7 +326,7 @@ class ServerActivity : BaseActivity() { et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty()) et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0") et_local_address?.text = Utils.getEditable( - config.localAddress ?: "$WIREGUARD_LOCAL_ADDRESS_V4,$WIREGUARD_LOCAL_ADDRESS_V6" + config.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4 ) et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU) } else if (config.configType == EConfigType.HYSTERIA2) { @@ -421,7 +419,7 @@ class ServerActivity : BaseActivity() { et_public_key?.text = null et_reserved1?.text = Utils.getEditable("0,0,0") et_local_address?.text = - Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}") + Utils.getEditable(WIREGUARD_LOCAL_ADDRESS_V4) et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU) return true } 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 9423310c..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) } @@ -161,7 +162,7 @@ class SettingsActivity : BaseActivity() { } delayTestUrl?.setOnPreferenceChangeListener { _, any -> val nval = any as String - delayTestUrl?.summary = if (nval == "") AppConfig.DelayTestUrl else nval + delayTestUrl?.summary = if (nval == "") AppConfig.DELAY_TEST_URL else nval true } mode?.setOnPreferenceChangeListener { _, newValue -> @@ -202,7 +203,7 @@ class SettingsActivity : BaseActivity() { remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY) domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT) dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS) - delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl) + delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DELAY_TEST_URL) initSharedPreference() } @@ -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( @@ -364,6 +367,6 @@ class SettingsActivity : BaseActivity() { } fun onModeHelpClicked(view: View) { - Utils.openUri(this, AppConfig.v2rayNGWikiMode) + Utils.openUri(this, AppConfig.APP_WIKI_MODE) } } 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 3f5f9280..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/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt index f911bd89..efc0a39c 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt @@ -21,9 +21,10 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.v2ray.ang.AppConfig import com.v2ray.ang.R -import com.v2ray.ang.databinding.ActivitySubSettingBinding +import com.v2ray.ang.databinding.ActivityUserAssetBinding import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding import com.v2ray.ang.dto.AssetUrlItem +import com.v2ray.ang.extension.concatUrl import com.v2ray.ang.extension.toTrafficString import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toastError @@ -42,8 +43,7 @@ import java.text.DateFormat import java.util.Date class UserAssetActivity : BaseActivity() { - private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) } - + private val binding by lazy { ActivityUserAssetBinding.inflate(layoutInflater) } val extDir by lazy { File(Utils.userAssetPath(this)) } val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat") @@ -90,6 +90,11 @@ class UserAssetActivity : BaseActivity() { binding.recyclerView.layoutManager = LinearLayoutManager(this) addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) binding.recyclerView.adapter = UserAssetAdapter() + + binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources() + binding.layoutGeoFilesSources.setOnClickListener { + setGeoFilesSources() + } } override fun onResume() { @@ -111,6 +116,22 @@ class UserAssetActivity : BaseActivity() { else -> super.onOptionsItemSelected(item) } + private fun getGeoFilesSources(): String { + return MmkvManager.decodeSettingsString(AppConfig.PREF_GEO_FILES_SOURCES) ?: AppConfig.GEO_FILES_SOURCES.first() + } + + private fun setGeoFilesSources() { + AlertDialog.Builder(this).setItems(AppConfig.GEO_FILES_SOURCES.toTypedArray()) { _, i -> + try { + val value = AppConfig.GEO_FILES_SOURCES[i] + MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value) + binding.tvGeoFilesSourcesSummary.text = value + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to set geo files sources", e) + } + }.show() + } + private fun showFileChooser() { val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { Manifest.permission.READ_MEDIA_IMAGES @@ -264,7 +285,8 @@ class UserAssetActivity : BaseActivity() { list.add( Utils.getUuid() to AssetUrlItem( it, - AppConfig.GeoUrl + it + String.format(AppConfig.GITHUB_DOWNLOAD_URL, getGeoFilesSources()).concatUrl(it), + locked = true ) ) } @@ -315,7 +337,7 @@ class UserAssetActivity : BaseActivity() { holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found) } - if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) { + if (item.second.locked == true) { holder.itemUserAssetBinding.layoutEdit.visibility = GONE //holder.itemUserAssetBinding.layoutRemove.visibility = GONE } else { diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt index 956000de..577698ea 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt @@ -25,7 +25,7 @@ object AppManagerUtil { val appName = applicationInfo.loadLabel(packageManager).toString() val appIcon = applicationInfo.loadIcon(packageManager) ?: continue - val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0 + val isSystemApp = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM > 0 val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0) apps.add(appInfo) @@ -33,4 +33,8 @@ object AppManagerUtil { return@withContext apps } -} \ No newline at end of file + + fun getLastUpdateTime(context: Context): Long = + context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime + +} 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 b2a64777..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 @@ -7,20 +7,26 @@ import com.v2ray.ang.BuildConfig import com.v2ray.ang.util.Utils.encode import com.v2ray.ang.util.Utils.urlDecode import java.io.IOException -import java.net.* -import java.util.* +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 { - val url = URI(str) + fun toIdnUrl(str: String): String { + val url = URL(str) val host = url.host val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED) if (host != asciiHost) { @@ -30,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 d83a9989..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 @@ -17,10 +17,12 @@ import android.webkit.URLUtil import androidx.core.content.ContextCompat import androidx.core.net.toUri import com.v2ray.ang.AppConfig -import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.LOOPBACK +import com.v2ray.ang.BuildConfig import java.io.IOException +import java.net.InetAddress import java.net.ServerSocket +import java.net.URI import java.net.URLDecoder import java.net.URLEncoder import java.util.Locale @@ -196,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. * @@ -461,13 +478,23 @@ object Utils { fun isValidSubUrl(value: String?): Boolean { if (value.isNullOrEmpty()) return false - return try { - URLUtil.isHttpsUrl(value) || - (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK)) + try { + if (URLUtil.isHttpsUrl(value)) return true + if (URLUtil.isHttpUrl(value)) { + if (value.contains(LOOPBACK)) return true + + //Check private ip address + val uri = URI(fixIllegalUrl(value)) + if (isIpAddress(uri.host)) { + AppConfig.PRIVATE_IP_LIST.forEach { + if (isIpInCidr(uri.host, it)) return true + } + } + } } catch (e: Exception) { Log.e(AppConfig.TAG, "Failed to validate subscription URL", e) - false } + return false } /** @@ -486,7 +513,58 @@ object Utils { * * @return True if the package is Xray, false otherwise. */ - fun isXray(): Boolean = ANG_PACKAGE.startsWith("com.v2ray.ang") + fun isXray(): Boolean = BuildConfig.APPLICATION_ID.startsWith("com.v2ray.ang") + /** + * Check if it is the Google Play version. + * + * @return True if the package is Google Play, false otherwise. + */ + fun isGoogleFlavor(): Boolean = BuildConfig.FLAVOR == "playstore" + + /** + * Converts an InetAddress to its long representation + * + * @param ip The InetAddress to convert + * @return The long representation of the IP address + */ + private fun inetAddressToLong(ip: InetAddress): Long { + val bytes = ip.address + var result: Long = 0 + for (i in bytes.indices) { + result = result shl 8 or (bytes[i].toInt() and 0xff).toLong() + } + return result + } + + /** + * Check if an IP address is within a CIDR range + * + * @param ip The IP address to check + * @param cidr The CIDR notation range (e.g., "192.168.1.0/24") + * @return True if the IP is within the CIDR range, false otherwise + */ + fun isIpInCidr(ip: String, cidr: String): Boolean { + try { + if (!isIpAddress(ip)) return false + + // Parse CIDR (e.g., "192.168.1.0/24") + val (cidrIp, prefixLen) = cidr.split("/") + val prefixLength = prefixLen.toInt() + + // Convert IP and CIDR's IP portion to Long + val ipLong = inetAddressToLong(InetAddress.getByName(ip)) + val cidrIpLong = inetAddressToLong(InetAddress.getByName(cidrIp)) + + // Calculate subnet mask (e.g., /24 → 0xFFFFFF00) + val mask = if (prefixLength == 0) 0L else (-1L shl (32 - prefixLength)) + + // Check if they're in the same subnet + return (ipLong and mask) == (cidrIpLong and mask) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to check if IP is in CIDR", e) + return false + } + } } 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/drawable-night/ic_check_update_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_check_update_24dp.xml new file mode 100644 index 00000000..5a7c2fd0 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_check_update_24dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml new file mode 100644 index 00000000..b0f163c6 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml index 55db5240..0fc67600 100644 --- a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml +++ b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml @@ -5,7 +5,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" - android:orientation="vertical"> + android:orientation="vertical" + tools:context=".ui.PerAppProxyActivity"> + + + + + + @@ -83,7 +101,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical" - app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" - tools:context=".ui.PerAppProxyActivity" /> + app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" /> \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_check_update.xml b/V2rayNG/app/src/main/res/layout/activity_check_update.xml new file mode 100644 index 00000000..29345075 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_check_update.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml b/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml index e04cff2a..53a49fec 100644 --- a/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml +++ b/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" - tools:context=".ui.SubSettingActivity"> + tools:context=".ui.RoutingEditActivity"> + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:gravity="center|start" + android:orientation="vertical" + android:padding="@dimen/padding_spacing_dp16"> + android:text="@string/routing_settings_domain_strategy" + android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> - + android:maxLines="1" + android:textAppearance="@style/TextAppearance.AppCompat.Small" /> + tools:context=".ui.SubEditActivity"> @@ -138,6 +138,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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" /> + - - - - - - - - - - - - - - - - - - - - - - - الكتابة يدويًا [Trojan] الكتابة يدويًا [Wireguard] Type manually[Hysteria2] - تكوين مخصص - استيراد تكوين مخصص من الحافظة - استيراد تكوين مخصص من الجهاز - استيراد تكوين مخصص من عنوان URL - استيراد تكوين مخصص مسح عنوان URL تأكيد الحذف؟ Please test before deleting! Confirm delete ? ملاحظات @@ -127,6 +122,7 @@ إضافة عنوان URL للأصل الملف غير موجود الملاحظات موجودة بالفعل + Geo files source (optional) جار التحميل @@ -145,6 +141,7 @@ الإعدادات إعدادات متقدمة + إعدادات النواة إعدادات VPN الوكيل لكل تطبيق عام: التطبيق المحدد هو وكيل، غير المحدد اتصال مباشر؛ \nوضع التجاوز: التطبيق المحدد متصل مباشرة، غير المحدد وكيل. \nخيار تحديد تطبيق الوكيل تلقائيًا في القائمة @@ -185,6 +182,8 @@ VPN DNS (IPv4/v6 فقط) Does VPN bypass LAN + VPN Interface Address + DNS المحلي (اختياري) DNS @@ -242,6 +241,7 @@ فاصل التحديث التلقائي (بالدقائق، الحد الأدنى للقيمة 15) مستوى السجل + Outbound domain pre-resolve method الوضع انقر هنا للحصول على مزيد من المساعدة اللغة @@ -262,9 +262,10 @@ Remarks regular filter تفعيل التحديث تفعيل التحديث التلقائي - Previous proxy remarks - Next proxy remarks - The remarks exists and is unique + Allow insecure HTTP address + Previous proxy configuration remarks + Next proxy configuration remarks + The configuration remarks exists and is unique تحديث الاشتراك (أول خطوة) Tcping لجميع الإعدادات اختبر جميع الإعدادات (3) @@ -314,6 +315,13 @@ فاصل الجزء (الحد الأدنى - الحد الأقصى) تفعيل الجزء + Check for update + Already on the latest version + New version found: %s + Update now + Check Pre-release + Checking for update… + رمز استجابة سريعة (QRcode) تصدير إلى الحافظة @@ -350,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 5f37e6de..f36c9d3a 100644 --- a/V2rayNG/app/src/main/res/values-bn/strings.xml +++ b/V2rayNG/app/src/main/res/values-bn/strings.xml @@ -35,11 +35,6 @@ ম্যানুয়ালি টাইপ করুন [Trojan] ম্যানুয়ালি টাইপ করুন [Wireguard] Type manually[Hysteria2] - কাস্টম কনফিগারেশন - ক্লিপবোর্ড থেকে কাস্টম কনফিগারেশন আমদানি করুন - স্থানীয়ভাবে কাস্টম কনফিগারেশন আমদানি করুন - URL থেকে কাস্টম কনফিগারেশন আমদানি করুন - কাস্টম কনফিগারেশন স্ক্যান URL আমদানি করুন মুছে ফেলুন নিশ্চিত করুন? Please test before deleting! Confirm delete ? মন্তব্য @@ -126,6 +121,7 @@ অ্যাসেট URL যোগ করুন ফাইল খুঁজে পাওয়া যায়নি মন্তব্য ইতিমধ্যে বিদ্যমান + Geo files source (optional) লোড হচ্ছে @@ -143,6 +139,7 @@ সেটিংস এডভান্সড সেটিংস + কোর সেটিংস VPN সেটিংস প্রতি-অ্যাপ প্রক্সি সাধারণ: চেকড অ্যাপ প্রক্সি, আনচেকড সরাসরি সংযোগ; \nবাইপাস মোড: চেকড অ্যাপ সরাসরি সংযুক্ত, আনচেকড প্রক্সি। \nমেনুতে প্রক্সি অ্যাপ্লিকেশন স্বয়ংক্রিয়ভাবে নির্বাচন করার বিকল্প @@ -185,6 +182,8 @@ VPN DNS (শুধুমাত্র IPv4/v6) Does VPN bypass LAN + VPN Interface Address + ঘরোয়া DNS (ঐচ্ছিক) DNS @@ -242,6 +241,7 @@ অটো আপডেট ইন্টারভ্যাল (মিনিট, সর্বনিম্ন মান ১৫) লগ স্তর + Outbound domain pre-resolve method মোড আরো সাহায্যের জন্য ক্লিক করুন ভাষা @@ -262,9 +262,10 @@ Remarks regular filter আপডেট সক্রিয় করুন স্বয়ংক্রিয় আপডেট সক্রিয় করুন - Previous proxy remarks - Next proxy remarks - The remarks exists and is unique + Allow insecure HTTP address + Previous proxy configuration remarks + Next proxy configuration remarks + The configuration remarks exists and is unique সাবস্ক্রিপশন আপডেট সব কনফিগারেশন TCPing সব কনফিগারেশন প্রকৃত বিলম্ব @@ -313,6 +314,13 @@ ফ্র্যাগমেন্ট ইন্টারভ্যাল (ন্যূনতম-সর্বাধিক) ফ্র্যাগমেন্ট সক্রিয় করুন + Check for update + Already on the latest version + New version found: %s + Update now + Check Pre-release + Checking for update… + QR কোড ক্লিপবোর্ডে রপ্তানি করুন @@ -355,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 65d501b1..90e5cc26 100644 --- a/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml +++ b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml @@ -7,9 +7,9 @@ بستن نومگه کشاری مووفقیت من جاگورویی داده جاگورویی داده ٱنجوم نگرؽڌ - Please pull down to refresh! + سی وانۊ کردن، بکشینس بلم! - + واڌاشتن گرؽڌن موجوز مومکن نؽڌ گرؽڌن موجوز وارسۊوی مومکن نؽڌ @@ -26,7 +26,7 @@ پاک کردن کانفیگ و من ٱووردن کانفیگ ز QRcode و من ٱووردن کانفیگ ز کلیپ بورد - Import config from locally + و من ٱووردن کانفیگ ز مهلی هؽل دستی[VMess] هؽل دستی[VLESS] هؽل دستی[Shadowsocks] @@ -35,17 +35,12 @@ هؽل دستی[Trojan] هؽل دستی[Wireguard] هؽل دستی[Hysteria2] - کانفیگ سفارشی - کانفیگ سفارشین ز کلیپ بورد و من بیار - کانفیگ سفارشین ز مهلی و من بیار - کانفیگ سفارشین ز آدرس اینترنتی و من بیار - نشۊوی اینترنتی اسکن کانفیگ سفارشین بزݩ پاک بۊ؟ پؽش ز پاک کردن کانفیگ نا موئتبر واجۊری کوݩ! پاک کردن کانفیگن قوۊل اکۊنی؟ نیشتنا - آدرس + نشۊوی پورت - نوم من توری + نوم منتوری شناسه جایگۊزین ٱمنیت شبکه @@ -73,21 +68,21 @@ Alpn اجازه نا ٱمن SNI - آدرس + نشۊوی پورت رزم ٱمنیت رزم (اختیاری) - نوم من توری (اختیاری) + نوم منتوری (اختیاری) رزم نگاری جریان کیلیت پوی وولاتی - کیلیت رمز ناهاڌن ازاف (اختیاری) + کیلیت رزم ناهاڌن ازاف (اختیاری) ShortID SpiderX کیلیت سیخومی Reserved(اختیاری، وا کاما ز یک جوڌا ابۊن) - آدرس مهلی (اختیاری IPv4/IPv6، وا کاما ز یک جوڌا ابۊن) + نشۊوی مهلی (اختیاری IPv4/IPv6، وا کاما ز یک جوڌا ابۊن) Mtu(اختیاری، پؽش فرز 1420) وا مووفقیت ٱنجوم وابی شکست خرد @@ -101,11 +96,11 @@ موئتوا هیچ داده ای من کلیپ بورد وۊجۊڌ نڌاره نشۊوی اینترنتی نا موئتبر هڌ - آدرس اشتراک پوروتوکول نا ٱمن HTTP ن و کار مبرین + نشۊوی اشتراک پوروتوکول نا ٱمن HTTP ن و کار مبرین موتمعن بۊین ک پورت وۊرۊڌی وا سامووا ی جۊر هڌ کانفیگ زبال نؽڌ هاست(SNI)(اختیاری) - ای کار ممنۊ هڌ + ای کار ممنۊع هڌ رزم obfs پورت گوم (درگا سرورن ز نۊ هؽل اکونه) فاسله پورت گوم (سانیه) @@ -121,29 +116,31 @@ ازاف کردن فایل ازاف کردن لینگ اسکن QRcode - آدرس اینترنتی + نشۊوی اینترنتی دانلود فایلا - آدرس اینترنتی دارایین ازاف کۊنین + نشۊوی اینترنتی دارایین ازاف کۊنین فایلن نجوست - ائزارات ز زیتر بیڌسۉݩ + نوم ز زیتر بیڌس + بونچک فایلا جوقرافیایی (اختیاری) - هون بار ونی بۊ + هونی بار ونی بۊ پیتینیڌن پسند پوی - رزمان بزنین + رزما ن بزنین هالت Bypass پسند خوتکار پروکسی برنومه موئتوا هونی دانلود ابۊن و در کشیڌن من کلیپ بورد و من ٱووردن ز کلیپ بورد - Per-app settings - Enable per-app + سامووا ب تفکیک برنومه + ر وندن ب تفکیک برنومه سامووا سامووا پؽش رئڌه + سامووا هسته سامووا VPN پروکسی و ری برنومه پوی وولاتی: برنومه واجۊری بیڌه پروکسی هڌ، منپیز موستقیم بؽ نشووه هڌ. هالت دور زیڌن: برنومه نشووک ناڌه موستقیمن منپیز هڌ، پروکسی نشووک زیڌه نؽڌ. گۊزینه پسند خوتکار برنومه پروکسی من نومگه @@ -152,10 +149,10 @@ سامووا Mux ر وندن Mux - زل تر، ٱما گاشڌ منپیز زی قت بۊ بارت دؽوۉداری، TCP، UDP و QUIC ن ای لم سفارشی کۊنین. + زل تر، ٱما گاشڌ منپیز زی قت بۊ\nمخزن ترافیک TCP وا 8 منپیز پؽش فرز، بارت دؽوۉداری UDP وو QUIC ن ای لم سفارشی کۊنین. منپیزا TCP (تلایه منجا 1-1024) منپیزا XUDP (تلایه منجا 1-1024) - دؽوۉداری QUIC من تونل mux + دؽوۉداری QUIC من تۊنل mux رڌ کردن موجاز @@ -168,34 +165,36 @@ ر وندن Sniffing دامنه sniff ن ز کتن امتهۉݩ کۊنین (پؽش فرز رۊشن) ر وندن routeOnly - ز نوم دامنه sniffed تینا سی تور جوستن استفاڌه کۊنین وو آدرس مورد نزرن و عونوان آدرس IP ووردارین. + ز نوم دامنه sniffed تینا سی تور جوستن استفاڌه کۊنین وو نشۊوی مۉرد نزرن و عونوان نشۊوی IP ووردارین. ر وندن DNS مهلی - DNS پردازشت وابیڌه و دس هسته ماژول DNS (پؽشنهاڌ ابۊ، ٱر نیاز هڌ ک جوستن تور وو ولات ٱسلین دور زنی) + درخاستا DNS و هسته و من ایان وو و دست ماژول DNS پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین فعال بۊ) ر وندن DNS جئلی - DNS مهلی آدرسا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من یقرد ز برنومیل کار نکونه) + 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) + نشۊوی اینترنتی آزمایش تئخیر واقعی (http/https) نشۊوی اینترنتی هشتن منپیزا ز شبکه مهلی - پوی دسگایل ترن وا آدرس IP ایسا، ز ر socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل اعتماد فعال بۊ تا ز منپیز غیر موجاز جلو گری بۊ. + پوی دسگایل ترن وا نشۊوی IP ایسا، ز ر socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل اعتماد فعال بۊ تا ز منپیز غیر موجاز جلو گری بۊ. منپیزا ز شبکه مهلی ن موجار کۊنین، موتمعن بۊین ک من ی شبکه قابل ائتماڌ هڌین. اجازه نا ٱمن @@ -208,20 +207,20 @@ پورت DNS مهلی قوۊل کردن پاک کردن کانفیگ - سی پاک وابیڌن فایل کانفیگ نیاز به قوۊل کردن دووارته ز سمت منتور هڌ + سی پاک وابیڌن فایل کانفیگ نیاز به قوۊل کردن دووارته ز سمت منتور هڌ. زی اسکنن ر ون - شؽواتگرن سی اسکن، زی مجال ر وندن بۊگۊشین، اندی ترین کودن اسکن کۊنین یا شؽواتی ن منه نوار ٱوزار پسند کۊنین. + شؽواتگرن سی اسکن، زی مجال ر وندن بۊگۊشین، ٱندی ترین کودن اسکن کۊنین یا شؽواتی ن منه نوار ٱوزار پسند کۊنین. پروکسی HTTP ن و VPN ازاف کۊنین - پروکسی HTTP ن موسقیمن ز (مۊرۊرگر/ی قرد ز برنومیل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ. + پروکسی HTTP ن موسقیمن ز (مۊرۊرگر/ی قرد ز برنومه یل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ. - Enable double column display - The profile list is displayed in double columns, allowing more content to be displayed on the screen. You need to restart the application to take effect. + ر وندن نشۉݩ داڌن دو سۊتۊنی + نومگه نمایه یل من دو سۊتۊن نشۉݩ داڌه ابۊن وو چینۉ ترین موئتوا بیشتری ن سیل کۊنین. سی ر وستن، وا برنومه ن ز نۊ ر ونین. فشناڌن منشڌ - فشناڌن منشڌ یا داسوو موشکلا من Github + فشناڌن منشڌ یا داسووݩ موشکلا من Github ٱووڌن من جرگه تلگرام برنومه تلگرامن نجوست هریم سیخومی @@ -242,6 +241,7 @@ فاسله ورۊ کردن خوتکار (اقلن وا 15 دؽقه بۊ) سئت داسووا + بارت پؽش هل دامنه دری هالت سی دووسمندیا وو هیاری بیشتر، ری ای هؽل بزݩ زۉݩ @@ -262,16 +262,17 @@ نوم موستعار فیلتر فعال بیڌن ورۊ کردن فعال بیڌن ورۊ کردن خوتکار + موجاز کردن نشۊوی HTTP نا ٱمن نوم موستعار پروکسی دیندایی نوم موستعار پروکسی نیایی موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی ورۊ کردن اشتراک جرگه سکویی Tcping کانفیگا جرگه سکویی تئخیر واقعی کانفیگا جرگه سکویی - Asset files - ترتیب و ری نتیجیل آزمایش + فایلا بونچک جوقرافیایی + ترتیب و ری نتیجه یل آزمایش فیلتر کردن کانفیگا - پوی جرگیل + پوی جرگه یل کانفیگ پاک کردن %d کانفیگ تکراری پاک کردن %d کانفیگ @@ -307,7 +308,7 @@ منپیزن واجۊری کوݩ هونی آزمایش ابۊ… %d کانفیگ هونی آزمایش ابۊ... - مووفق بی: منپیز HTTP %dms تۊل کشی + مووفق بی: منپیز %dms تۊل کشی منپیز و اینترنتن نجوست: %s اینترنت من دسرس نؽ کود ختا: #%d @@ -316,11 +317,18 @@ اشتراک وا مووفقیت زفت زابی اشتراک زفت نوابی - سامووا Fragment - Fragment Packets - Fragment Length (min-max) - Fragment Interval (min-max) - ر وندن Fragment + سامووا فرگمنت + کتنا فرگمنت + تۊل کتنا فرگمنت (هدقل-هدکسر) + فاسله منجا کتنا فرگمنت (هدقل-هدکسر) + ر وندن فرگمنت + + واجۊری سی ورۊ رسۊوی + سکو نوسخه دیندایی پۊرنیڌه هڌ + نوسخه نۊ ن جوست: %s + سکو ورۊ رسۊوی کۊنین + واجۊری نوسخه یل پؽش ز تیجنیڌن + ورۊ رسۊوی ن هونی واجۊری اکونه... QRcode @@ -330,10 +338,10 @@ QRcode - Export to clipboard - Export full configuration to clipboard - Edit - Delete + و در کشیڌن من کلیپ بورد + و در کشیڌن پوی کانفیگ من کلیپ بورد + آلشت + پاک کردن @@ -365,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 790ed3de..081571a3 100644 --- a/V2rayNG/app/src/main/res/values-fa/strings.xml +++ b/V2rayNG/app/src/main/res/values-fa/strings.xml @@ -35,11 +35,6 @@ تایپ دستی[TROJAN] ‌تایپ دستی[WIREGUARD] تایپ دستی[HYSTERIA2] - کانفیگ سفارشی - کانفیگ سفارشی را از کلیپ ‌بورد وارد کنید - کانفیگ سفارشی را به صورت محلی وارد کنید - کانفیگ سفارشی را از طریق نشانی اینترنتی وارد کنید - نشانی اینترنتی اسکن کانفیگ سفارشی را وارد کنید حذف شود؟ لطفا قبل از حذف کانفیگ نامعتبر بررسی کنید! حذف کانفیگ را تایید می کنید؟ ملاحظات @@ -124,6 +119,7 @@ آدرس اینترنتی را اضافه کنید فایل پیدا نشد نام قبلاً وجود دارد + منبع فایل های جغرافیایی (اختیاری) بارگذاری @@ -141,6 +137,7 @@ تنظیمات تنظیمات پیشرفته + تنظیمات هسته تنظیمات VPN پروکسی به تفکیک برنامه عمومی: برنامه انتخاب شده از طریق یک پروکسی متصل می شود، برنامه انتخاب نشده مستقیماً متصل می شود. \nحالت دور زدن: برنامه انتخاب شده مستقیماً متصل می شود، برنامه انتخاب نشده از طریق یک پروکسی متصل می شود. \nانتخاب خودکار برنامه های پراکسی در منو امکان پذیر است. @@ -175,7 +172,7 @@ دی ان اس محلی آدرس های آیپی جعلی را بر می گرداند (سریع تر می باشد و تاخیر را کاهش می دهد اما ممکن است برای برخی از برنامه ها کار نکند) ترجیح دادن IPV6 - ترجیح دادن نشانی و مسیر های IPv6 + مسیرهای IPv6 را فعال کنید و آدرس‌های IPv6 را ترجیح دهید DNS از راه دور (اختیاری) (udp/tcp/https/quic) DNS @@ -183,6 +180,8 @@ VPN DNS (فقط IPv4/v6) آیا VPN از شبکه محلی عبور می کند؟ + VPN Interface Address + DNS داخلی (اختیاری) DNS @@ -239,6 +238,7 @@ اشتراک های خود را به طور خودکار با فاصله زمانی در پس زمینه به روز کنید. بسته به دستگاه، این ویژگی ممکن است همیشه کار نکند. فاصله به‌ روزرسانی خودکار ( حداقل مقدار ، 15 دقیقه ) سطح گزارشات + Outbound domain pre-resolve method حالت برای اطلاعات و راهنمایی بیشتر، روی این متن کلیک کنید زبان @@ -259,6 +259,7 @@ نام مستعار فیلتر فعال کردن به‌روزرسانی فعال سازی به‌روزرسانی خودکار + مجاز کردن آدرس HTTP ناامن نام مستعار پروکسی قبلی نام مستعار پروکسی بعدی لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است @@ -304,7 +305,7 @@ اتصال را بررسی کنید در حال آزمایش... تست کردن %d کانفیگ… - موفقیت: اتصال HTTP %dms طول کشید + موفقیت: اتصال %dms طول کشید اتصال به اینترنت شناسایی نشد: %s اینترنت در دسترس نیست کد خطا: #%d @@ -319,6 +320,13 @@ فاصله بین بسته های فرگمنت (حداقل-حداکثر) فعال کردن فرگمنت + بررسی به روز رسانی + در حال حاضر آخرین نسخه نصب شده است + نسخه جدید پیدا شد: %s + اکنون به روز رسانی کنید + بررسی نسخه پیش از انتشار + در حال بررسی برای به‌روزرسانی… + QRcode خروجی گرفتن در کلیپ‌ بورد @@ -364,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 46a0a5f3..c71054a0 100644 --- a/V2rayNG/app/src/main/res/values-ru/strings.xml +++ b/V2rayNG/app/src/main/res/values-ru/strings.xml @@ -35,11 +35,6 @@ Ручной ввод Trojan Ручной ввод WireGuard Ручной ввод Hysteria2 - Другой профиль - Импорт из буфера обмена - Импорт из файла - Импорт из URL - Импорт сканированием URL Подтверждаете удаление? Выполните проверку перед удалением! Подтверждаете удаление? Название @@ -126,6 +121,7 @@ Добавить URL ресурса Файл не найден Название уже существует + Источник геофайлов (необязательно) Загрузка… @@ -143,6 +139,7 @@ Настройки Расширенные настройки + Настройки ядра Настройки VPN Прокси для выбранных приложений Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню. @@ -176,7 +173,7 @@ Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями) Предпочитать IPv6 - Предпочитать IPv6-адреса и маршрутизацию + Использовать маршрутизацию IPv6 предпочитать IPv6-адреса Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно) DNS @@ -184,6 +181,8 @@ VPN DNS (только IPv4/v6) VPN пропускает LAN + VPN частный IP + Внутренняя DNS (необязательно) DNS @@ -241,6 +240,7 @@ Интервал автообновления (минут, не менее 15) Подробность ведения журнала + Outbound domain pre-resolve method Режим Нажмите для получения дополнительной информации Язык @@ -261,9 +261,10 @@ Название фильтра Использовать обновление Использовать автообновление - Название предыдущего прокси - Название следующего прокси - Название должно существовать и быть уникальным + Разрешать незащищённые HTTP-адреса + Предыдущая конфигурация прокси + Следующая конфигурация прокси + Конфигурация должна быть уникальной Обновить подписку группы Проверка профилей группы Время отклика профилей группы @@ -306,7 +307,7 @@ Проверить подключение Проверка… Проверка профилей (%d) - Успешно: HTTP-соединение заняло %d мс + Успешно: соединение заняло %d мс Сбой проверки интернет-соединения: %s Интернет недоступен Код ошибки: #%d @@ -321,6 +322,13 @@ Интервал фрагментов (от - до) Использовать фрагментирование + Проверить обновление + Установлена последняя версия + Найдена новая версия: %s + Обновить + Искать предварительный выпуск + Проверка обновления… + QR-код Экспорт в буфер обмена @@ -364,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 0be072c2..86238b79 100644 --- a/V2rayNG/app/src/main/res/values-vi/strings.xml +++ b/V2rayNG/app/src/main/res/values-vi/strings.xml @@ -35,11 +35,6 @@ Nhập thủ công [Trojan] Nhập thủ công [WireGuard] Type manually[Hysteria2] - Nâng cao / Cấu hình tùy chỉnh - Nhập cấu hình tùy chỉnh từ Clipboard - Nhập cấu hình tùy chỉnh từ Tệp - Nhập cấu hình tùy chỉnh từ URL - Nhập cấu hình tùy chỉnh quét URL Xác nhận xóa? Please test before deleting! Confirm delete ? Tên cấu hình @@ -124,6 +119,7 @@ Thêm URL nội dung Không tìm thấy tập tin! Nhận xét đã tồn tại! + Geo files source (optional) Đang tải... @@ -142,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. @@ -185,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 @@ -242,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ữ @@ -262,9 +262,10 @@ Remarks regular filter Sử dụng gói đăng ký này Bật tự động cập nhật - Previous proxy remarks - Next proxy remarks - The remarks exists and is unique + Allow insecure HTTP address + Previous proxy configuration remarks + Next proxy configuration remarks + The configuration remarks exists and is unique Cập nhật các gói đăng ký Ping tất cả máy chủ Kiểm tra HTTP tất cả máy chủ @@ -309,6 +310,19 @@ Nhập gói đăng ký thành công! Nhập gói đăng ký không thành công! + Fragment Settings + Fragment Packets + Fragment Length (min-max) + Fragment Interval (min-max) + Enable Fragment + + Check for update + Already on the latest version + 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) Sao chép vào Clipboard @@ -340,16 +354,16 @@ Sáng Tối - Fragment Settings - Fragment Packets - Fragment Length (min-max) - Fragment Interval (min-max) - Enable Fragment - Follow config Bypass 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 dedfdb86..a8eec856 100644 --- a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml @@ -35,11 +35,6 @@ 手动输入 [Trojan] 手动输入 [Wireguard] 手动输入 [Hysteria2] - 自定义配置 - 从剪贴板导入自定义配置 - 从本地导入自定义配置 - 剪贴板 URL 导入自定义配置 - 扫描 URL 导入自定义配置 确认删除? 删除前请先测试!确认删除? 别名 (remarks) @@ -124,6 +119,7 @@ 添加资产网址 文件未找到 备注已经存在 + Geo 文件来源 (可选) 正在加载 @@ -141,6 +137,7 @@ 设置 进阶设置 + 核心设置 VPN 设置 分应用 常规: 勾选的 App 被代理, 未勾选的直连;\n绕行模式: 勾选的 App 直连, 未勾选的被代理.\n不明白者在菜单中选择自动选中需代理应用 @@ -182,6 +179,8 @@ VPN DNS (仅支持 IPv4/v6) VPN 是否绕过局域网 + VPN 接口地址 + 境内 DNS (可选) DNS @@ -239,6 +238,7 @@ 自动更新间隔(分钟,最小值 15) 日志级别 + Outbound 域名预解析方式 模式 点此查看更多帮助 语言 @@ -259,9 +259,10 @@ 别名正则过滤 启用更新 启用自动更新 - 前置代理别名 - 落地代理別名 - 请确保别名存在并唯一 + 允许不安全的 HTTP 地址 + 前置代理配置文件别名 + 落地代理配置文件別名 + 请确保配置文件别名存在并唯一 更新当前组订阅 测试当前组配置 Tcping 测试当前组配置真连接 @@ -313,6 +314,13 @@ 分片间隔(最小 - 最大) 启用分片(Fragment) + 检查更新 + 目前已是最新版本 + 发现新版本: %s + 立即更新 + 检查 Pre-release + 正在检查更新中… + 二维码 导出至剪贴板 @@ -356,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 8861d19c..f8b938c5 100644 --- a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml @@ -35,11 +35,6 @@ 手動鍵入 [Trojan] 手動鍵入 [Wireguard] 手動鍵入 [Hysteria2] - 自訂設定 - 從剪貼簿匯入自訂設定 - 從本地匯入自訂設定 - 從 URL 匯入自訂設定 - 掃描 URL 匯入自訂設定 確定刪除? 刪除前請先測試!確認刪除? 備註 @@ -124,6 +119,7 @@ 新增資產網址 文件未找到 備註已經存在 + Geo 檔案來源 (可選) 載入 @@ -142,6 +138,7 @@ 設定 進階 + 核心設定 VPN 設定 Proxy 個別應用程式 常規:勾選的 App 啟用 Proxy,未勾選的直接連線;\n繞行模式:勾選的 App 直接連線,未勾選的啟用 Proxy。\n可在選單中選擇自動選中需 Proxy 應用 @@ -184,6 +181,8 @@ VPN DNS (僅支援 IPv4/v6) VPN 是否繞過區域網 + VPN 介面位址 + DNS 境内 DNS (可选) DNS hosts (格式: 網域:位址,…) @@ -240,6 +239,7 @@ 自動更新間隔(分鐘,最小值 15) 記錄層級 + Outbound 網域預解析方式 模式 輕觸以檢視說明 語言 @@ -260,9 +260,10 @@ 別名正規過濾 啟用更新 啟用自動更新 - 前置代理别名 - 落地代理別名 - 请确保别名存在并唯一 + 允許不安全的 HTTP 位址 + 前置代理設定檔别名 + 落地代理設定檔別名 + 请确保設定檔别名存在并唯一 更新目前群組訂閱 偵測目前群組設定 Tcping 偵測目前群組設定真延遲 @@ -313,6 +314,13 @@ 分片間隔(最小-最大) 啟用分片(Fragment) + 檢查更新 + 当前已是最新版本 + 發現新版本: %s + 立即更新 + 檢查 Pre-release + 正在檢查更新中… + QR Code 匯出至剪貼簿 @@ -356,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 f758f2b4..27f0846e 100644 --- a/V2rayNG/app/src/main/res/values/arrays.xml +++ b/V2rayNG/app/src/main/res/values/arrays.xml @@ -124,43 +124,6 @@ xtls-rprx-vision-udp443 - - - - 0.0.0.0/5 - 8.0.0.0/7 - 11.0.0.0/8 - 12.0.0.0/6 - 16.0.0.0/4 - 32.0.0.0/3 - 64.0.0.0/2 - 128.0.0.0/3 - 160.0.0.0/5 - 168.0.0.0/6 - 172.0.0.0/12 - 172.32.0.0/11 - 172.64.0.0/10 - 172.128.0.0/9 - 173.0.0.0/8 - 174.0.0.0/7 - 176.0.0.0/4 - 192.0.0.0/9 - 192.128.0.0/11 - 192.160.0.0/13 - 192.169.0.0/16 - 192.170.0.0/15 - 192.172.0.0/14 - 192.176.0.0/12 - 192.192.0.0/10 - 193.0.0.0/8 - 194.0.0.0/7 - 196.0.0.0/6 - 200.0.0.0/5 - 208.0.0.0/4 - 240.0.0.0/4 - - - auto English @@ -219,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 b085a3df..57106306 100644 --- a/V2rayNG/app/src/main/res/values/strings.xml +++ b/V2rayNG/app/src/main/res/values/strings.xml @@ -36,11 +36,6 @@ Type manually[Trojan] Type manually[Wireguard] Type manually[Hysteria2] - Custom config - Import custom config from Clipboard - Import custom config from locally - Import custom config from URL - Import custom config scan URL Confirm delete ? Please test before deleting! Confirm delete ? remarks @@ -127,6 +122,7 @@ Add asset URL File not found The remarks already exists + Geo files source (optional) Loading @@ -144,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 @@ -178,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 @@ -186,6 +183,8 @@ VPN DNS (only IPv4/v6) Does VPN bypass LAN + VPN Interface Address + Domestic DNS (Optional) DNS @@ -243,6 +242,7 @@ Auto Update Interval (Minutes, Min value 15) Log Level + Outbound domain pre-resolve method Mode Click me for more help Language @@ -263,9 +263,10 @@ Remarks regular filter Enable update Enable automatic update - Previous proxy remarks - Next proxy remarks - The remarks exists and is unique + Allow insecure HTTP address + Previous proxy configuration remarks + Next proxy configuration remarks + The configuration remarks exists and is unique Update current group subscription Tcping current group configuration Real delay current group configuration @@ -308,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 @@ -323,6 +324,13 @@ Fragment Interval (min-max) Enable Fragment + Check for update + Already on the latest version + New version found: %s + Update now + Check Pre-release + Checking for update… + QRcode Export to clipboard @@ -366,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/network_security_config.xml b/V2rayNG/app/src/main/res/xml/network_security_config.xml index 134bc5fa..24579b10 100644 --- a/V2rayNG/app/src/main/res/xml/network_security_config.xml +++ b/V2rayNG/app/src/main/res/xml/network_security_config.xml @@ -1,6 +1,6 @@ - + + + + + @@ -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 new file mode 100644 index 00000000..07d87f4d --- /dev/null +++ b/V2rayNG/app/src/test/java/com/v2ray/ang/HttpUtilTest.kt @@ -0,0 +1,41 @@ +package com.v2ray.ang + +import com.v2ray.ang.util.HttpUtil +import org.junit.Assert.assertEquals +import org.junit.Test + +class HttpUtilTest { + + @Test + fun testIdnToASCII() { + // Regular URL remains unchanged + val regularUrl = "https://example.com/path" + 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.toIdnUrl(nonAsciiUrl)) + + // Mixed URL only converts the host part + val mixedUrl = "https://例子.com/测试" + val expectedMixed = "https://xn--fsqu00a.com/测试" + assertEquals(expectedMixed, HttpUtil.toIdnUrl(mixedUrl)) + + // URL with Basic Authentication using regular domain + val basicAuthUrl = "https://user:password@example.com/path" + 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.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.toIdnUrl(nonAsciiAuth)) + } + + +} \ No newline at end of file diff --git a/V2rayNG/app/src/test/java/com/v2ray/ang/AngUnitTest.kt b/V2rayNG/app/src/test/java/com/v2ray/ang/UtilsTest.kt similarity index 73% rename from V2rayNG/app/src/test/java/com/v2ray/ang/AngUnitTest.kt rename to V2rayNG/app/src/test/java/com/v2ray/ang/UtilsTest.kt index 547d15d3..77dd53ab 100644 --- a/V2rayNG/app/src/test/java/com/v2ray/ang/AngUnitTest.kt +++ b/V2rayNG/app/src/test/java/com/v2ray/ang/UtilsTest.kt @@ -11,7 +11,7 @@ import org.junit.Test * * See [testing documentation](http://d.android.com/tools/testing). */ -class AngUnitTest { +class UtilsTest { @Test fun test_parseInt() { @@ -45,4 +45,18 @@ class AngUnitTest { assertTrue(Utils.isIpAddress("240e:1234:abcd:12::/64")) } + @Test + fun test_IsIpInCidr() { + assertTrue(Utils.isIpInCidr("192.168.1.1", "192.168.1.0/24")) + assertTrue(Utils.isIpInCidr("192.168.1.254", "192.168.1.0/24")) + assertFalse(Utils.isIpInCidr("192.168.2.1", "192.168.1.0/24")) + + assertTrue(Utils.isIpInCidr("10.0.0.0", "10.0.0.0/8")) + assertTrue(Utils.isIpInCidr("10.255.255.255", "10.0.0.0/8")) + assertFalse(Utils.isIpInCidr("11.0.0.0", "10.0.0.0/8")) + + assertFalse(Utils.isIpInCidr("invalid-ip", "192.168.1.0/24")) + assertFalse(Utils.isIpInCidr("192.168.1.1", "invalid-cidr")) + } + } \ No newline at end of file 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