Compare commits

...

46 Commits

Author SHA1 Message Date
2dust 3773962b64 up 1.10.2 2025-05-07 10:47:50 +08:00
2dust be0a2506ce Update AndroidLibXrayLite 2025-05-07 10:44:19 +08:00
2dust 7f9cb8dfdd Check upgrade function is visible 2025-05-07 10:14:14 +08:00
2dust 71a5b6e480 Update AndroidLibXrayLite 2025-05-04 17:49:02 +08:00
2dust 02e53ced50 Update AndroidLibXrayLite 2025-04-30 14:49:02 +08:00
2dust 42c27a5e7e Update hysteria 2025-04-30 14:48:59 +08:00
2dust af04bbcf87 up 1.10.1 2025-04-30 14:35:48 +08:00
2dust 9bedfe8a7b Bug fix
https://github.com/2dust/v2rayNG/issues/4555
2025-04-30 08:31:51 +08:00
2dust 2fdf684ee7 Fix
https://github.com/2dust/v2rayNG/issues/4548
2025-04-28 15:30:16 +08:00
2dust 5b79951da7 up 1.10.0 2025-04-24 10:33:23 +08:00
2dust 06aa680d45 Update libs.versions.toml 2025-04-24 10:26:03 +08:00
Hossein Abaspanah cdb9b1811c Update Luri Bakhtiari translation (#4535) 2025-04-24 10:11:20 +08:00
solokot 0fc1f2f5d3 Update Russian translation (#4534) 2025-04-24 10:11:10 +08:00
Pk-web6936 ef1bb3dd34 Update Persian translation (#4533) 2025-04-24 10:11:02 +08:00
2dust 1bca321d3f Temporarily add option to allow insecure HTTP subscription address
https://github.com/2dust/v2rayNG/issues/4526
2025-04-23 10:05:49 +08:00
solokot 247e2b3ba3 Update Russian translation (#4532) 2025-04-22 10:36:23 +08:00
2dust 41fd2b0cfb Fix 2025-04-22 10:36:10 +08:00
Hossein Abaspanah 72da42ee40 Update Luri Bakhtiari translation (#4524) 2025-04-20 09:33:01 +08:00
AmirHossein Abdolmotallebi c130d55e8f Update V2rayConfig.kt (#4522) 2025-04-20 09:32:52 +08:00
Pk-web6936 5ae84f7eac Update Persian translate (#4518)
https://github.com/2dust/v2rayNG/pull/4507
2025-04-20 09:31:50 +08:00
patterniha df5ea251e1 move Prefer_IPv6 settings (#4507) 2025-04-19 15:35:20 +08:00
2dust 8890d9f004 Organize and optimize the code of V2rayConfigManager 2025-04-19 11:38:02 +08:00
2dust 4fcb3f9d06 Refactor ConfigResult 2025-04-19 10:24:15 +08:00
2dust 5bf7c98cd3 Refactor outbound related code 2025-04-18 20:02:55 +08:00
2dust 46bc1a49df Refactor reference code with libv2ray remove protect 2025-04-18 17:20:21 +08:00
2dust 21175f41ec Update AndroidLibXrayLite 2025-04-18 17:19:40 +08:00
DHR60 864c63987e Adds domain strategy option to sockopt (#4511)
* Adds domain strategy option to sockopt

* Simplifies sockopt handling in V2Ray config
2025-04-18 16:49:51 +08:00
DHR60 4ac0547e22 Resolves hostnames in config (#4508)
* Removes IP resolution and resolves in config

* Resolves hostnames to multiple IPs for DNS

* Improves custom config handling
2025-04-18 14:12:14 +08:00
2dust 12a9ee262c Revert "Update gradle.properties"
This reverts commit 56e33e6cdd.
2025-04-18 10:24:54 +08:00
2dust cfa9c19c94 Clean code 2025-04-18 10:22:44 +08:00
2dust 56e33e6cdd Update gradle.properties 2025-04-18 10:22:17 +08:00
2dust 02421072c1 Merge branch 'master' of https://github.com/2dust/v2rayNG 2025-04-17 16:41:04 +08:00
2dust b862a0dc65 Update AndroidLibXrayLite 2025-04-17 16:40:53 +08:00
Pk-web6936 1f25d6a000 Update dependencies (#4504)
* Update libs.versions.toml

* Update libs.versions.toml
2025-04-17 16:29:56 +08:00
2dust e1def0616a Refactor the Outbound transport and tls in the configuration file 2025-04-17 15:23:57 +08:00
2dust 83fd6efc17 Resolve remote host names in the configuration file to IP addresses 2025-04-17 14:08:45 +08:00
2dust f0c0e2e83a Refactor reference code with libv2ray 2025-04-17 10:41:30 +08:00
Pk-web6936 6ca3eb769e Update dependencies (#4485)
* Update libs.versions.toml

* Update libs.versions.toml

* Update libs.versions.toml

* Update libs.versions.toml

* Update libs.versions.toml

* Update libs.versions.toml
2025-04-16 20:06:44 +08:00
2dust 963d24ab66 Optimize and improve
https://github.com/2dust/v2rayNG/commit/38193b56216f332eb20b84a8e1395c9b4a1d7f42
2025-04-16 15:09:49 +08:00
2dust cfd81441fa Update libs.versions.toml 2025-04-15 20:44:03 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ 4084ae2938 Updating PRIVATE_IP_LIST (#4498) 2025-04-15 19:21:25 +08:00
kore kas nadar 3f9bc098ec Update Luri Bakhtiari translation (#4497) 2025-04-15 19:21:17 +08:00
Pk-web6936 9cb28ed969 Update Persian translate (#4495)
https://github.com/2dust/v2rayNG/commit/38193b56216f332eb20b84a8e1395c9b4a1d7f42
2025-04-15 14:14:13 +08:00
DHR60 773ddc5373 Fix wg chain proxy (#4496)
* Fix handle null streamSettings in WireGuard chained proxy

* Allows null preshared key for WireGuard

* remove WIREGUARD_LOCAL_ADDRESS_V6

* Considers WireGuard outbound for domain port
2025-04-14 20:42:02 +08:00
2dust 38193b5621 Added IP display in connection test
https://github.com/2dust/v2rayNG/issues/4489
2025-04-14 14:15:25 +08:00
solokot 358713a2a3 Update Russian translation (#4484) 2025-04-11 09:19:02 +08:00
46 changed files with 878 additions and 674 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 21
targetSdk = 35
versionCode = 646
versionName = "1.9.46"
versionCode = 652
versionName = "1.10.2"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -97,7 +97,7 @@
}
],
"routing": {
"domainStrategy": "IPIfNonMatch",
"domainStrategy": "AsIs",
"rules": []
},
"dns": {
@@ -103,6 +103,7 @@ object AppConfig {
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
const val IP_API_Url = "https://api.ip.sb/geoip"
/** DNS server addresses. */
const val DNS_PROXY = "1.1.1.1"
@@ -223,7 +224,9 @@ object AppConfig {
)
val PRIVATE_IP_LIST = arrayListOf(
"0.0.0.0/8",
"10.0.0.0/8",
"127.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16",
@@ -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,
)
@@ -0,0 +1,11 @@
package com.v2ray.ang.dto
data class IPAPIInfo(
var ip: String? = null,
var city: String? = null,
var region: String? = null,
var region_code: String? = null,
var country: String? = null,
var country_name: String? = null,
var country_code: String? = 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?,
)
@@ -11,5 +11,6 @@ data class SubscriptionItem(
var prevProfile: String? = null,
var nextProfile: String? = null,
var filter: String? = null,
var allowInsecureUrl: Boolean = false,
)
@@ -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<InboundBean>,
var outbounds: ArrayList<OutboundBean>,
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<VnextBean>? = 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<Double>() {}.type,
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
)
.create()
.toJson(this)
fun getAllProxyOutbound(): List<OutboundBean> {
return outbounds.filter { outbound ->
EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) }
}
}
}
}
@@ -148,4 +148,7 @@ open class FmtBase {
return dicQuery
}
}
@@ -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,7 +14,7 @@ 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()
@@ -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
}
}
@@ -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,7 +132,7 @@ 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()
@@ -140,29 +141,13 @@ object ShadowsocksFmt : FmtBase() {
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
}
@@ -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,7 +61,7 @@ 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()
@@ -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,7 +61,7 @@ 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()
@@ -69,29 +70,13 @@ object TrojanFmt : FmtBase() {
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
}
@@ -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,7 +57,7 @@ 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()
@@ -67,31 +67,13 @@ object VlessFmt : FmtBase() {
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
}
@@ -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,7 +169,7 @@ 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()
@@ -177,29 +178,13 @@ object VmessFmt : FmtBase() {
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
}
@@ -7,6 +7,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -24,14 +25,14 @@ object WireguardFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.secretKey = uri.userInfo.orEmpty()
config.localAddress = queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
config.publicKey = queryParam["publickey"].orEmpty()
config.preSharedKey = queryParam["presharedkey"].orEmpty()
config.preSharedKey = queryParam["presharedkey"]?.takeIf { it.isNotEmpty() }
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.reserved = queryParam["reserved"] ?: "0,0,0"
@@ -83,7 +84,7 @@ object WireguardFmt : FmtBase() {
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.publicKey = peerParams["publickey"].orEmpty()
config.preSharedKey = peerParams["presharedkey"].orEmpty()
config.preSharedKey = peerParams["presharedkey"]?.takeIf { it.isNotEmpty() }
val endpoint = peerParams["endpoint"].orEmpty()
val endpointParts = endpoint.split(":", limit = 2)
if (endpointParts.size == 2) {
@@ -105,18 +106,18 @@ object WireguardFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.WIREGUARD)
outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = profileItem.secretKey
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
wireguard.peers?.firstOrNull()?.let { peer ->
peer.publicKey = profileItem.publicKey.orEmpty()
peer.preSharedKey = profileItem.preSharedKey.orEmpty()
peer.preSharedKey = profileItem.preSharedKey?.takeIf { it.isNotEmpty() }
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
}
wireguard.mtu = profileItem.mtu
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() }
}
return outboundBean
@@ -419,8 +419,10 @@ object AngConfigManager {
if (!Utils.isValidUrl(url)) {
return 0
}
if (!Utils.isValidSubUrl(url)) {
return 0
if (!it.second.allowInsecureUrl) {
if (!Utils.isValidSubUrl(url)) {
return 0
}
}
Log.i(AppConfig.TAG, url)
@@ -216,7 +216,7 @@ object SettingsManager {
* @return The ProfileItem.
*/
fun getServerViaRemarks(remarks: String?): ProfileItem? {
if (remarks == null) {
if (remarks.isNullOrEmpty()) {
return null
}
val serverList = decodeServerList()
@@ -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,14 @@ 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
return "(${ipInfo.country_code}) ${ipInfo.ip}"
}
/**
* Gets the version of the V2Ray library.
*
@@ -4,39 +4,15 @@ 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
@@ -47,12 +23,15 @@ 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 +83,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)
}
/**
@@ -131,29 +109,33 @@ 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
}
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
}
@@ -179,9 +161,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 +181,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 +196,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 +206,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 +258,14 @@ object V2rayConfigManager {
return true
}
private fun outbounds(v2rayConfig: V2rayConfig, config: ProfileItem, isPlugin: Boolean): Pair<Boolean, String>? {
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 +273,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 +299,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 +320,22 @@ object V2rayConfigManager {
}
}
private fun userRule2Domain(tag: String): ArrayList<String> {
/**
* 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<String> {
val domain = ArrayList<String>()
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 +347,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 +388,7 @@ object V2rayConfigManager {
V2rayConfig.InboundBean(
tag = "dns-in",
port = localDnsPort,
listen = LOOPBACK,
listen = AppConfig.LOOPBACK,
protocol = "dokodemo-door",
settings = dnsInboundSettings,
sniffing = null
@@ -427,14 +424,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<String, Any>()
val servers = ArrayList<Any>()
//remote Dns
val remoteDns = SettingsManager.getRemoteDnsServers()
val proxyDomain = userRule2Domain(TAG_PROXY)
val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY)
remoteDns.forEach {
servers.add(it)
}
@@ -449,9 +454,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 +471,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
@@ -489,21 +494,21 @@ object V2rayConfigManager {
}
//block dns
val blkDomain = userRule2Domain(TAG_BLOCKED)
val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
if (blkDomain.isNotEmpty()) {
hosts.putAll(blkDomain.map { it to LOOPBACK })
hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK })
}
// hardcode googleapi rule to fix play store problems
hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN
hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.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
hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES
hosts[AppConfig.DNS_CLOUDFLARE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ADDRESSES
hosts[AppConfig.DNS_DNSPOD_DOMAIN] = AppConfig.DNS_DNSPOD_ADDRESSES
hosts[AppConfig.DNS_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES
hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES
hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES
// DNS dns
@@ -516,7 +521,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 +535,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 +698,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 +708,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 +719,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 +738,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 +759,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 +776,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 +785,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 +802,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 +812,47 @@ object V2rayConfigManager {
return true
}
private fun moreOutbounds(
v2rayConfig: V2rayConfig,
subscriptionId: String,
isPlugin: Boolean
): Pair<Boolean, String> {
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 (newHosts.containsKey(domain)) continue
if (subscriptionId.isEmpty()) {
return returnPair
}
try {
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return returnPair
val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6)
if (resolvedIps.isNullOrEmpty()) continue
//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()
}
item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6"
newHosts[domain] = if (resolvedIps.size == 1) {
resolvedIps[0]
} else {
resolvedIps
}
//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
)
}
}
} 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 +861,216 @@ 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()) 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
}
@@ -158,6 +158,7 @@ object NotificationService {
mBuilder = null
speedNotificationJob?.cancel()
speedNotificationJob = null
mNotificationManager = null
}
/**
@@ -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()
}
/**
@@ -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,10 +91,13 @@ 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
@@ -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, "")
@@ -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())
}
/**
@@ -104,7 +104,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)
}
@@ -255,6 +257,7 @@ 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 cmd = arrayListOf(
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
@@ -293,11 +296,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
runTun2socks()
}
}.start()
Log.i(AppConfig.TAG, process.toString())
Log.i(AppConfig.TAG, "$TUN2SOCKS process info : ${process.toString()}")
sendFd()
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to start tun2socks process", e)
Log.e(AppConfig.TAG, "Failed to start $TUN2SOCKS process", e)
}
}
@@ -308,13 +311,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
private fun sendFd() {
val fd = mInterface.fileDescriptor
val path = File(applicationContext.filesDir, "sock_path").absolutePath
Log.i(AppConfig.TAG, path)
Log.i(AppConfig.TAG, "LocalSocket path : $path")
CoroutineScope(Dispatchers.IO).launch {
var tries = 0
while (true) try {
Thread.sleep(50L shl tries)
Log.i(AppConfig.TAG, "sendFd tries: $tries")
Log.i(AppConfig.TAG, "LocalSocket sendFd tries: $tries")
LocalSocket().use { localSocket ->
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
localSocket.setFileDescriptorsForSend(arrayOf(fd))
@@ -348,13 +351,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
@@ -106,13 +106,13 @@ class AboutActivity : BaseActivity() {
}
//If it is the Google Play version, not be displayed within 1 days after update
if (Utils.isGoogleFlavor()) {
val lastUpdateTime = AppManagerUtil.getLastUpdateTime(this)
val currentTime = System.currentTimeMillis()
if ((currentTime - lastUpdateTime) < 1 * 24 * 60 * 60 * 1000L) {
binding.layoutCheckUpdate.visibility = View.GONE
}
}
// if (Utils.isGoogleFlavor()) {
// val lastUpdateTime = AppManagerUtil.getLastUpdateTime(this)
// val currentTime = System.currentTimeMillis()
// if ((currentTime - lastUpdateTime) < 1 * 24 * 60 * 60 * 1000L) {
// binding.layoutCheckUpdate.visibility = View.GONE
// }
// }
binding.layoutCheckUpdate.setOnClickListener {
checkForUpdates(binding.checkPreRelease.isChecked)
}
@@ -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<Mai
* @param guid The server unique identifier
*/
private fun shareFullContent(guid: String) {
if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
mActivity.toastSuccess(R.string.toast_success)
} else {
mActivity.toastError(R.string.toast_failure)
mActivity.lifecycleScope.launch(Dispatchers.IO) {
val result = AngConfigManager.shareFullContent2Clipboard(mActivity, guid)
launch(Dispatchers.Main) {
if (result == 0) {
mActivity.toastSuccess(R.string.toast_success)
} else {
mActivity.toastError(R.string.toast_failure)
}
}
}
}
@@ -18,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
@@ -327,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) {
@@ -420,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
}
@@ -46,6 +46,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 +78,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 +92,9 @@ class SubEditActivity : BaseActivity() {
if (!Utils.isValidSubUrl(subItem.url)) {
toast(R.string.toast_insecure_url_protocol)
return false
if (!subItem.allowInsecureUrl) {
return false
}
}
}
@@ -9,13 +9,14 @@ import com.v2ray.ang.util.Utils.urlDecode
import java.io.IOException
import java.net.HttpURLConnection
import java.net.IDN
import java.net.Inet6Address
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
object HttpUtil {
/**
* Converts a URL string to its ASCII representation.
*
@@ -33,6 +34,45 @@ object HttpUtil {
}
}
/**
* 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<String>? {
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.
*
@@ -23,20 +23,20 @@ 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) {
if (config == null || socksPort == null) {
Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
return
}
try {
if (config.configType == EConfigType.HYSTERIA2) {
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 +66,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 +85,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")
@@ -138,6 +138,28 @@
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp16"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:text="@string/sub_allow_insecure_url" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/allow_insecure_url"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_spacing_dp16"
android:paddingEnd="@dimen/padding_spacing_dp16"
app:theme="@style/BrandedSwitch" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -258,6 +258,7 @@
<string name="sub_setting_filter">Remarks regular filter</string>
<string name="sub_setting_enable">تفعيل التحديث</string>
<string name="sub_auto_update">تفعيل التحديث التلقائي</string>
<string name="sub_allow_insecure_url">Allow insecure HTTP address</string>
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
@@ -258,6 +258,7 @@
<string name="sub_setting_filter">Remarks regular filter</string>
<string name="sub_setting_enable">আপডেট সক্রিয় করুন</string>
<string name="sub_auto_update">স্বয়ংক্রিয় আপডেট সক্রিয় করুন</string>
<string name="sub_allow_insecure_url">Allow insecure HTTP address</string>
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
@@ -40,7 +40,7 @@
<string name="server_lab_remarks">نیشتنا</string>
<string name="server_lab_address">نشۊوی</string>
<string name="server_lab_port">پورت</string>
<string name="server_lab_id">نوم من توری</string>
<string name="server_lab_id">نوم منتوری</string>
<string name="server_lab_alterid">شناسه جایگۊزین</string>
<string name="server_lab_security">ٱمنیت</string>
<string name="server_lab_network">شبکه</string>
@@ -73,7 +73,7 @@
<string name="server_lab_id3">رزم</string>
<string name="server_lab_security3">ٱمنیت</string>
<string name="server_lab_id4">رزم (اختیاری)</string>
<string name="server_lab_security4">نوم من توری (اختیاری)</string>
<string name="server_lab_security4">نوم منتوری (اختیاری)</string>
<string name="server_lab_encryption">رزم نگاری</string>
<string name="server_lab_flow">جریان</string>
<string name="server_lab_public_key">کیلیت پوی وولاتی</string>
@@ -173,7 +173,7 @@
<string name="summary_pref_fake_dns_enabled">DNS مهلی نشۊویا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من یقرد ز برنومه یل کار نکونه)</string>
<string name="title_pref_prefer_ipv6">ترجی IPv6</string>
<string name="summary_pref_prefer_ipv6">ترجی داڌن نشۊوی وو تورا IPv6</string>
<string name="summary_pref_prefer_ipv6">تورا IPv6 ن فعال کۊنین وو نشۊویا IPv6 ن ترجی بڌین</string>
<string name="title_pref_remote_dns">DNS ز ر دیر (اختیاری) (udp/tcp/https/quic) (اختیاری)</string>
<string name="summary_pref_remote_dns">DNS</string>
@@ -258,6 +258,7 @@
<string name="sub_setting_filter">نوم موستعار فیلتر</string>
<string name="sub_setting_enable">فعال بیڌن ورۊ کردن</string>
<string name="sub_auto_update">فعال بیڌن ورۊ کردن خوتکار</string>
<string name="sub_allow_insecure_url">موجاز کردن نشۊوی HTTP نا ٱمن</string>
<string name="sub_setting_pre_profile">نوم موستعار پروکسی دیندایی</string>
<string name="sub_setting_next_profile">نوم موستعار پروکسی نیایی</string>
<string name="sub_setting_pre_profile_tip">موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی</string>
@@ -303,7 +304,7 @@
<string name="connection_test_pending">منپیزن واجۊری کوݩ</string>
<string name="connection_test_testing">هونی آزمایش ابۊ…</string>
<string name="connection_test_testing_count">%d کانفیگ هونی آزمایش ابۊ...</string>
<string name="connection_test_available">مووفق بی: منپیز HTTP %dms تۊل کشی</string>
<string name="connection_test_available">مووفق بی: منپیز %dms تۊل کشی</string>
<string name="connection_test_error">منپیز و اینترنتن نجوست: %s</string>
<string name="connection_test_fail">اینترنت من دسرس نؽ</string>
<string name="connection_test_error_status_code">کود ختا: #%d</string>
@@ -322,7 +323,7 @@
<string name="update_already_latest_version">سکو نوسخه دیندایی پۊرنیڌه هڌ</string>
<string name="update_new_version_found">نوسخه نۊ ن جوست: %s</string>
<string name="update_now">سکو ورۊ رسۊوی کۊنین</string>
<string name="update_check_pre_release">نوسخیل پؽش ز تیجنیڌنن واجۊری کۊنین</string>
<string name="update_check_pre_release">واجۊری نوسخه یل پؽش ز تیجنیڌن</string>
<string-array name="share_method">
<item>QRcode</item>
@@ -171,7 +171,7 @@
<string name="summary_pref_fake_dns_enabled">دی ان اس محلی آدرس های آیپی جعلی را بر می گرداند (سریع تر می باشد و تاخیر را کاهش می دهد اما ممکن است برای برخی از برنامه ها کار نکند)</string>
<string name="title_pref_prefer_ipv6">ترجیح دادن IPV6</string>
<string name="summary_pref_prefer_ipv6">ترجیح دادن نشانی و مسیر های IPv6</string>
<string name="summary_pref_prefer_ipv6">مسیرهای IPv6 را فعال کنید و آدرس‌های IPv6 را ترجیح دهید</string>
<string name="title_pref_remote_dns">DNS از راه دور (اختیاری) (udp/tcp/https/quic)</string>
<string name="summary_pref_remote_dns">DNS</string>
@@ -255,6 +255,7 @@
<string name="sub_setting_filter">نام مستعار فیلتر</string>
<string name="sub_setting_enable">فعال کردن به‌روزرسانی</string>
<string name="sub_auto_update">فعال سازی به‌روزرسانی خودکار</string>
<string name="sub_allow_insecure_url">مجاز کردن آدرس HTTP ناامن</string>
<string name="sub_setting_pre_profile">نام مستعار پروکسی قبلی</string>
<string name="sub_setting_next_profile">نام مستعار پروکسی بعدی</string>
<string name="sub_setting_pre_profile_tip">لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است</string>
@@ -300,7 +301,7 @@
<string name="connection_test_pending">اتصال را بررسی کنید</string>
<string name="connection_test_testing">در حال آزمایش...</string>
<string name="connection_test_testing_count">تست کردن %d کانفیگ…</string>
<string name="connection_test_available">موفقیت: اتصال HTTP %dms طول کشید</string>
<string name="connection_test_available">موفقیت: اتصال %dms طول کشید</string>
<string name="connection_test_error">اتصال به اینترنت شناسایی نشد: %s</string>
<string name="connection_test_fail">اینترنت در دسترس نیست</string>
<string name="connection_test_error_status_code">کد خطا: #%d</string>
@@ -172,7 +172,7 @@
<string name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string>
<string name="title_pref_prefer_ipv6">Предпочитать IPv6</string>
<string name="summary_pref_prefer_ipv6">Предпочитать IPv6-адреса и маршрутизацию</string>
<string name="summary_pref_prefer_ipv6">Использовать маршрутизацию IPv6 предпочитать IPv6-адреса</string>
<string name="title_pref_remote_dns">Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно)</string>
<string name="summary_pref_remote_dns">DNS</string>
@@ -257,9 +257,10 @@
<string name="sub_setting_filter">Название фильтра</string>
<string name="sub_setting_enable">Использовать обновление</string>
<string name="sub_auto_update">Использовать автообновление</string>
<string name="sub_setting_pre_profile">Название предыдущего прокси</string>
<string name="sub_setting_next_profile">Название следующего прокси</string>
<string name="sub_setting_pre_profile_tip">Название должно существовать и быть уникальным</string>
<string name="sub_allow_insecure_url">Разрешать незащищённые HTTP-адреса</string>
<string name="sub_setting_pre_profile">Предыдущая конфигурация прокси</string>
<string name="sub_setting_next_profile">Следующая конфигурация прокси</string>
<string name="sub_setting_pre_profile_tip">Конфигурация должна быть уникальной</string>
<string name="title_sub_update">Обновить подписку группы</string>
<string name="title_ping_all_server">Проверка профилей группы</string>
<string name="title_real_ping_all_server">Время отклика профилей группы</string>
@@ -302,7 +303,7 @@
<string name="connection_test_pending">Проверить подключение</string>
<string name="connection_test_testing">Проверка…</string>
<string name="connection_test_testing_count">Проверка профилей (%d)</string>
<string name="connection_test_available">Успешно: HTTP-соединение заняло %d мс</string>
<string name="connection_test_available">Успешно: соединение заняло %d мс</string>
<string name="connection_test_error">Сбой проверки интернет-соединения: %s</string>
<string name="connection_test_fail">Интернет недоступен</string>
<string name="connection_test_error_status_code">Код ошибки: #%d</string>
@@ -321,7 +322,7 @@
<string name="update_already_latest_version">Установлена последняя версия</string>
<string name="update_new_version_found">Найдена новая версия: %s</string>
<string name="update_now">Обновить</string>
<string name="update_check_pre_release">Проверить предварительный выпуск</string>
<string name="update_check_pre_release">Искать предварительный выпуск</string>
<string-array name="share_method">
<item>QR-код</item>
@@ -258,6 +258,7 @@
<string name="sub_setting_filter">Remarks regular filter</string>
<string name="sub_setting_enable">Sử dụng gói đăng ký này</string>
<string name="sub_auto_update">Bật tự động cập nhật</string>
<string name="sub_allow_insecure_url">Allow insecure HTTP address</string>
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
@@ -255,6 +255,7 @@
<string name="sub_setting_filter">别名正则过滤</string>
<string name="sub_setting_enable">启用更新</string>
<string name="sub_auto_update">启用自动更新</string>
<string name="sub_allow_insecure_url">允许不安全的 HTTP 地址</string>
<string name="sub_setting_pre_profile">前置代理配置文件别名</string>
<string name="sub_setting_next_profile">落地代理配置文件別名</string>
<string name="sub_setting_pre_profile_tip">请确保配置文件别名存在并唯一</string>
@@ -256,6 +256,7 @@
<string name="sub_setting_filter">別名正規過濾</string>
<string name="sub_setting_enable">啟用更新</string>
<string name="sub_auto_update">啟用自動更新</string>
<string name="sub_allow_insecure_url">允許不安全的 HTTP 位址</string>
<string name="sub_setting_pre_profile">前置代理設定檔别名</string>
<string name="sub_setting_next_profile">落地代理設定檔別名</string>
<string name="sub_setting_pre_profile_tip">请确保設定檔别名存在并唯一</string>
+3 -2
View File
@@ -174,7 +174,7 @@
<string name="summary_pref_fake_dns_enabled">Local DNS returns fake IP addresses (faster, but it may not work for some apps)</string>
<string name="title_pref_prefer_ipv6">Prefer IPv6</string>
<string name="summary_pref_prefer_ipv6">Prefer IPv6 addresses and routes</string>
<string name="summary_pref_prefer_ipv6">Enable IPv6 routes and Prefer IPv6 addresses</string>
<string name="title_pref_remote_dns">Remote DNS (udp/tcp/https/quic)(Optional)</string>
<string name="summary_pref_remote_dns">DNS</string>
@@ -259,6 +259,7 @@
<string name="sub_setting_filter">Remarks regular filter</string>
<string name="sub_setting_enable">Enable update</string>
<string name="sub_auto_update">Enable automatic update</string>
<string name="sub_allow_insecure_url">Allow insecure HTTP address</string>
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
@@ -304,7 +305,7 @@
<string name="connection_test_pending">Check Connectivity</string>
<string name="connection_test_testing">Testing…</string>
<string name="connection_test_testing_count">Testing %d configurations…</string>
<string name="connection_test_available">Success: HTTP connection took %dms</string>
<string name="connection_test_available">Success: Connection took %dms</string>
<string name="connection_test_error">Fail to detect internet connection: %s</string>
<string name="connection_test_fail">Internet Unavailable</string>
<string name="connection_test_error_status_code">Error code: #%d</string>
@@ -19,6 +19,11 @@
android:title="@string/title_pref_is_booted" />
<PreferenceCategory android:title="@string/title_vpn_settings">
<CheckBoxPreference
android:key="pref_prefer_ipv6"
android:summary="@string/summary_pref_prefer_ipv6"
android:title="@string/title_pref_prefer_ipv6" />
<CheckBoxPreference
android:key="pref_per_app_proxy"
android:summary="@string/summary_pref_per_app_proxy"
@@ -170,11 +175,6 @@
android:title="@string/title_advanced"
app:initialExpandedChildrenCount="0">
<CheckBoxPreference
android:key="pref_prefer_ipv6"
android:summary="@string/summary_pref_prefer_ipv6"
android:title="@string/title_pref_prefer_ipv6" />
<CheckBoxPreference
android:defaultValue="false"
android:key="pref_proxy_sharing_enabled"
+9 -9
View File
@@ -1,9 +1,9 @@
[versions]
agp = "8.9.1"
desugar_jdk_libs = "2.1.5"
agp = "8.9.2"
desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8"
kotlin = "2.1.20"
coreKtx = "1.15.0"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
@@ -14,13 +14,13 @@ 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"
workRuntimeKtx = "2.10.1"
lifecycleViewmodelKtx = "2.8.7"
multidex = "2.0.1"
mockitoMockitoInline = "5.2.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" }