Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -81,6 +82,9 @@ android {
if (enableX86TestAbi) {
supportedAbis += 'x86'
}
if (enableX8664TestAbi) {
supportedAbis += 'x86_64'
}
abiFilters(*supportedAbis)
}
}
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
<activity
android:name=".PcView"
android:exported="true"
android:launchMode="singleTop"
android:resizeableActivity="true"
android:banner="@drawable/atv_banner"
android:icon="@mipmap/ic_launcher_saba"
Expand All @@ -200,6 +201,16 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<category android:name="tv.ouya.intent.category.APP" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="moonlight"
android:host="pair" />
</intent-filter>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</activity>
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
<activity
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/java/com/easytier/jni/EasyTierJNI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ package com.easytier.jni
object EasyTierJNI {

init {
// 加载本地库 (libeasytier_android_jni.so)
// The JNI bridge calls symbols exported by libeasytier_ffi.so. Load the
// dependency explicitly as well as retaining the ELF DT_NEEDED link used
// by release builds, so test ABIs and vendor-packaged builds initialize in
// the same deterministic order.
System.loadLibrary("easytier_ffi")
System.loadLibrary("easytier_android_jni")
}

Expand Down
40 changes: 33 additions & 7 deletions app/src/main/java/com/easytier/jni/EasyTierManager.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.easytier.jni

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
Expand All @@ -11,10 +11,12 @@ import org.json.JSONObject
import java.util.Objects

class EasyTierManager(
private val activity: Activity,
context: Context,
private val instanceName: String,
private val networkConfig: String
internal val networkConfig: String,
internal val allowedRemoteHost: String? = null
) {
private val appContext = context.applicationContext
private val handler = Handler(Looper.getMainLooper())

@Volatile
Expand Down Expand Up @@ -126,7 +128,7 @@ class EasyTierManager(
}

val proxyCidrsArray = route.optJSONArray("proxy_cidrs")
if (proxyCidrsArray != null) {
if (allowedRemoteHost == null && proxyCidrsArray != null) {
for (j in 0 until proxyCidrsArray.length()) {
newProxyCidrs.add(proxyCidrsArray.getString(j))
}
Expand Down Expand Up @@ -177,17 +179,19 @@ class EasyTierManager(
}

private fun startVpnService(ipv4: String, proxyCidrs: List<String>) {
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("allowed_remote_host", allowedRemoteHost)
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
}
Expand Down Expand Up @@ -218,3 +222,25 @@ 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,
allowedRemoteHost: String? = null
): EasyTierManager {
val current = manager
if (current != null &&
current.networkConfig == networkConfig &&
current.allowedRemoteHost == allowedRemoteHost
) return current
current?.stop()
return EasyTierManager(context, instanceName, networkConfig, allowedRemoteHost).also { manager = it }
}
}
59 changes: 45 additions & 14 deletions app/src/main/java/com/easytier/jni/EasyTierVpnService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,15 @@ class EasyTierVpnService : VpnService() {
try {
val ipv4Address = intent.getStringExtra("ipv4_address")
val proxyCidrs = intent.getStringArrayListExtra("proxy_cidrs")
val allowedRemoteHost = intent.getStringExtra("allowed_remote_host")
instanceName = intent.getStringExtra("instance_name")

if (ipv4Address == null || instanceName == null) {
cleanupAndStop()
return@Thread
}

setupVpnInterface(ipv4Address, proxyCidrs ?: ArrayList())
setupVpnInterface(ipv4Address, proxyCidrs ?: ArrayList(), allowedRemoteHost)
} catch (t: Throwable) {
Log.e(TAG, "VPN设置线程失败", t)
cleanupAndStop()
Expand All @@ -71,25 +72,38 @@ class EasyTierVpnService : VpnService() {
return START_NOT_STICKY
}

private fun setupVpnInterface(ipv4Address: String, proxyCidrs: List<String>) {
private fun setupVpnInterface(
ipv4Address: String,
proxyCidrs: List<String>,
allowedRemoteHost: String?
) {
try {
val addressInfo = parseIpv4Address(ipv4Address)

val builder = Builder()
builder.setSession("EasyTier VPN")
.addAddress(addressInfo.ip, addressInfo.networkLength)
.addDnsServer("223.5.5.5")

try {
builder.addAddress("fd00::1", 128)
Log.i(TAG, "已激活 VPN 接口 IPv6 协议栈 (fd00::1/128) 以支持双栈通信")
} catch (e: Exception) {
Log.w(TAG, "添加 IPv6 地址失败", e)
val builder = Builder().setSession("EasyTier VPN")

if (allowedRemoteHost != null) {
// A host-issued profile may reach exactly one overlay host and nothing else.
requireValidIpv4(allowedRemoteHost)
builder.addAddress(addressInfo.ip, 32)
.addRoute(allowedRemoteHost, 32)
Log.i(TAG, "为主机签发配置添加唯一允许路由:$allowedRemoteHost/32")
} else {
builder.addAddress(addressInfo.ip, addressInfo.networkLength)
.addRoute(networkAddress(addressInfo.ip, addressInfo.networkLength), addressInfo.networkLength)
Log.i(TAG, "为虚拟网络添加了VPN路由:${addressInfo.ip}/${addressInfo.networkLength}")
}

Log.i(TAG, "为虚拟网络添加了VPN路由:${addressInfo.ip}/${addressInfo.networkLength}")
if (allowedRemoteHost == null) {
try {
builder.addAddress("fd00::1", 128)
Log.i(TAG, "已激活 VPN 接口 IPv6 协议栈 (fd00::1/128) 以支持双栈通信")
} catch (e: Exception) {
Log.w(TAG, "添加 IPv6 地址失败", e)
}
}

for (cidr in proxyCidrs) {
for (cidr in if (allowedRemoteHost == null) proxyCidrs else emptyList()) {
Log.i(TAG, "为虚拟网络添加代理CIDR:$cidr")
try {
val routeInfo = parseCidr(cidr)
Expand Down Expand Up @@ -162,6 +176,23 @@ 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}"
}

private fun requireValidIpv4(ip: String) {
require(ip.split('.').size == 4) { "Invalid IPv4 address" }
ip.split('.').forEach { octet -> require(octet.toIntOrNull() in 0..255) { "Invalid IPv4 address" } }
}

companion object {
private const val TAG = "EasyTierVpnService"
const val ACTION_STOP_VPN = "com.easytier.jni.ACTION_STOP_VPN"
Expand Down
Loading
Loading