base vpn android plugin

This commit is contained in:
2026-03-12 17:37:47 +07:00
parent c765246d79
commit 2f7462e299
28 changed files with 874 additions and 132 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"rust-analyzer.cargo.features": ["mobile", "desktop"],
"rust-analyzer.cargo.allTargets": true,
"rust-analyzer.check.allTargets": true,
"rust-analyzer.check.targets": [
"x86_64-unknown-linux-gnu",
"aarch64-linux-android",
"x86_64-linux-android"
],
"rust-analyzer.procMacro.enable": true
}
+6 -1
View File
@@ -12,9 +12,14 @@ tauri-build = { version = "2.0.0", features = [] }
[dependencies] [dependencies]
jni = "0.22.3" jni = "0.22.3"
tauri = { version = "2.0.0", features = [] } tauri = { version = "2.10.3", features = [] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
ndk-context = "0.1" ndk-context = "0.1"
log = "0.4" log = "0.4"
android_logger = "0.15.1" android_logger = "0.15.1"
tauri-plugin-vpn = { path = "./src/plugins/vpn-plugin" } tauri-plugin-vpn = { path = "./src/plugins/vpn-plugin" }
[features]
mobile = []
desktop = []
+2 -4
View File
@@ -5,9 +5,7 @@
"windows": ["main"], "windows": ["main"],
"permissions": [ "permissions": [
"core:default", "core:default",
{ "vpn:allow-start-tunnel",
"identifier": "core:app:default", "vpn:allow-stop-tunnel"
"allow": ["start_vpn_android"]
}
] ]
} }
+1
View File
@@ -8,6 +8,7 @@ pub fn run() {
.with_tag("NetrunnerRust"), .with_tag("NetrunnerRust"),
); );
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_vpn::init())
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }
@@ -20,3 +20,7 @@ dist
/android/.tauri /android/.tauri
/android/.gradle /android/.gradle
/android/build /android/build
*jniLibs
*uniffi
*rollup.cache
/android/gradle
@@ -15,3 +15,7 @@ thiserror = "2"
[build-dependencies] [build-dependencies]
tauri-plugin = { version = "2.5.4", features = ["build"] } tauri-plugin = { version = "2.5.4", features = ["build"] }
[features]
mobile = []
desktop = []
@@ -1,6 +1,7 @@
plugins { plugins {
id("com.android.library") id("com.android.library")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("kotlin-parcelize")
} }
android { android {
@@ -10,6 +11,7 @@ android {
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 21
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
@@ -18,9 +20,19 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "17" jvmTarget = "17"
} }
sourceSets {
getByName("main") {
// Добавляем папку с исходным кодом (где лежат твои uniffi .kt файлы)
java.srcDirs("src/main/java")
// Оставляем jniLibs для .so файлов
jniLibs.srcDirs("src/main/jniLibs")
}
}
} }
dependencies { dependencies {
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")
implementation(project(":tauri-android")) implementation(project(":tauri-android"))
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.netrunner.vpn"> package="app.netrunner.vpn">
<uses-permission android:name="android.permission.BIND_VPN_SERVICE" />
<application> <application>
<service <service
android:name=".ExampleVpnService" android:name="app.netrunner.vpn.VpnPlugin"
android:permission="android.permission.BIND_VPN_SERVICE" android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
@@ -1,6 +0,0 @@
package app.netrunner.vpn;
import android.net.VpnService;
class VpnEngine : VpnService() {
}
@@ -0,0 +1,192 @@
package app.netrunner.vpn
import android.app.Activity
import android.content.Intent
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.Command
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.Session
import uniffi.netrunner_client.NoHandle
import android.net.VpnService as AndroidVpnService
import app.netrunner.vpn.TunInterface
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.io.Closeable
@Parcelize
data class TunnelConfig(
val address: String,
val prefix: Int,
val dns: String,
val mtu: Int
) : Parcelable
class TunInterface : Closeable {
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 val tunInterface = TunInterface()
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 {
val action = intent?.action
android.util.Log.d("VPN_DEBUG", "VpnService: onStartCommand() action='$action'")
when (action) {
"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) {
intent.getParcelableExtra("config", TunnelConfig::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra("config")
}
if (config == null) {
android.util.Log.e("VPN_DEBUG", "VpnService: ERROR - TunnelConfig is NULL! Check if parcelable passed correctly.")
} else {
android.util.Log.d("VPN_DEBUG", "VpnService: Config received: $config. Calling controller.startVpn()...")
controller?.startVpn(config, ip ?: "", port)
}
}
"STOP_VPN" -> {
android.util.Log.d("VPN_DEBUG", "VpnService: Stopping VPN...")
controller?.stopVpn()
stopSelf()
}
else -> {
android.util.Log.w("VPN_DEBUG", "VpnService: Unknown action received: '$action'")
}
}
return START_STICKY
}
override fun onDestroy() {
controller?.stopVpn()
super.onDestroy()
}
}
@InvokeArg
class VpnArgs {
var config: TunnelConfig? = null
var ip: String = ""
var port: Int = 0
}
@TauriPlugin
class VpnPlugin(private val activity: Activity) : Plugin(activity) {
@Command
fun startVpn(invoke: Invoke) {
android.util.Log.d("VPN_DEBUG", "Plugin: startVpn invoked")
val args = invoke.parseArgs(VpnArgs::class.java)
val intent = Intent(activity, VpnService::class.java).apply {
action = "START_VPN"
putExtra("ip", args.ip)
putExtra("port", args.port)
putExtra("config", args.config)
}
android.util.Log.d("VPN_DEBUG", "Plugin: Starting VpnService...")
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
fun stopVpn(invoke: Invoke) {
val intent = Intent(activity, VpnService::class.java).apply {
action = "STOP_VPN"
}
activity.startService(intent)
invoke.resolve()
}
}
+3 -1
View File
@@ -1,4 +1,6 @@
const COMMANDS: &[&str] = &["ping"]; // src-tauri/src/plugins/vpn-plugin/build.rs
const COMMANDS: &[&str] = &["start_tunnel", "stop_tunnel"];
fn main() { fn main() {
tauri_plugin::Builder::new(COMMANDS) tauri_plugin::Builder::new(COMMANDS)
@@ -1,9 +1,20 @@
import { invoke } from '@tauri-apps/api/core' import { invoke } from "@tauri-apps/api/core";
export async function ping(value: string): Promise<string | null> { export interface TunnelConfig {
return await invoke<{value?: string}>('plugin:vpn|ping', { address: string;
payload: { prefix: number;
value, dns: string;
}, mtu: number;
}).then((r) => (r.value ? r.value : null)); }
export async function startVpn(config: TunnelConfig, ip: string, port: number) {
return await invoke("plugin:vpn|start_vpn", {
config,
ip,
port,
});
}
export async function stopVpn() {
return await invoke("plugin:vpn|stop_vpn");
} }
@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-ping"
description = "Enables the ping command without any pre-configured scope."
commands.allow = ["ping"]
[[permission]]
identifier = "deny-ping"
description = "Denies the ping command without any pre-configured scope."
commands.deny = ["ping"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-start-tunnel"
description = "Enables the start_tunnel command without any pre-configured scope."
commands.allow = ["start_tunnel"]
[[permission]]
identifier = "deny-start-tunnel"
description = "Denies the start_tunnel command without any pre-configured scope."
commands.deny = ["start_tunnel"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-stop-tunnel"
description = "Enables the stop_tunnel command without any pre-configured scope."
commands.allow = ["stop_tunnel"]
[[permission]]
identifier = "deny-stop-tunnel"
description = "Denies the stop_tunnel command without any pre-configured scope."
commands.deny = ["stop_tunnel"]
@@ -10,12 +10,12 @@
<tr> <tr>
<td> <td>
`vpn:allow-ping` `vpn:allow-start-tunnel`
</td> </td>
<td> <td>
Enables the ping command without any pre-configured scope. Enables the start_tunnel command without any pre-configured scope.
</td> </td>
</tr> </tr>
@@ -23,12 +23,12 @@ Enables the ping command without any pre-configured scope.
<tr> <tr>
<td> <td>
`vpn:deny-ping` `vpn:deny-start-tunnel`
</td> </td>
<td> <td>
Denies the ping command without any pre-configured scope. Denies the start_tunnel command without any pre-configured scope.
</td> </td>
</tr> </tr>
@@ -36,12 +36,38 @@ Denies the ping command without any pre-configured scope.
<tr> <tr>
<td> <td>
`vpn:vpn-control` `vpn:allow-stop-tunnel`
</td> </td>
<td> <td>
Allows access to VPN Service Enables the stop_tunnel command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`vpn:deny-stop-tunnel`
</td>
<td>
Denies the stop_tunnel command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`vpn:allow-vpn-control`
</td>
<td>
Allows Vpn Control
</td> </td>
</tr> </tr>
@@ -1,5 +1,6 @@
[[permission]] [[permission]]
identifier = "vpn-control" identifier = "allow-vpn-control"
description = "Allows access to VPN Service" description = "Allows Vpn Control"
name = "allow-vpn-control"
commands = { allow = ["ping", "prepare_vpn", "start_vpn", "stop_vpn"] } [permission.commands]
allow = ["start_vpn", "stop_vpn"]
@@ -295,22 +295,34 @@
"type": "string", "type": "string",
"oneOf": [ "oneOf": [
{ {
"description": "Enables the ping command without any pre-configured scope.", "description": "Enables the start_tunnel command without any pre-configured scope.",
"type": "string", "type": "string",
"const": "allow-ping", "const": "allow-start-tunnel",
"markdownDescription": "Enables the ping command without any pre-configured scope." "markdownDescription": "Enables the start_tunnel command without any pre-configured scope."
}, },
{ {
"description": "Denies the ping command without any pre-configured scope.", "description": "Denies the start_tunnel command without any pre-configured scope.",
"type": "string", "type": "string",
"const": "deny-ping", "const": "deny-start-tunnel",
"markdownDescription": "Denies the ping command without any pre-configured scope." "markdownDescription": "Denies the start_tunnel command without any pre-configured scope."
}, },
{ {
"description": "Allows access to VPN Service", "description": "Enables the stop_tunnel command without any pre-configured scope.",
"type": "string", "type": "string",
"const": "vpn-control", "const": "allow-stop-tunnel",
"markdownDescription": "Allows access to VPN Service" "markdownDescription": "Enables the stop_tunnel command without any pre-configured scope."
},
{
"description": "Denies the stop_tunnel command without any pre-configured scope.",
"type": "string",
"const": "deny-stop-tunnel",
"markdownDescription": "Denies the stop_tunnel command without any pre-configured scope."
},
{
"description": "Allows Vpn Control",
"type": "string",
"const": "allow-vpn-control",
"markdownDescription": "Allows Vpn Control"
} }
] ]
} }
+402
View File
@@ -0,0 +1,402 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@tauri-apps/api':
specifier: ^2.0.0
version: 2.10.1
devDependencies:
'@rollup/plugin-typescript':
specifier: ^12.0.0
version: 12.3.0(rollup@4.59.0)(tslib@2.8.1)(typescript@5.9.3)
rollup:
specifier: ^4.9.6
version: 4.59.0
tslib:
specifier: ^2.6.2
version: 2.8.1
typescript:
specifier: ^5.3.3
version: 5.9.3
packages:
'@rollup/plugin-typescript@12.3.0':
resolution: {integrity: sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^2.14.0||^3.0.0||^4.0.0
tslib: '*'
typescript: '>=3.7.0'
peerDependenciesMeta:
rollup:
optional: true
tslib:
optional: true
'@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/rollup-android-arm-eabi@4.59.0':
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.59.0':
resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.59.0':
resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.59.0':
resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.59.0':
resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.59.0':
resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.59.0':
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.59.0':
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.59.0':
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.59.0':
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.59.0':
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.59.0':
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.59.0':
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.59.0':
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.59.0':
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.59.0':
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.59.0':
resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.59.0':
resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.59.0':
resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.59.0':
resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.59.0':
resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==}
cpu: [x64]
os: [win32]
'@tauri-apps/api@2.10.1':
resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
is-core-module@2.16.1:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
resolve@1.22.11:
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
engines: {node: '>= 0.4'}
hasBin: true
rollup@4.59.0:
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
snapshots:
'@rollup/plugin-typescript@12.3.0(rollup@4.59.0)(tslib@2.8.1)(typescript@5.9.3)':
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.59.0)
resolve: 1.22.11
typescript: 5.9.3
optionalDependencies:
rollup: 4.59.0
tslib: 2.8.1
'@rollup/pluginutils@5.3.0(rollup@4.59.0)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
optionalDependencies:
rollup: 4.59.0
'@rollup/rollup-android-arm-eabi@4.59.0':
optional: true
'@rollup/rollup-android-arm64@4.59.0':
optional: true
'@rollup/rollup-darwin-arm64@4.59.0':
optional: true
'@rollup/rollup-darwin-x64@4.59.0':
optional: true
'@rollup/rollup-freebsd-arm64@4.59.0':
optional: true
'@rollup/rollup-freebsd-x64@4.59.0':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-arm64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-loong64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-x64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-x64-musl@4.59.0':
optional: true
'@rollup/rollup-openbsd-x64@4.59.0':
optional: true
'@rollup/rollup-openharmony-arm64@4.59.0':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.59.0':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.59.0':
optional: true
'@rollup/rollup-win32-x64-gnu@4.59.0':
optional: true
'@rollup/rollup-win32-x64-msvc@4.59.0':
optional: true
'@tauri-apps/api@2.10.1': {}
'@types/estree@1.0.8': {}
estree-walker@2.0.2: {}
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
is-core-module@2.16.1:
dependencies:
hasown: 2.0.2
path-parse@1.0.7: {}
picomatch@4.0.3: {}
resolve@1.22.11:
dependencies:
is-core-module: 2.16.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
rollup@4.59.0:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.59.0
'@rollup/rollup-android-arm64': 4.59.0
'@rollup/rollup-darwin-arm64': 4.59.0
'@rollup/rollup-darwin-x64': 4.59.0
'@rollup/rollup-freebsd-arm64': 4.59.0
'@rollup/rollup-freebsd-x64': 4.59.0
'@rollup/rollup-linux-arm-gnueabihf': 4.59.0
'@rollup/rollup-linux-arm-musleabihf': 4.59.0
'@rollup/rollup-linux-arm64-gnu': 4.59.0
'@rollup/rollup-linux-arm64-musl': 4.59.0
'@rollup/rollup-linux-loong64-gnu': 4.59.0
'@rollup/rollup-linux-loong64-musl': 4.59.0
'@rollup/rollup-linux-ppc64-gnu': 4.59.0
'@rollup/rollup-linux-ppc64-musl': 4.59.0
'@rollup/rollup-linux-riscv64-gnu': 4.59.0
'@rollup/rollup-linux-riscv64-musl': 4.59.0
'@rollup/rollup-linux-s390x-gnu': 4.59.0
'@rollup/rollup-linux-x64-gnu': 4.59.0
'@rollup/rollup-linux-x64-musl': 4.59.0
'@rollup/rollup-openbsd-x64': 4.59.0
'@rollup/rollup-openharmony-arm64': 4.59.0
'@rollup/rollup-win32-arm64-msvc': 4.59.0
'@rollup/rollup-win32-ia32-msvc': 4.59.0
'@rollup/rollup-win32-x64-gnu': 4.59.0
'@rollup/rollup-win32-x64-msvc': 4.59.0
fsevents: 2.3.3
supports-preserve-symlinks-flag@1.0.0: {}
tslib@2.8.1: {}
typescript@5.9.3: {}
@@ -1,13 +1,20 @@
use tauri::{AppHandle, command, Runtime}; use crate::{models::*, Vpn, VpnInterface};
use tauri::{command, AppHandle, Manager, Runtime};
use crate::models::*;
use crate::Result;
use crate::VpnExt;
#[command] #[command]
pub(crate) async fn ping<R: Runtime>( pub(crate) fn start_vpn<R: Runtime>(
app: AppHandle<R>, app: AppHandle<R>,
payload: PingRequest, config: TunnelConfig,
) -> Result<PingResponse> { ip: String,
app.vpn().ping(payload) port: i32,
) -> crate::Result<()> {
// Получаем состояние Vpn<R> и вызываем метод интерфейса
eprintln!("--- [DEBUG] START_VPN COMMAND CALLED! ---");
app.state::<Vpn<R>>().start_vpn(config, ip, port)
}
#[command]
pub(crate) fn stop_vpn<R: Runtime>(app: AppHandle<R>) -> crate::Result<()> {
// Получаем состояние Vpn<R> и вызываем метод интерфейса
app.state::<Vpn<R>>().stop_vpn()
} }
+15 -12
View File
@@ -1,22 +1,25 @@
use crate::models::*;
use crate::VpnInterface;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime}; use tauri::{plugin::PluginApi, AppHandle, Runtime};
use crate::models::*; // Инициализация для ПК
pub fn init<R: Runtime, C: DeserializeOwned>( pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>, app: &AppHandle<R>,
_api: PluginApi<R, C>, _api: PluginApi<R, C>,
) -> crate::Result<Vpn<R>> { ) -> crate::Result<Vpn<R>> {
Ok(Vpn(app.clone())) Ok(Vpn(app.clone()))
} }
/// Access to the vpn APIs.
pub struct Vpn<R: Runtime>(AppHandle<R>); pub struct Vpn<R: Runtime>(AppHandle<R>);
impl<R: Runtime> Vpn<R> { impl<R: Runtime> VpnInterface<R> for Vpn<R> {
pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> { fn start_vpn(&self, _config: TunnelConfig, _ip: String, _port: i32) -> crate::Result<()> {
Ok(PingResponse { // Логика для ПК: например, запуск системного процесса
value: payload.value, Ok(())
}) }
}
fn stop_vpn(&self) -> crate::Result<()> {
Ok(())
}
} }
+33 -25
View File
@@ -1,6 +1,7 @@
use std::ops::Deref;
use tauri::{ use tauri::{
plugin::{Builder, TauriPlugin}, plugin::{Builder, TauriPlugin},
Manager, Runtime, Manager, Runtime,
}; };
pub use models::*; pub use models::*;
@@ -16,33 +17,40 @@ mod models;
pub use error::{Error, Result}; pub use error::{Error, Result};
#[cfg(desktop)] // В lib.rs
use desktop::Vpn; pub trait VpnInterface<R: Runtime>: Send + Sync + 'static {
#[cfg(mobile)] fn start_vpn(&self, config: TunnelConfig, ip: String, port: i32) -> crate::Result<()>;
use mobile::Vpn; fn stop_vpn(&self) -> crate::Result<()>;
}
pub struct Vpn<R: Runtime>(Box<dyn VpnInterface<R>>);
/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the vpn APIs. impl<R: Runtime> Vpn<R> {
pub trait VpnExt<R: Runtime> { pub fn new<I: VpnInterface<R>>(inner: I) -> Self {
fn vpn(&self) -> &Vpn<R>; Self(Box::new(inner))
}
} }
impl<R: Runtime, T: Manager<R>> crate::VpnExt<R> for T { impl<R: Runtime> Deref for Vpn<R> {
fn vpn(&self) -> &Vpn<R> { type Target = dyn VpnInterface<R>;
self.state::<Vpn<R>>().inner()
} fn deref(&self) -> &Self::Target {
&*self.0
}
} }
/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R> { pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("vpn") Builder::new("vpn")
.invoke_handler(tauri::generate_handler![commands::ping]) .invoke_handler(tauri::generate_handler![
.setup(|app, api| { commands::start_vpn,
#[cfg(mobile)] commands::stop_vpn
let vpn = mobile::init(app, api)?; ])
#[cfg(desktop)] .setup(|app, api| {
let vpn = desktop::init(app, api)?; #[cfg(mobile)]
app.manage(vpn); let vpn_impl = mobile::init(app, api)?;
Ok(()) #[cfg(desktop)]
}) let vpn_impl = desktop::init(app, api)?;
.build() app.manage(Vpn::new(vpn_impl));
Ok(())
})
.build()
} }
+28 -19
View File
@@ -1,34 +1,43 @@
use crate::models::*;
use crate::VpnInterface;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use tauri::{ use tauri::{
plugin::{PluginApi, PluginHandle}, plugin::{PluginApi, PluginHandle},
AppHandle, Runtime, AppHandle, Runtime,
}; };
use crate::models::*; #[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.netrunner.vpn";
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_vpn); tauri::ios_plugin_binding!(init_plugin_vpn);
// initializes the Kotlin or Swift plugin classes
pub fn init<R: Runtime, C: DeserializeOwned>( pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>, _app: &AppHandle<R>,
api: PluginApi<R, C>, api: PluginApi<R, C>,
) -> crate::Result<Vpn<R>> { ) -> crate::Result<Vpn<R>> {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
let handle = api.register_android_plugin("", "ExamplePlugin")?; let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "VpnPlugin")?;
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_vpn)?; let handle = api.register_ios_plugin(init_plugin_vpn)?;
Ok(Vpn(handle)) Ok(Vpn(handle))
} }
/// Access to the vpn APIs.
pub struct Vpn<R: Runtime>(PluginHandle<R>); pub struct Vpn<R: Runtime>(PluginHandle<R>);
impl<R: Runtime> Vpn<R> { impl<R: Runtime> VpnInterface<R> for Vpn<R> {
pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> { fn start_vpn(&self, config: TunnelConfig, ip: String, port: i32) -> crate::Result<()> {
self self.send("startVpn", StartVpnRequest { config, ip, port })
.0 }
.run_mobile_plugin("ping", payload)
.map_err(Into::into) fn stop_vpn(&self) -> crate::Result<()> {
} self.send("stopVpn", StopVpnRequest {})
}
}
// Приватный метод для уменьшения дублирования кода
impl<R: Runtime> Vpn<R> {
fn send<S: serde::Serialize>(&self, cmd: &str, payload: S) -> crate::Result<()> {
self.0.run_mobile_plugin(cmd, payload).map_err(Into::into)
}
} }
+15 -7
View File
@@ -1,13 +1,21 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TunnelConfig {
pub address: String,
pub prefix: i32, // Соответствует Int в Kotlin
pub dns: String,
pub mtu: i32,
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PingRequest { pub struct StartVpnRequest {
pub value: Option<String>, pub config: TunnelConfig,
pub ip: String,
pub port: i32,
} }
#[derive(Debug, Clone, Default, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] pub struct StopVpnRequest {}
pub struct PingResponse {
pub value: Option<String>,
}
File diff suppressed because one or more lines are too long
+17 -5
View File
@@ -4,26 +4,37 @@ import { Power } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { TunnelConfig, startVpn, stopVpn } from "vpn-plugin";
export function VpnControl() { export function VpnControl() {
const [status, setStatus] = useState<"idle" | "connecting" | "connected">( const [status, setStatus] = useState<"idle" | "connecting" | "connected">(
"idle", "idle",
); );
const handleToggle = async () => { const handleToggle = async () => {
try { try {
if (status === "idle") { if (status === "idle") {
setStatus("connecting"); setStatus("connecting");
await invoke("start_vpn_android", {
remoteAddress: "62.60.244.156", // Подготавливаем конфигурацию (замените на нужные вам значения)
}); const config: TunnelConfig = {
address: "10.0.0.2",
prefix: 24,
dns: "8.8.8.8",
mtu: 1400,
};
// Вызываем вашу функцию-обертку
// ip и port берем из вашего примера
await startVpn(config, "62.60.244.156", 443);
setStatus("connected"); setStatus("connected");
} else { } else {
// Вызываем функцию для остановки
await stopVpn();
setStatus("idle"); setStatus("idle");
} }
} catch (error) { } catch (error) {
console.error("Ошибка VPN:", error); console.error("Ошибка управления VPN:", error);
setStatus("idle"); setStatus("idle");
} }
}; };
@@ -93,6 +104,7 @@ export function VpnControl() {
</svg> </svg>
<button <button
onClick={handleToggle} onClick={handleToggle}
disabled={status === "connecting"}
className={cn( className={cn(
"relative size-40 rounded-full transition-all duration-700 flex items-center justify-center", "relative size-40 rounded-full transition-all duration-700 flex items-center justify-center",
isActive isActive
+2 -1
View File
@@ -17,7 +17,8 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"],
"vpn-plugin": ["./src-tauri/src/plugins/vpn-plugin/guest-js"]
} }
}, },
"include": ["src"] "include": ["src"]
+4
View File
@@ -10,6 +10,10 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),
"vpn-plugin": path.resolve(
__dirname,
"./src-tauri/src/plugins/vpn-plugin/guest-js",
),
}, },
}, },
clearScreen: false, clearScreen: false,