Compare commits

..
6 Commits
Author SHA1 Message Date
2dust aa4b05503a up 2.3.14 2026-10-28 10:40:04 +08:00
Yuriy Tumanov 7179f0838f Use the default port for Hysteria2 links without a port (#6358) 2026-10-28 10:12:18 +08:00
Miheichev Aleksandr Sergeevich 0d1c57000f build(deps): bump OkHttp from 5.5.0 to 5.5.1 (#6351) 2026-10-28 10:11:05 +08:00
Figim 40b8706ccd Update subscription page string in Azerbaijani (#6349)
Edit the translation added yesterday.
2026-10-28 10:10:50 +08:00
Miheichev Aleksandr Sergeevich 0f961c54c8 fix(deps): update hev-socks5-tunnel to 2.19.3 (#6357)
* chore(deps): update hev-socks5-tunnel to 2.19.2

* fix(root): disable jumbo for LAN forwarding

* fix(root): guard IPv6 LAN TSO fallback

* chore(deps): update hev-socks5-tunnel to 2.19.3
2026-10-25 18:49:47 +08:00
2dust 1b78a510f2 Revert "chore(deps): update hev-socks5-tunnel to 2.19.1 (#6328)"
This reverts commit 552cc2425d.
2026-10-24 18:45:21 +08:00
8 changed files with 146 additions and 9 deletions

No files matched your search

+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 24
targetSdk = 37
versionCode = 753
versionName = "2.3.13"
versionCode = 754
versionName = "2.3.14"
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
splits {
@@ -23,7 +23,7 @@ object Hysteria2Fmt : FmtBase() {
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.serverPort = (if (uri.port == -1) AppConfig.DEFAULT_PORT else uri.port).toString()
config.password = uri.userInfo
config.security = AppConfig.TLS
config.network = NetworkType.HYSTERIA.type
@@ -126,7 +126,7 @@ object RootProxyManager {
val lanShare = forceLanShare || MmkvManager.decodeSettingsBool(AppConfig.PREF_ROOT_LAN_SHARING)
val corePid = Process.myPid()
val config = buildHevConfig(socksUsername, socksPassword, port, ipv6)
val config = buildHevConfig(socksUsername, socksPassword, port, ipv6, SettingsManager.getVpnMtu(), lanShare)
if (!writeHevConfig(cfgFile, config)) {
return null
}
@@ -209,14 +209,28 @@ object RootProxyManager {
* tun address when IPv6 is enabled; whether v6 actually flows in is decided separately by
* the v6 route into [TABLE].
*/
private fun buildHevConfig(socksUsername: String?, socksPassword: String?, socksPort: Int, ipv6: Boolean): String {
internal fun buildHevConfig(
socksUsername: String?,
socksPassword: String?,
socksPort: Int,
ipv6: Boolean,
mtu: Int,
lanSharing: Boolean,
): String {
val v4 = AppConfig.ROOT_TUN_ADDR_V4.substringBefore("/")
val v6 = AppConfig.ROOT_TUN_ADDR_V6.substringBefore("/")
return buildString {
appendLine("tunnel:")
appendLine(" name: '$TUN'")
appendLine(" mtu: ${SettingsManager.getVpnMtu()}")
appendLine(" mtu: $mtu")
appendLine(" multi-queue: true")
if (lanSharing) {
// VNET setup or TCP-GSO EINVAL on Android can leave IPv6 packets above the LAN MTU.
// Remove when both fallback forwarding tests pass with jumbo and TSO enabled.
appendLine(" offload:")
appendLine(" jumbo: false")
if (ipv6) appendLine(" tso: false")
}
appendLine(" ipv4: '$v4'")
if (ipv6) appendLine(" ipv6: '$v6'")
appendLine("socks5:")
@@ -561,7 +561,8 @@
<string name="connection_test_empty_message">ətraflı məlumat yoxdur</string>
<string name="value_unknown">naməlum</string>
<string name="sub_info_open_support">Dəstək linkini aç</string>
<string name="sub_info_open_page">Abunəlik məlumat səhifəsini aç</string>
<string name="sub_info_open_page">Abunəlik səhifəsini aç</string>
<string name="sub_info_traffic" translatable="false">%1$s / %2$s</string>
<string name="sub_info_unlimited">Limitsiz</string>
</resources>
@@ -0,0 +1,89 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.net.URI
class Hysteria2FmtTest {
@Test
fun hysteria2WithoutPortUses443() {
val profile = Hysteria2Fmt.parse("hysteria2://test@example.com/")
assertEquals("example.com", profile.server)
assertEquals("443", profile.serverPort)
}
@Test
fun hy2WithoutPortUses443() {
val profile = Hysteria2Fmt.parse("hy2://test@example.com/")
assertEquals("example.com", profile.server)
assertEquals("443", profile.serverPort)
}
@Test
fun uriWithoutCredentialsUses443() {
val profile = Hysteria2Fmt.parse("hysteria2://example.com/")
assertEquals("example.com", profile.server)
assertEquals("443", profile.serverPort)
}
@Test
fun omittedPortPreservesTlsParameters() {
val profile = Hysteria2Fmt.parse(
"hy2://test@example.com/?sni=example.org&insecure=0&pinSHA256=test-pin#example"
)
assertEquals("test", profile.password)
assertEquals("example", profile.remarks)
assertEquals(AppConfig.TLS, profile.security)
assertEquals("example.org", profile.sni)
assertEquals(false, profile.insecure)
assertEquals("test-pin", profile.pinnedCA256)
assertEquals("443", profile.serverPort)
}
@Test
fun ipv6WithoutPortUses443() {
val profile = Hysteria2Fmt.parse("hysteria2://test@[2001:db8::1]/")
assertEquals("2001:db8::1", profile.server)
assertEquals("443", profile.serverPort)
}
@Test
fun explicitPortsArePreserved() {
for (scheme in listOf("hysteria2", "hy2")) {
// Port range validation is separate from choosing a missing-port default.
for (port in listOf(0, 443, 8443, 65535, 65536)) {
val profile = Hysteria2Fmt.parse("$scheme://test@example.com:$port/")
assertEquals("example.com", profile.server)
assertEquals(port.toString(), profile.serverPort)
}
}
}
@Test
fun nonnumericPortDoesNotAcquireAHost() {
val profile = Hysteria2Fmt.parse("hysteria2://test@example.com:invalid/")
assertEquals("", profile.server)
}
@Test
fun defaultPortSurvivesUriRoundTrip() {
val profile = Hysteria2Fmt.parse("hy2://test@example.com/?sni=example.org")
val exported = "hysteria2://${Hysteria2Fmt.toUri(profile)}"
val reparsed = Hysteria2Fmt.parse(exported)
assertEquals(443, URI(exported).port)
assertEquals("443", reparsed.serverPort)
assertEquals(profile.server, reparsed.server)
assertEquals(profile.password, reparsed.password)
assertEquals(profile.sni, reparsed.sni)
}
}
@@ -1,10 +1,43 @@
package com.v2ray.ang.root
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class RootProxyManagerTest {
@Test
fun `IPv6 LAN forwarding disables jumbo and TSO`() {
val config = RootProxyManager.buildHevConfig(null, null, 10808, true, 1500, true)
assertTrue(config.contains(" offload:\n jumbo: false\n"))
assertTrue(config.contains(" multi-queue: true\n"))
assertTrue(config.contains(" tso: false\n"))
assertFalse(config.contains("uso:"))
}
@Test
fun `local root keeps default offloads and configured MTU`() {
val config = RootProxyManager.buildHevConfig(null, null, 10808, true, 8500, false)
assertFalse(config.contains("offload:"))
assertTrue(config.contains(" mtu: 8500\n"))
assertTrue(config.contains(" ipv6:"))
}
@Test
fun `LAN offload guard preserves IPv4 only and quoted authentication`() {
val config = RootProxyManager.buildHevConfig("user'name", "pa'ss", 10809, false, 1500, true)
assertTrue(config.contains(" offload:\n jumbo: false\n"))
assertFalse(config.contains("tso:"))
assertFalse(config.contains(" ipv6:"))
assertTrue(config.contains(" port: 10809\n"))
assertTrue(config.contains(" username: 'user''name'\n"))
assertTrue(config.contains(" password: 'pa''ss'\n"))
}
@Test
fun `single quotes are escaped in YAML scalars`() {
assertEquals("'user''name'", "user'name".toSingleQuotedYamlScalar())
+1 -1
View File
@@ -11,7 +11,7 @@ activity = "1.13.0"
appcompat = "1.8.0"
mmkvStatic = "1.3.17"
gson = "2.14.0"
okhttp = "5.5.0"
okhttp = "5.5.1"
kotlinxCoroutines = "1.11.0"
core = "3.5.4"
workRuntimeKtx = "2.12.0"