From 5de1471438ac032bb0fdb7f652550c8dc00bb6b1 Mon Sep 17 00:00:00 2001
From: qiin2333 <414382190@qq.com>
Date: Sun, 30 Aug 2026 21:41:10 +0800
Subject: [PATCH 1/4] feat(easytier): add one-tap VPlus host pairing
---
.gitignore | 6 +-
app/build.gradle | 4 +
app/src/main/AndroidManifest.xml | 11 ++
.../main/java/com/easytier/jni/EasyTierJNI.kt | 6 +-
.../java/com/easytier/jni/EasyTierManager.kt | 28 ++++-
.../com/easytier/jni/EasyTierVpnService.kt | 14 ++-
app/src/main/java/com/limelight/PcView.kt | 92 +++++++++++---
.../utils/easytier/EasyTierController.kt | 32 ++++-
.../utils/easytier/EasyTierTomlCodec.kt | 19 ++-
.../utils/easytier/VPlusConnectionCode.kt | 117 ++++++++++++++++++
app/src/main/jniLibs/x86_64/README.md | 17 +++
.../jniLibs/x86_64/libeasytier_android_jni.so | Bin 0 -> 6648744 bytes
.../main/jniLibs/x86_64/libeasytier_ffi.so | Bin 0 -> 19923824 bytes
app/src/main/res/values-zh-rCN/strings.xml | 3 +
app/src/main/res/values/strings.xml | 3 +
.../utils/easytier/EasyTierTomlCodecTest.kt | 8 ++
.../easytier/VPlusConnectionCodeParserTest.kt | 56 +++++++++
17 files changed, 385 insertions(+), 31 deletions(-)
create mode 100644 app/src/main/java/com/limelight/utils/easytier/VPlusConnectionCode.kt
create mode 100644 app/src/main/jniLibs/x86_64/README.md
create mode 100644 app/src/main/jniLibs/x86_64/libeasytier_android_jni.so
create mode 100644 app/src/main/jniLibs/x86_64/libeasytier_ffi.so
create mode 100644 app/src/test/java/com/limelight/utils/easytier/VPlusConnectionCodeParserTest.kt
diff --git a/.gitignore b/.gitignore
index f2ec659f99..eba5150c75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,7 +38,11 @@ build/
*.iml
# Compiled JNI libraries folder
-**/jniLibs
+**/jniLibs/*
+!app/src/main/jniLibs/x86_64/
+!app/src/main/jniLibs/x86_64/libeasytier_android_jni.so
+!app/src/main/jniLibs/x86_64/libeasytier_ffi.so
+!app/src/main/jniLibs/x86_64/README.md
app/.externalNativeBuild/
# NDK stuff
diff --git a/app/build.gradle b/app/build.gradle
index f44c3b9c93..c7d7592977 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -22,6 +22,7 @@ def escapeBuildConfigString = { value ->
value.replace("\\", "\\\\").replace("\"", "\\\"")
}
def enableX86TestAbi = (project.findProperty("enableX86TestAbi") ?: "false").toString().toBoolean()
+def enableX8664TestAbi = (project.findProperty("enableX8664TestAbi") ?: "false").toString().toBoolean()
def audioHapticsAppLabel = "Moonlight V+"
def audioHapticsSdkDir = (
project.findProperty("audioHapticsSdkDir")
@@ -81,6 +82,9 @@ android {
if (enableX86TestAbi) {
supportedAbis += 'x86'
}
+ if (enableX8664TestAbi) {
+ supportedAbis += 'x86_64'
+ }
abiFilters(*supportedAbis)
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4b70fbd9b1..c0c0444815 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -180,6 +180,7 @@
+
+
+
+
+
+
+
+
) {
- val intent = Intent(activity, EasyTierVpnService::class.java)
+ val intent = Intent(appContext, EasyTierVpnService::class.java)
intent.putExtra("ipv4_address", ipv4)
intent.putStringArrayListExtra("proxy_cidrs", ArrayList(proxyCidrs))
intent.putExtra("instance_name", instanceName)
- activity.startService(intent)
+ appContext.startService(intent)
vpnServiceIntent = intent
}
private fun stopVpnService() {
val stopIntent = Intent(EasyTierVpnService.ACTION_STOP_VPN)
- activity.sendBroadcast(stopIntent)
+ stopIntent.setPackage(appContext.packageName)
+ appContext.sendBroadcast(stopIntent)
Log.i(TAG, "停止发送VPN广播。")
vpnServiceIntent = null
}
@@ -218,3 +220,17 @@ class EasyTierManager(
private const val MONITOR_INTERVAL = 3000L
}
}
+
+/** Keeps the active mesh alive across activity recreation and streaming UI transitions. */
+object EasyTierRuntime {
+ @Volatile
+ private var manager: EasyTierManager? = null
+
+ @Synchronized
+ fun getOrCreate(context: Context, instanceName: String, networkConfig: String): EasyTierManager {
+ val current = manager
+ if (current != null && current.networkConfig == networkConfig) return current
+ current?.stop()
+ return EasyTierManager(context, instanceName, networkConfig).also { manager = it }
+ }
+}
diff --git a/app/src/main/java/com/easytier/jni/EasyTierVpnService.kt b/app/src/main/java/com/easytier/jni/EasyTierVpnService.kt
index 1301d6f188..6e8f52bfea 100644
--- a/app/src/main/java/com/easytier/jni/EasyTierVpnService.kt
+++ b/app/src/main/java/com/easytier/jni/EasyTierVpnService.kt
@@ -78,7 +78,7 @@ class EasyTierVpnService : VpnService() {
val builder = Builder()
builder.setSession("EasyTier VPN")
.addAddress(addressInfo.ip, addressInfo.networkLength)
- .addDnsServer("223.5.5.5")
+ .addRoute(networkAddress(addressInfo.ip, addressInfo.networkLength), addressInfo.networkLength)
try {
builder.addAddress("fd00::1", 128)
@@ -162,6 +162,18 @@ class EasyTierVpnService : VpnService() {
return IpAddressInfo(parts[0], parts[1].toInt())
}
+ private fun networkAddress(ip: String, prefixLength: Int): String {
+ require(prefixLength in 0..32) { "Invalid IPv4 prefix length" }
+ val value = ip.split('.').fold(0L) { result, octet ->
+ val parsed = octet.toInt()
+ require(parsed in 0..255) { "Invalid IPv4 address" }
+ (result shl 8) or parsed.toLong()
+ }
+ val mask = if (prefixLength == 0) 0L else (0xffffffffL shl (32 - prefixLength)) and 0xffffffffL
+ val network = value and mask
+ return "${(network shr 24) and 0xff}.${(network shr 16) and 0xff}.${(network shr 8) and 0xff}.${network and 0xff}"
+ }
+
companion object {
private const val TAG = "EasyTierVpnService"
const val ACTION_STOP_VPN = "com.easytier.jni.ACTION_STOP_VPN"
diff --git a/app/src/main/java/com/limelight/PcView.kt b/app/src/main/java/com/limelight/PcView.kt
index 92ef865e6d..b8561bf8a3 100644
--- a/app/src/main/java/com/limelight/PcView.kt
+++ b/app/src/main/java/com/limelight/PcView.kt
@@ -6,6 +6,8 @@ import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.StringReader
+import java.net.InetSocketAddress
+import java.net.Socket
import java.net.UnknownHostException
import java.text.SimpleDateFormat
import java.util.Date
@@ -63,6 +65,8 @@ import com.limelight.utils.CacheHelper
import com.limelight.utils.ConfigurationSyncScheduler
import com.limelight.utils.Dialog
import com.limelight.utils.easytier.EasyTierController
+import com.limelight.utils.easytier.VPlusConnectionCode
+import com.limelight.utils.easytier.VPlusConnectionCodeParser
import com.limelight.utils.HelpLauncher
import com.limelight.utils.Iperf3Tester
import com.limelight.utils.NetHelper
@@ -213,6 +217,7 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
private const val NETWORK_QUALITY_ID = 18
private const val ADD_PC_MANUALLY_ID = 19
private const val ADD_PC_QR_SCAN_ID = 20
+ private const val REMOTE_HOST_CONNECT_TIMEOUT_MS = 30_000L
}
@@ -236,6 +241,8 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
private var lastShakeTime = 0L
private var activeSceneNumber: Int? = null
private var pendingAddedComputerUuid: String? = null
+ private var pendingRemoteConnectionCode: VPlusConnectionCode? = null
+ private var pendingConnectionIntentUrl: String? = null
private val exitGate = PcViewExitGate()
// Helpers
@@ -283,6 +290,7 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
// 与网络往返重叠,最坏情况持平、最佳情况节省整段证书时间。
managerBinder = localBinder
showPendingAddedComputer()
+ consumePendingConnectionIntent()
startComputerUpdates()
// 后台预热:等 DiscoveryService bind(mDNS 可能还没好),并把客户端证书
@@ -370,6 +378,7 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
}
easyTierController = EasyTierController(this, this)
+ captureConnectionIntent(intent)
inForeground = true
val glPrefs = GlPreferences.readPreferences(this)
@@ -389,6 +398,12 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
AboutDialogLauncher.onConfigurationChanged(this, newConfig)
}
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ setIntent(intent)
+ captureConnectionIntent(intent)
+ }
+
override fun onResume() {
super.onResume()
UiHelper.showDecoderCrashDialog(this)
@@ -2119,29 +2134,51 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
integrator.initiateScan()
}
- private fun handleQrPairResult(url: String) {
- val uri = url.toUri()
- if ("moonlight" != uri.scheme || "pair" != uri.host) {
- showToast(getString(R.string.qr_invalid_code))
+ private fun captureConnectionIntent(intent: Intent?) {
+ val uri = intent?.data ?: return
+ if (intent.action != Intent.ACTION_VIEW ||
+ !uri.scheme.equals("moonlight", ignoreCase = true) ||
+ !uri.host.equals("pair", ignoreCase = true)
+ ) {
return
}
- val host = uri.getQueryParameter("host")
- val portStr = uri.getQueryParameter("port")
- val pin = uri.getQueryParameter("pin")
+ pendingConnectionIntentUrl = uri.toString()
+ consumePendingConnectionIntent()
+ }
+
+ private fun consumePendingConnectionIntent() {
+ if (!completeOnCreateCalled || managerBinder == null || easyTierController == null) return
+ val url = pendingConnectionIntentUrl ?: return
+ pendingConnectionIntentUrl = null
+ handleQrPairResult(url)
+ }
- if (host == null || pin == null) {
+ private fun handleQrPairResult(url: String) {
+ val code = try {
+ VPlusConnectionCodeParser.parse(url)
+ } catch (_: IllegalArgumentException) {
showToast(getString(R.string.qr_invalid_code))
return
}
- var port = NvHTTP.DEFAULT_HTTP_PORT
- if (portStr != null) {
- try { port = portStr.toInt() } catch (ignored: NumberFormatException) {}
+ if (code.easyTierProfile != null) {
+ pendingRemoteConnectionCode = code
+ showToast(getString(R.string.remote_connect_preparing))
+ try {
+ easyTierController?.activateConnectionProfile(code.easyTierProfile)
+ ?: throw IllegalStateException("EasyTier controller unavailable")
+ } catch (_: Exception) {
+ pendingRemoteConnectionCode = null
+ showToast(getString(R.string.remote_connect_setup_failed))
+ }
+ } else {
+ pairFromConnectionCode(code, waitForRemoteHost = false)
}
+ }
+ private fun pairFromConnectionCode(code: VPlusConnectionCode, waitForRemoteHost: Boolean) {
showToast(getString(R.string.qr_pairing))
- val finalPort = port
uiScope.launch {
var message: String?
var success = false
@@ -2150,10 +2187,16 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
try {
stopComputerUpdatesAndWait()
+ if (waitForRemoteHost && !waitForHostEndpoint(code.host, code.port)) {
+ showToast(getString(R.string.remote_connect_host_timeout))
+ startComputerUpdates()
+ return@launch
+ }
+
val result = withContext(Dispatchers.IO) {
// Add the computer first
val addDetails = ComputerDetails()
- addDetails.manualAddress = ComputerDetails.AddressTuple(host, finalPort)
+ addDetails.manualAddress = ComputerDetails.AddressTuple(code.host, code.port)
val added = managerBinder?.addComputerBlocking(addDetails) == true
if (!added) {
return@withContext QrPairResult(getString(R.string.addpc_fail), false, null, null)
@@ -2179,7 +2222,7 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
}
val pm = httpConn.pairingManager
- val pairResult = pm.pair(httpConn.getServerInfo(true), pin)
+ val pairResult = pm.pair(httpConn.getServerInfo(true), code.pin)
when (pairResult.state) {
PairState.PIN_WRONG ->
QrPairResult(getString(R.string.pair_incorrect_pin), false, null, null)
@@ -2227,6 +2270,22 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
}
}
+ private suspend fun waitForHostEndpoint(host: String, port: Int): Boolean =
+ withContext(Dispatchers.IO) {
+ val deadline = SystemClock.elapsedRealtime() + REMOTE_HOST_CONNECT_TIMEOUT_MS
+ while (SystemClock.elapsedRealtime() < deadline) {
+ try {
+ Socket().use { socket ->
+ socket.connect(InetSocketAddress(host, port), 1_000)
+ return@withContext true
+ }
+ } catch (_: IOException) {
+ Thread.sleep(500)
+ }
+ }
+ false
+ }
+
private data class QrPairResult(
val message: String?,
val success: Boolean,
@@ -3353,6 +3412,11 @@ class PcView : Activity(), AdapterFragmentCallbacks, ShakeDetector.Listener, Eas
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == VPN_PERMISSION_REQUEST_CODE && easyTierController != null) {
easyTierController?.handleVpnPermissionResult(resultCode)
+ val pendingCode = pendingRemoteConnectionCode
+ pendingRemoteConnectionCode = null
+ if (resultCode == RESULT_OK && pendingCode != null) {
+ pairFromConnectionCode(pendingCode, waitForRemoteHost = true)
+ }
} else if (requestCode == UpdateManager.INSTALL_PERMISSION_REQUEST_CODE) {
UpdateManager.onInstallPermissionResult(this)
}
diff --git a/app/src/main/java/com/limelight/utils/easytier/EasyTierController.kt b/app/src/main/java/com/limelight/utils/easytier/EasyTierController.kt
index 0018fc4491..0276fab965 100644
--- a/app/src/main/java/com/limelight/utils/easytier/EasyTierController.kt
+++ b/app/src/main/java/com/limelight/utils/easytier/EasyTierController.kt
@@ -63,6 +63,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.doOnLayout
import com.easytier.jni.EasyTierManager
+import com.easytier.jni.EasyTierRuntime
import com.limelight.LimeLog
import com.limelight.R
import com.limelight.ui.theme.AppShapes
@@ -132,16 +133,16 @@ class EasyTierController(
private fun initEasyTierManager() {
val config = getEasyTierConfig()
- if (easyTierManager != null && easyTierManager?.latestNetworkInfoJson != null) {
- easyTierManager?.stop()
- }
- LimeLog.info("使用的easytier配置为:\n$config")
- easyTierManager = EasyTierManager(activity, instanceName, config)
+ val redactedConfig = config.replace(
+ Regex("(?m)^(network_secret\\s*=\\s*)\".*\"$"),
+ "$1\"\""
+ )
+ LimeLog.info("使用的easytier配置为:\n$redactedConfig")
+ easyTierManager = EasyTierRuntime.getOrCreate(activity.applicationContext, instanceName, config)
LimeLog.info("$TAG: EasyTierManager initialized with instance: $instanceName")
}
fun onDestroy() {
- easyTierManager?.stop()
if (currentDialog != null && currentDialog?.isShowing == true) {
currentDialog?.dismiss()
}
@@ -169,6 +170,23 @@ class EasyTierController(
}
}
+ /**
+ * Activates a host-issued V+ connection profile and starts the VPN permission flow.
+ * The structured profile is converted to an allow-listed TOML configuration here;
+ * connection codes are never allowed to inject raw EasyTier configuration.
+ */
+ internal fun activateConnectionProfile(profile: EasyTierConnectionProfile) {
+ val toml = EasyTierTomlCodec.buildConnectionProfile(profile)
+ activity.getSharedPreferences(EASYTIER_PREFS, Context.MODE_PRIVATE)
+ .edit()
+ .putString(KEY_TOML_CONFIG, toml)
+ .putString(KEY_ACTIVE_PROFILE, profile.id)
+ .putString(KEY_PROFILE_PREFIX + profile.id, toml)
+ .apply()
+ initEasyTierManager()
+ vpnCallback.requestVpnPermission()
+ }
+
// ==================== 对话框管理 ====================
private fun createAndShowDialog() {
@@ -1044,5 +1062,7 @@ class EasyTierController(
private const val TAG = "EasyTierController"
private const val EASYTIER_PREFS = "easytier_preferences"
private const val KEY_TOML_CONFIG = "toml_config_string"
+ private const val KEY_ACTIVE_PROFILE = "active_connection_profile"
+ private const val KEY_PROFILE_PREFIX = "connection_profile."
}
}
diff --git a/app/src/main/java/com/limelight/utils/easytier/EasyTierTomlCodec.kt b/app/src/main/java/com/limelight/utils/easytier/EasyTierTomlCodec.kt
index 88eede69b6..bb93992fbe 100644
--- a/app/src/main/java/com/limelight/utils/easytier/EasyTierTomlCodec.kt
+++ b/app/src/main/java/com/limelight/utils/easytier/EasyTierTomlCodec.kt
@@ -4,6 +4,7 @@ internal data class EasyTierConfigUiState(
val networkName: String = "",
val networkSecret: String = "",
val ipv4: String = "",
+ val dhcp: Boolean = false,
val listeners: String = "",
val peers: String = "",
val useSmoltcp: Boolean = false,
@@ -38,6 +39,7 @@ internal object EasyTierTomlCodec {
networkName = extractValue(toml, "network_name", ""),
networkSecret = extractValue(toml, "network_secret", ""),
ipv4 = ipv4,
+ dhcp = extractValue(toml, "dhcp", "false").toBoolean(),
listeners = extractListAsString(toml, "listeners"),
peers = extractListAsString(toml, "uri"),
useSmoltcp = extractValue(toml, "use_smoltcp", "false").toBoolean(),
@@ -60,8 +62,10 @@ internal object EasyTierTomlCodec {
val sb = StringBuilder()
appendTomlString(sb, "hostname", "moonlight-V+")
appendTomlString(sb, "instance_name", "Default")
- sb.append("dhcp = false\n")
- appendTomlString(sb, "ipv4", "${config.ipv4.ifBlank { DEFAULT_IPV4 }}/24", writeEmpty = true)
+ sb.append("dhcp = ${config.dhcp}\n")
+ if (!config.dhcp) {
+ appendTomlString(sb, "ipv4", "${config.ipv4.ifBlank { DEFAULT_IPV4 }}/24", writeEmpty = true)
+ }
appendTomlStringArray(sb, "listeners", nonBlankLines(config.listeners))
@@ -93,6 +97,17 @@ internal object EasyTierTomlCodec {
return sb.toString()
}
+ fun buildConnectionProfile(profile: EasyTierConnectionProfile): String = build(
+ EasyTierConfigUiState(
+ networkName = profile.networkName,
+ networkSecret = profile.networkSecret,
+ dhcp = true,
+ listeners = "tcp://0.0.0.0:11010\nudp://0.0.0.0:11010",
+ peers = profile.peers.joinToString("\n"),
+ latencyFirst = true
+ )
+ )
+
private fun extractValue(toml: String, key: String, defaultValue: String): String {
for (rawLine in toml.split("\n")) {
val line = rawLine.trim()
diff --git a/app/src/main/java/com/limelight/utils/easytier/VPlusConnectionCode.kt b/app/src/main/java/com/limelight/utils/easytier/VPlusConnectionCode.kt
new file mode 100644
index 0000000000..48a4792c68
--- /dev/null
+++ b/app/src/main/java/com/limelight/utils/easytier/VPlusConnectionCode.kt
@@ -0,0 +1,117 @@
+package com.limelight.utils.easytier
+
+import java.net.URI
+import java.net.URLDecoder
+import java.nio.charset.StandardCharsets
+
+internal data class EasyTierConnectionProfile(
+ val id: String,
+ val networkName: String,
+ val networkSecret: String,
+ val peers: List,
+ val hostVirtualIp: String
+)
+
+internal data class VPlusConnectionCode(
+ val host: String,
+ val port: Int,
+ val pin: String,
+ val name: String?,
+ val easyTierProfile: EasyTierConnectionProfile?
+)
+
+internal object VPlusConnectionCodeParser {
+ private const val DEFAULT_SUNSHINE_PORT = 47989
+ private val PROFILE_ID = Regex("[A-Za-z0-9._-]{1,128}")
+ private val IPV4 = Regex("(?:[0-9]{1,3}\\.){3}[0-9]{1,3}")
+ private val ALLOWED_PEER_SCHEMES = setOf("tcp", "udp", "wg", "ws", "wss", "quic")
+
+ fun parse(raw: String, nowEpochSeconds: Long = System.currentTimeMillis() / 1000): VPlusConnectionCode {
+ val uri = try {
+ URI(raw)
+ } catch (e: Exception) {
+ throw IllegalArgumentException("Invalid connection code", e)
+ }
+
+ require(uri.scheme.equals("moonlight", ignoreCase = true) && uri.host.equals("pair", ignoreCase = true)) {
+ "Unsupported connection code"
+ }
+
+ val query = parseQuery(uri.rawQuery)
+ val host = query.first("host")?.trim().orEmpty()
+ val pin = query.first("pin")?.trim().orEmpty()
+ require(host.isNotEmpty() && host.length <= 255) { "Missing host" }
+ require(pin.matches(Regex("[0-9]{4}"))) { "Invalid PIN" }
+
+ val port = query.first("port")?.toIntOrNull() ?: DEFAULT_SUNSHINE_PORT
+ require(port in 1..65535) { "Invalid port" }
+
+ val version = query.first("v")?.toIntOrNull() ?: 1
+ require(version == 1 || version == 2) { "Unsupported connection code version" }
+
+ val profile = if (version == 2) parseEasyTierProfile(query, host, nowEpochSeconds) else null
+ return VPlusConnectionCode(
+ host = host,
+ port = port,
+ pin = pin,
+ name = query.first("name")?.take(256),
+ easyTierProfile = profile
+ )
+ }
+
+ private fun parseEasyTierProfile(
+ query: Map>,
+ sunshineHost: String,
+ nowEpochSeconds: Long
+ ): EasyTierConnectionProfile {
+ val expiresAt = query.first("expires")?.toLongOrNull()
+ ?: throw IllegalArgumentException("Missing expiry")
+ require(expiresAt >= nowEpochSeconds && expiresAt <= nowEpochSeconds + 24 * 60 * 60) {
+ "Expired connection code"
+ }
+
+ val id = query.first("profile")?.trim().orEmpty()
+ val networkName = query.first("et_name")?.trim().orEmpty()
+ val networkSecret = query.first("et_secret").orEmpty()
+ val hostVirtualIp = query.first("et_host")?.trim().orEmpty()
+ val peers = query["et_peer"].orEmpty().map(String::trim).filter(String::isNotEmpty)
+
+ require(PROFILE_ID.matches(id)) { "Invalid profile identifier" }
+ require(networkName.isNotEmpty() && networkName.length <= 128) { "Invalid network name" }
+ require(networkSecret.length in 16..256) { "Invalid network secret" }
+ require(isValidIpv4(hostVirtualIp) && sunshineHost == hostVirtualIp) { "Invalid virtual host address" }
+ require(peers.isNotEmpty() && peers.size <= 8) { "Invalid peer list" }
+ peers.forEach { peer ->
+ require(peer.length <= 512) { "Peer URL is too long" }
+ val peerUri = runCatching { URI(peer) }.getOrNull()
+ ?: throw IllegalArgumentException("Invalid peer URL")
+ require(peerUri.scheme?.lowercase() in ALLOWED_PEER_SCHEMES && peerUri.host != null) {
+ "Unsupported peer URL"
+ }
+ }
+
+ return EasyTierConnectionProfile(id, networkName, networkSecret, peers, hostVirtualIp)
+ }
+
+ private fun isValidIpv4(value: String): Boolean {
+ if (!IPV4.matches(value)) return false
+ return value.split('.').all { it.toIntOrNull() in 0..255 }
+ }
+
+ private fun parseQuery(rawQuery: String?): Map> {
+ if (rawQuery.isNullOrEmpty()) return emptyMap()
+ val result = linkedMapOf>()
+ rawQuery.split('&').forEach { entry ->
+ val parts = entry.split('=', limit = 2)
+ val key = decode(parts[0])
+ val value = decode(parts.getOrElse(1) { "" })
+ result.getOrPut(key) { mutableListOf() }.add(value)
+ }
+ return result
+ }
+
+ private fun decode(value: String): String =
+ URLDecoder.decode(value, StandardCharsets.UTF_8.name())
+
+ private fun Map>.first(key: String): String? = get(key)?.firstOrNull()
+}
diff --git a/app/src/main/jniLibs/x86_64/README.md b/app/src/main/jniLibs/x86_64/README.md
new file mode 100644
index 0000000000..ec8ac8b1db
--- /dev/null
+++ b/app/src/main/jniLibs/x86_64/README.md
@@ -0,0 +1,17 @@
+# EasyTier x86_64 Android test libraries
+
+These libraries exist only to support emulator E2E builds. They are packaged
+when Gradle is invoked with `-PenableX8664TestAbi=true`; normal release builds
+continue to package the production ARM ABI only.
+
+- EasyTier source: official `v2.6.4` release
+- Target: `x86_64-linux-android`, Android API 21
+- Android NDK: 28.2
+- Rust: 1.95.0
+- `libeasytier_android_jni.so` SHA-256:
+ `80ECDEBAE94D01D1EC6AF6B14ED02D134F1C94FAB7E45F91F622897483E49A1D`
+- `libeasytier_ffi.so` SHA-256:
+ `795BA53B2D23D20CD942C30A15CE95E6B6F65607ED55ADF791AC34AAFA0C8F38`
+
+The JNI library is linked with an explicit `DT_NEEDED` dependency on
+`libeasytier_ffi.so`, matching Android's namespace-based native loader.
diff --git a/app/src/main/jniLibs/x86_64/libeasytier_android_jni.so b/app/src/main/jniLibs/x86_64/libeasytier_android_jni.so
new file mode 100644
index 0000000000000000000000000000000000000000..cc82f1c39db15d2b5c668cc8e1e2df22533d7c28
GIT binary patch
literal 6648744
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
%Mu^MGtD!ES6`I)
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ⅅI-zNz`bWld=9rfQ@Hg*0YB~~N34(YCH$l3
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