base android logic

This commit is contained in:
2026-03-12 19:10:16 +07:00
parent 2f7462e299
commit dfc24b8da3
5 changed files with 123 additions and 135 deletions
+1 -5
View File
@@ -3,9 +3,5 @@
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": ["main"],
"permissions": [ "permissions": ["core:default", "vpn:allow-vpn-control"]
"core:default",
"vpn:allow-start-tunnel",
"vpn:allow-stop-tunnel"
]
} }
@@ -32,6 +32,7 @@ android {
} }
dependencies { dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
implementation("net.java.dev.jna:jna:5.13.0@aar") implementation("net.java.dev.jna:jna:5.13.0@aar")
implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.0") implementation("androidx.appcompat:appcompat:1.6.0")
@@ -5,12 +5,16 @@
<application> <application>
<service <service
android:name="app.netrunner.vpn.VpnPlugin" android:name=".VpnService"
android:permission="android.permission.BIND_VPN_SERVICE" android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.net.VpnService"/> <action android:name="android.net.VpnService"/>
</intent-filter> </intent-filter>
<meta-data
android:name="android.net.VpnService.SUPPORTS_USER_CONTROL"
android:value="true" />
</service> </service>
</application> </application>
</manifest> </manifest>
@@ -2,150 +2,120 @@ package app.netrunner.vpn
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.VpnService
import android.os.ParcelFileDescriptor
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import app.tauri.plugin.Plugin import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke import app.tauri.plugin.Invoke
import app.tauri.annotation.InvokeArg import app.tauri.annotation.InvokeArg
import app.tauri.annotation.Command import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin import app.tauri.annotation.TauriPlugin
import app.tauri.annotation.Permission
import android.Manifest
import app.netrunner.vpn.VpnController
import android.os.ParcelFileDescriptor
import android.content.Context
import uniffi.netrunner_client.SessionManager import uniffi.netrunner_client.SessionManager
import uniffi.netrunner_client.Session import uniffi.netrunner_client.Session
import uniffi.netrunner_client.NoHandle
import android.net.VpnService as AndroidVpnService import com.fasterxml.jackson.annotation.JsonCreator
import app.netrunner.vpn.TunInterface import com.fasterxml.jackson.annotation.JsonProperty
import android.os.Parcelable
import kotlinx.parcelize.Parcelize // ------------------------------------------------
import java.io.Closeable // DATA
// ------------------------------------------------
@Parcelize @Parcelize
data class TunnelConfig( data class TunnelConfig @JsonCreator constructor(
val address: String, @JsonProperty("address") val address: String,
val prefix: Int, @JsonProperty("prefix") val prefix: Int,
val dns: String, @JsonProperty("dns") val dns: String,
val mtu: Int @JsonProperty("mtu") val mtu: Int
) : Parcelable ) : Parcelable
// ------------------------------------------------
// VPN SERVICE
// ------------------------------------------------
class TunInterface : Closeable { class TunVpnService : VpnService() {
private var vpnInterface: ParcelFileDescriptor? = null
fun establish(builder: AndroidVpnService.Builder, config: TunnelConfig): Int {
// Принудительно закрываем текущий, если вдруг вызываем повторно
close()
val descriptor = builder
.addAddress(config.address, config.prefix)
.addRoute("0.0.0.0", 0)
.setMtu(config.mtu)
.establish()
vpnInterface = descriptor
return descriptor?.fd ?: -1
}
override fun close() {
try {
vpnInterface?.close()
} catch (e: Exception) {
e.printStackTrace()
} finally {
vpnInterface = null
}
}
}
class VpnController(private val service: VpnService) {
init {
System.loadLibrary("netrunner_client")
}
private var activeSession: Session? = null private var activeSession: Session? = null
private val tunInterface = TunInterface() private var vpnInterface: ParcelFileDescriptor? = null
fun startVpn(config: TunnelConfig, ip: String, port: Int) {
val builder = service.Builder()
val fd = tunInterface.establish(builder, config)
if (fd != -1) {
val manager = SessionManager(NoHandle)
android.util.Log.d("VPN_DEBUG", "Starting session with FD: $fd")
activeSession = manager.start("$ip:$port", fd)
} else {
android.util.Log.d("VPN_DEBUG", "Starting session with FD: $fd")
}
}
fun stopVpn() {
activeSession?.stop()
activeSession = null
tunInterface.close()
}
}
class VpnService : AndroidVpnService() {
private var controller: VpnController? = null
override fun onCreate() {
super.onCreate()
android.util.Log.d("VPN_DEBUG", "VpnService: onCreate() executed")
try {
controller = VpnController(this)
android.util.Log.d("VPN_DEBUG", "VpnService: VpnController initialized successfully")
} catch (e: Exception) {
android.util.Log.e("VPN_DEBUG", "VpnService: CRITICAL - VpnController init failed", e)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val action = intent?.action
android.util.Log.d("VPN_DEBUG", "VpnService: onStartCommand() action='$action'")
when (action) { when (intent?.action) {
"START_VPN" -> { "START_VPN" -> {
val ip = intent.getStringExtra("ip")
val port = intent.getIntExtra("port", -1)
android.util.Log.d("VPN_DEBUG", "VpnService: Extracting extras -> IP: '$ip', Port: $port")
val config = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { val ip = intent.getStringExtra("ip") ?: ""
val port = intent.getIntExtra("port", 0)
val config = if (android.os.Build.VERSION.SDK_INT >= 33) {
intent.getParcelableExtra("config", TunnelConfig::class.java) intent.getParcelableExtra("config", TunnelConfig::class.java)
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
intent.getParcelableExtra("config") intent.getParcelableExtra("config")
} }
if (config == null) { if (config != null) {
android.util.Log.e("VPN_DEBUG", "VpnService: ERROR - TunnelConfig is NULL! Check if parcelable passed correctly.") startVpn(config, ip, port)
} else {
android.util.Log.d("VPN_DEBUG", "VpnService: Config received: $config. Calling controller.startVpn()...")
controller?.startVpn(config, ip ?: "", port)
} }
} }
"STOP_VPN" -> { "STOP_VPN" -> {
android.util.Log.d("VPN_DEBUG", "VpnService: Stopping VPN...") stopVpn()
controller?.stopVpn()
stopSelf() stopSelf()
} }
else -> {
android.util.Log.w("VPN_DEBUG", "VpnService: Unknown action received: '$action'")
}
} }
return START_STICKY return START_STICKY
} }
private fun startVpn(config: TunnelConfig, ip: String, port: Int) {
val builder = Builder()
.addAddress(config.address, config.prefix)
.addRoute("0.0.0.0", 0)
.setMtu(config.mtu)
vpnInterface = builder.establish()
val fd = vpnInterface?.detachFd() ?: return
try {
val manager = SessionManager()
activeSession = manager.start("$ip:$port", fd)
} catch (e: Exception) {
android.util.Log.e("VPN_DEBUG", "manager.start failed", e)
}
}
private fun stopVpn() {
activeSession?.stop()
activeSession = null
try {
vpnInterface?.close()
} catch (_: Exception) {}
vpnInterface = null
}
override fun onDestroy() { override fun onDestroy() {
controller?.stopVpn() stopVpn()
super.onDestroy() super.onDestroy()
} }
} }
// ------------------------------------------------
// TAURI PLUGIN
// ------------------------------------------------
@TauriPlugin
class VpnPlugin(private val activity: Activity) : Plugin(activity) {
init {
System.loadLibrary("netrunner_client")
}
@InvokeArg @InvokeArg
class VpnArgs { class VpnArgs {
@@ -154,39 +124,56 @@ class VpnArgs {
var port: Int = 0 var port: Int = 0
} }
@TauriPlugin private var pendingArgs: VpnArgs? = null
class VpnPlugin(private val activity: Activity) : Plugin(activity) {
@Command @Command
fun startVpn(invoke: Invoke) { fun startVpn(invoke: Invoke) {
android.util.Log.d("VPN_DEBUG", "Plugin: startVpn invoked")
val args = invoke.parseArgs(VpnArgs::class.java) val args = invoke.parseArgs(VpnArgs::class.java)
val config = args.config
if (config == null) {
invoke.reject("Config is null")
return
}
val intent = Intent(activity, VpnService::class.java).apply { val permissionIntent = VpnService.prepare(activity)
if (permissionIntent != null) {
pendingArgs = args
activity.startActivityForResult(permissionIntent, 100)
invoke.resolve() // не reject
return
}
startVpnService(args)
invoke.resolve()
}
private fun startVpnService(args: VpnArgs) {
val intent = Intent(activity, TunVpnService::class.java).apply {
action = "START_VPN" action = "START_VPN"
putExtra("ip", args.ip) putExtra("ip", args.ip)
putExtra("port", args.port) putExtra("port", args.port)
putExtra("config", args.config) putExtra("config", args.config)
} }
android.util.Log.d("VPN_DEBUG", "Plugin: Starting VpnService...") activity.startService(intent)
val result = activity.startService(intent)
if (result == null) {
android.util.Log.e("VPN_DEBUG", "Plugin: startService returned null (Service not found?)")
} else {
android.util.Log.d("VPN_DEBUG", "Plugin: startService triggered successfully")
}
invoke.resolve()
} }
@Command @Command
fun stopVpn(invoke: Invoke) { fun stopVpn(invoke: Invoke) {
val intent = Intent(activity, VpnService::class.java).apply {
val intent = Intent(activity, TunVpnService::class.java).apply {
action = "STOP_VPN" action = "STOP_VPN"
} }
activity.startService(intent) activity.startService(intent)
invoke.resolve() invoke.resolve()
} }
} }
+1 -1
View File
@@ -17,7 +17,7 @@ export function VpnControl() {
// Подготавливаем конфигурацию (замените на нужные вам значения) // Подготавливаем конфигурацию (замените на нужные вам значения)
const config: TunnelConfig = { const config: TunnelConfig = {
address: "10.0.0.2", address: "10.0.0.1",
prefix: 24, prefix: 24,
dns: "8.8.8.8", dns: "8.8.8.8",
mtu: 1400, mtu: 1400,