Skip to content
Open
Show file tree
Hide file tree
Changes from all 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