Release Android 1.0.2 with unified gateway routing
Android client acceptance / test-and-build (push) Has been cancelled

This commit is contained in:
UCVL Software Automation
2026-09-04 12:47:31 +08:00
parent 0a8bdc9fff
commit 261dde435b
8 changed files with 101 additions and 8 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "cn.toplc.zonghengjia"
minSdk = 24
targetSdk = 37
versionCode = 10001
versionName = "1.0.1"
versionCode = 10002
versionName = "1.0.2"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -8,10 +8,12 @@ import java.net.URLEncoder
object UcvlConfigBridge {
/** Keep the server's complete rules, credentials and node selection. Adapt only local runtime. */
fun buildRuntime(config: JsonObject, remarks: String, socksPort: Int, httpPort: Int,
socksUsername: String? = null, socksPassword: String? = null): JsonObject {
socksUsername: String? = null, socksPassword: String? = null,
gatewayAddress: String? = null): JsonObject {
require(socksPort in 1024..65535 && httpPort in 1024..65535 && socksPort != httpPort)
buildVlessUri(config, remarks) // Validate the supported server contract before saving.
val result = config.deepCopy()
overrideProxyServerAddress(result, gatewayAddress)
buildVlessUri(result, remarks) // Validate the supported server contract before saving.
result.addProperty("remarks", remarks)
result.add("log", JsonParser.parseString("""{"loglevel":"warning"}"""))
val inbounds = JsonArray()
@@ -57,6 +59,35 @@ object UcvlConfigBridge {
return result
}
fun overrideProxyServerAddress(config: JsonObject, gatewayAddress: String?): JsonObject {
val address = gatewayAddress?.trim().orEmpty()
if (address.isEmpty()) return config
val outbounds = config.getAsJsonArray("outbounds") ?: return config
outbounds.map { it.asJsonObject }
.filter { it.get("tag")?.asString == "proxy" }
.forEach { outbound ->
outbound.getAsJsonObject("settings")?.getAsJsonArray("vnext")
?.forEach { it.asJsonObject.addProperty("address", address) }
}
return config
}
fun isPublicIpv4(value: String): Boolean {
val parts = value.split('.').mapNotNull { it.toIntOrNull() }
if (parts.size != 4 || parts.any { it !in 0..255 }) return false
val (a, b, c) = parts
if (a == 0 || a == 10 || a == 127 || a >= 224) return false
if (a == 100 && b in 64..127) return false
if (a == 169 && b == 254) return false
if (a == 172 && b in 16..31) return false
if (a == 192 && b == 168) return false
if (a == 192 && b == 0 && (c == 0 || c == 2)) return false
if (a == 198 && (b == 18 || b == 19)) return false
if (a == 198 && b == 51 && c == 100) return false
if (a == 203 && b == 0 && c == 113) return false
return true
}
fun buildVlessUri(config: JsonObject, remarks: String): String {
val outbounds = config.getAsJsonArray("outbounds")
?: throw IllegalArgumentException("线路配置没有出站信息。")
@@ -313,7 +313,8 @@ class UcvlMainActivity : AppCompatActivity() {
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, false)
MmkvManager.encodeSettings(AppConfig.PREF_APPEND_HTTP_PROXY, false)
val raw = UcvlConfigBridge.buildRuntime(response.config, response.node.name, port, port + 1,
SettingsManager.getSocksUsername(), SettingsManager.getSocksPassword()).toString()
SettingsManager.getSocksUsername(), SettingsManager.getSocksPassword(),
response.gateway?.preferredAddress()).toString()
val ownedGuid = MmkvManager.decodeSettingsString(OWNED_PROFILE_KEY).orEmpty()
val selected = MmkvManager.encodeServerConfig(ownedGuid, CustomFmt.parse(raw))
MmkvManager.encodeServerRaw(selected, raw)
@@ -49,11 +49,21 @@ data class UcvlRouteMode(val id: String, val name: String) {
}
data class UcvlConfigResponse(
@SerializedName("config_contract") val configContract: Int? = null,
val gateway: UcvlGateway? = null,
val node: UcvlNode,
@SerializedName("route_mode") val routeMode: String,
val config: JsonObject,
)
data class UcvlGateway(
val host: String = "",
val ipv4: String? = null,
) {
fun preferredAddress(): String? = ipv4?.takeIf(UcvlConfigBridge::isPublicIpv4)
?: host.trim().takeIf { it.isNotEmpty() }
}
data class UcvlApiError(val error: String? = null)
class UcvlApiException(val statusCode: Int, message: String) : Exception(message)
@@ -41,6 +41,20 @@ class UcvlConfigBridgeTest {
assertFalse(rules.any { it.asJsonObject.get("network")?.asString == "tcp,udp" })
}
@Test fun usesUnifiedNpmGatewayWithoutChangingPortOrRealitySni() {
val runtime = UcvlConfigBridge.buildRuntime(
fixture(), "VPN002", 10808, 10809, gatewayAddress = "59.110.91.17")
val proxy = runtime.getAsJsonArray("outbounds")[0].asJsonObject
val server = proxy.getAsJsonObject("settings").getAsJsonArray("vnext")[0].asJsonObject
assertEquals("59.110.91.17", server.get("address").asString)
assertEquals(11082, server.get("port").asInt)
assertEquals("example.com", proxy.getAsJsonObject("streamSettings")
.getAsJsonObject("realitySettings").get("serverName").asString)
assertTrue(UcvlConfigBridge.isPublicIpv4("59.110.91.17"))
assertFalse(UcvlConfigBridge.isPublicIpv4("100.73.44.33"))
assertFalse(UcvlConfigBridge.isPublicIpv4("198.18.0.1"))
}
@Test(expected = IllegalArgumentException::class)
fun rejectsConflictingLocalPorts() {
UcvlConfigBridge.buildRuntime(fixture(), "VPN002", 10808, 10808)