base working button with saved state
This commit is contained in:
@@ -8,11 +8,15 @@ rust-version = "1.77.2"
|
||||
exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
|
||||
links = "tauri-plugin-vpn"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.10.3" }
|
||||
serde = "1.0"
|
||||
tauri = { version = "2.0.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "2"
|
||||
netrunner-client = { git = "ssh://git@github.com/nineAp/netrunner-core.git", branch = "main" }
|
||||
serde_json = "1.0.149"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.5.4", features = ["build"] }
|
||||
|
||||
@@ -36,5 +36,7 @@ dependencies {
|
||||
implementation("net.java.dev.jna:jna:5.13.0@aar")
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||
implementation(project(":tauri-android"))
|
||||
}
|
||||
@@ -6,10 +6,13 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name=".TunVpnService"
|
||||
android:process=":vpn"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
|
||||
@@ -2,13 +2,16 @@ package app.netrunner.vpn
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.VpnService
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.Parcelable
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
import app.tauri.plugin.Plugin
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.Command
|
||||
@@ -23,6 +26,15 @@ import uniffi.netrunner_client.Session
|
||||
import com.fasterxml.jackson.annotation.JsonCreator
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import android.webkit.WebView
|
||||
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
private const val TAG = "VPN_DEBUG"
|
||||
|
||||
@Parcelize
|
||||
@@ -47,6 +59,16 @@ class TunVpnService : VpnService() {
|
||||
Log.d(TAG, "TunVpnService created")
|
||||
}
|
||||
|
||||
companion object {
|
||||
var isRunning: Boolean = false
|
||||
private set
|
||||
|
||||
// Метод для внешнего контроля (опционально)
|
||||
fun setStatus(running: Boolean) {
|
||||
isRunning = running
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
||||
Log.d(TAG, "onStartCommand called action=${intent?.action}")
|
||||
@@ -88,6 +110,17 @@ class TunVpnService : VpnService() {
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun sendStatusBroadcast(status: String) {
|
||||
val intent = Intent("com.netrunner.vpn.STATUS_UPDATE").apply {
|
||||
putExtra("status", status)
|
||||
// Указываем пакет нашего приложения, чтобы интент не улетел в другие аппки
|
||||
// и гарантированно дошел до нашего VpnPlugin
|
||||
setPackage(packageName)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
Log.d("VPN_DEBUG", ">>> [SERVICE] Broadcast sent: $status")
|
||||
}
|
||||
|
||||
private fun stopServiceProperly() {
|
||||
stopVpn()
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
@@ -98,9 +131,8 @@ class TunVpnService : VpnService() {
|
||||
}
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
|
||||
private fun startVpn(config: TunnelConfig, ip: String, port: Int) {
|
||||
|
||||
Log.d(TAG, "startVpn called")
|
||||
|
||||
val builder = Builder()
|
||||
@@ -109,16 +141,15 @@ class TunVpnService : VpnService() {
|
||||
.addRoute("0.0.0.0", 0)
|
||||
.setMtu(config.mtu)
|
||||
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
try {
|
||||
builder.addDisallowedApplication("com.netrunner.vpn")
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Can't exclude ${e.message}")
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
try {
|
||||
// Исключаем само приложение из проксирования, чтобы не было петли
|
||||
builder.addDisallowedApplication(packageName)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Can't exclude app: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "VPN builder configured")
|
||||
|
||||
|
||||
vpnInterface = builder.establish()
|
||||
|
||||
if (vpnInterface == null) {
|
||||
@@ -126,37 +157,40 @@ class TunVpnService : VpnService() {
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "VPN interface established")
|
||||
|
||||
val fd = vpnInterface?.detachFd() ?: run {
|
||||
Log.e(TAG, "detachFd failed")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "TUN fd=$fd")
|
||||
|
||||
try {
|
||||
|
||||
val manager = SessionManager()
|
||||
|
||||
val cachePath = cacheDir.absolutePath
|
||||
|
||||
Log.d(TAG, "Starting Rust Session with cachePath: $cachePath")
|
||||
|
||||
Log.d(TAG, "SessionManager created")
|
||||
|
||||
activeSession = manager.start("$ip:$port", fd)
|
||||
|
||||
Log.d(TAG, "Session started to $ip:$port")
|
||||
activeSession = manager.startMobile("$ip:$port", fd, cachePath)
|
||||
triggerVibration()
|
||||
sendStatusBroadcast("connected")
|
||||
isRunning = true
|
||||
Log.d(TAG, "Session started successfully")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Rust manager.startMobile failed", e)
|
||||
sendStatusBroadcast("idle")
|
||||
stopVpn()
|
||||
|
||||
Log.e(TAG, "manager.start failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopVpn() {
|
||||
|
||||
Log.d(TAG, "stopVpn called")
|
||||
|
||||
triggerVibration()
|
||||
sendStatusBroadcast("idle")
|
||||
activeSession?.stop()
|
||||
activeSession = null
|
||||
isRunning = false
|
||||
|
||||
try {
|
||||
vpnInterface?.close()
|
||||
@@ -171,35 +205,66 @@ class TunVpnService : VpnService() {
|
||||
|
||||
private fun startForegroundNotification() {
|
||||
val channelId = "vpn_service_channel"
|
||||
val channelName = "VPN Service"
|
||||
val channelName = "Netrunner VPN Service"
|
||||
val notificationManager = getSystemService(android.app.NotificationManager::class.java)
|
||||
|
||||
// Создаем канал для Android 8.0+
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
val channel = android.app.NotificationChannel(
|
||||
channelId, channelName, android.app.NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
channelId, channelName, android.app.NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
description = "Показывает статус работы VPN"
|
||||
setShowBadge(false)
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
// Создаем Intent, чтобы при клике на уведомление открывалось приложение
|
||||
val pendingIntent = android.app.PendingIntent.getActivity(
|
||||
this, 0,
|
||||
packageManager.getLaunchIntentForPackage(packageName),
|
||||
android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = android.app.Notification.Builder(this, channelId)
|
||||
.setContentTitle("Netrunner VPN")
|
||||
.setContentText("VPN активен")
|
||||
.setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) // Используй иконку из ресурсов
|
||||
.setOngoing(true) // Пользователь не может смахнуть уведомление
|
||||
.setContentText("Защита активна")
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info) // Стандартная системная иконка
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setCategory(android.app.Notification.CATEGORY_SERVICE)
|
||||
.build()
|
||||
|
||||
// Запуск Foreground с обязательным указанием типа для Android 14+
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(1, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(1, notification)
|
||||
try {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(1, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(1, notification)
|
||||
}
|
||||
Log.d(TAG, "Foreground notification started successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start foreground: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun triggerVibration() {
|
||||
val vibrator = getSystemService(android.content.Context.VIBRATOR_SERVICE) as android.os.Vibrator
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(android.os.VibrationEffect.createOneShot(500, android.os.VibrationEffect.EFFECT_HEAVY_CLICK))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrator.vibrate(500)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
|
||||
Log.d(TAG, "UI process killed (swiped), but VPN process should keep running.")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
isRunning = false
|
||||
Log.d(TAG, "TunVpnService destroyed")
|
||||
stopVpn()
|
||||
sendStatusBroadcast("idle")
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -211,12 +276,106 @@ class TunVpnService : VpnService() {
|
||||
@TauriPlugin
|
||||
class VpnPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
|
||||
init {
|
||||
Log.d(TAG, "VpnPlugin init")
|
||||
System.loadLibrary("netrunner_client")
|
||||
Log.d(TAG, "Rust library loaded")
|
||||
companion object {
|
||||
private var instance: VpnPlugin? = null
|
||||
|
||||
fun sendStatus(status: String) {
|
||||
val hasInstance = (instance != null)
|
||||
Log.d(TAG, ">>> [PLUGIN COMPANION] sendStatus called: $status | Instance exists: $hasInstance")
|
||||
if (hasInstance) {
|
||||
instance?.updateStatus(status)
|
||||
} else {
|
||||
Log.e(TAG, "CRITICAL: Cannot send status, VpnPlugin instance is NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val statusReceiver = object : android.content.BroadcastReceiver() {
|
||||
override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
|
||||
val status = intent?.getStringExtra("status") ?: return
|
||||
Log.d(TAG, ">>> [RECEIVER] Got status from Service: $status")
|
||||
instance?.updateStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerStatusReceiver() {
|
||||
val filter = IntentFilter("com.netrunner.vpn.STATUS_UPDATE")
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
activity.registerReceiver(statusReceiver, filter, android.content.Context.RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
activity.registerReceiver(statusReceiver, filter)
|
||||
}
|
||||
Log.d(TAG, "Status receiver registered successfully")
|
||||
}
|
||||
|
||||
override fun load(webView: WebView) {
|
||||
super.load(webView)
|
||||
instance = this
|
||||
Log.d(TAG, "VpnPlugin loaded with WebView and instance assigned")
|
||||
|
||||
try {
|
||||
System.loadLibrary("netrunner_client")
|
||||
Log.d(TAG, "Rust library loaded")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load Rust library: ${e.message}")
|
||||
}
|
||||
|
||||
requestNotificationPermission()
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
updateBlocklistIfNeeded()
|
||||
}
|
||||
|
||||
registerStatusReceiver()
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun updateBlocklistIfNeeded() {
|
||||
try {
|
||||
val cachePath = activity.cacheDir.absolutePath
|
||||
val hostsFile = File(cachePath, "hosts_cache.txt")
|
||||
|
||||
val isFresh = hostsFile.exists() &&
|
||||
(System.currentTimeMillis() - hostsFile.lastModified() < 24 * 60 * 60 * 1000)
|
||||
|
||||
if (isFresh) {
|
||||
Log.d(TAG, "DNS: Blocklist is fresh, skipping download")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "DNS: Starting blocklist download via Kotlin...")
|
||||
|
||||
// Качаем
|
||||
val url = URL("https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts")
|
||||
val connection = url.openConnection()
|
||||
connection.connectTimeout = 15000
|
||||
connection.readTimeout = 15000
|
||||
|
||||
val bytes = connection.getInputStream().use { it.readBytes() }
|
||||
|
||||
// Сохраняем
|
||||
hostsFile.writeBytes(bytes)
|
||||
|
||||
Log.d(TAG, "DNS: Blocklist updated! Size: ${bytes.size} bytes")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "DNS: Failed to download blocklist: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestNotificationPermission() {
|
||||
if (android.os.Build.VERSION.SDK_INT >= 33) {
|
||||
val permission = "android.permission.POST_NOTIFICATIONS"
|
||||
if (androidx.core.content.ContextCompat.checkSelfPermission(activity, permission) !=
|
||||
android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
androidx.core.app.ActivityCompat.requestPermissions(activity, arrayOf(permission), 101)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class VpnArgs {
|
||||
var config: TunnelConfig? = null
|
||||
@@ -285,6 +444,22 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
activity.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStatus(status: String) {
|
||||
val data = JSObject()
|
||||
data.put("status", status)
|
||||
|
||||
Log.d(TAG, ">>> [PLUGIN INSTANCE] trigger() called with event: status-change, data: $status")
|
||||
|
||||
try {
|
||||
// Пробуем вызвать trigger. В Tauri 2.0 он должен прокинуть это в JS
|
||||
trigger("status-change", data)
|
||||
Log.d(TAG, ">>> [PLUGIN INSTANCE] trigger() executed successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "!!! [PLUGIN INSTANCE] trigger() FAILED: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
@ActivityCallback
|
||||
fun onVpnPermissionResult(invoke: Invoke, result: ActivityResult) {
|
||||
|
||||
@@ -324,4 +499,14 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@Command
|
||||
fun checkActualVpnStatus(invoke: Invoke) {
|
||||
val status = if (TunVpnService.isRunning) "connected" else "idle"
|
||||
|
||||
val res = JSObject()
|
||||
res.put("status", status)
|
||||
|
||||
invoke.resolve(res)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
// src-tauri/src/plugins/vpn-plugin/build.rs
|
||||
|
||||
const COMMANDS: &[&str] = &["start_tunnel", "stop_tunnel"];
|
||||
const COMMANDS: &[&str] = &[
|
||||
"start_vpn",
|
||||
"stop_vpn",
|
||||
"check_actual_vpn_status",
|
||||
"registerListener",
|
||||
"unregisterListener",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import { addPluginListener, invoke } from "@tauri-apps/api/core";
|
||||
export interface TunnelConfig {
|
||||
address: string;
|
||||
prefix: number;
|
||||
@@ -18,3 +17,24 @@ export async function startVpn(config: TunnelConfig, ip: string, port: number) {
|
||||
export async function stopVpn() {
|
||||
return await invoke("plugin:vpn|stop_vpn");
|
||||
}
|
||||
|
||||
export async function checkActualVpnStatus(): Promise<"connected" | "idle"> {
|
||||
try {
|
||||
console.log("Actual vpn status called");
|
||||
const result = await invoke<{ status: string }>(
|
||||
"plugin:vpn|check_actual_vpn_status",
|
||||
);
|
||||
console.log(result);
|
||||
return result.status as "connected" | "idle";
|
||||
} catch (e) {
|
||||
console.error("Failed to check VPN status", e);
|
||||
return "idle";
|
||||
}
|
||||
}
|
||||
|
||||
export const setupVpnStatusListener = async (callback: (p: any) => void) => {
|
||||
return await addPluginListener("vpn", "status-change", (payload) => {
|
||||
console.log(">>> [SUCCESS] Пришло через мобильный листенер:", payload);
|
||||
callback(payload);
|
||||
});
|
||||
};
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-check-actual-vpn-status"
|
||||
description = "Enables the check_actual_vpn_status command without any pre-configured scope."
|
||||
commands.allow = ["check_actual_vpn_status"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-check-actual-vpn-status"
|
||||
description = "Denies the check_actual_vpn_status command without any pre-configured scope."
|
||||
commands.deny = ["check_actual_vpn_status"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-registerListener"
|
||||
description = "Enables the registerListener command without any pre-configured scope."
|
||||
commands.allow = ["registerListener"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-registerListener"
|
||||
description = "Denies the registerListener command without any pre-configured scope."
|
||||
commands.deny = ["registerListener"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# 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-start-vpn"
|
||||
description = "Enables the start_vpn command without any pre-configured scope."
|
||||
commands.allow = ["start_vpn"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-start-vpn"
|
||||
description = "Denies the start_vpn command without any pre-configured scope."
|
||||
commands.deny = ["start_vpn"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# 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"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-stop-vpn"
|
||||
description = "Enables the stop_vpn command without any pre-configured scope."
|
||||
commands.allow = ["stop_vpn"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-stop-vpn"
|
||||
description = "Denies the stop_vpn command without any pre-configured scope."
|
||||
commands.deny = ["stop_vpn"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-unregisterListener"
|
||||
description = "Enables the unregisterListener command without any pre-configured scope."
|
||||
commands.allow = ["unregisterListener"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-unregisterListener"
|
||||
description = "Denies the unregisterListener command without any pre-configured scope."
|
||||
commands.deny = ["unregisterListener"]
|
||||
@@ -10,12 +10,12 @@
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`vpn:allow-start-tunnel`
|
||||
`vpn:allow-check-actual-vpn-status`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the start_tunnel command without any pre-configured scope.
|
||||
Enables the check_actual_vpn_status command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@@ -23,12 +23,12 @@ Enables the start_tunnel command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`vpn:deny-start-tunnel`
|
||||
`vpn:deny-check-actual-vpn-status`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the start_tunnel command without any pre-configured scope.
|
||||
Denies the check_actual_vpn_status command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@@ -36,12 +36,12 @@ Denies the start_tunnel command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`vpn:allow-stop-tunnel`
|
||||
`vpn:allow-registerListener`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the stop_tunnel command without any pre-configured scope.
|
||||
Enables the registerListener command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@@ -49,12 +49,90 @@ Enables the stop_tunnel command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`vpn:deny-stop-tunnel`
|
||||
`vpn:deny-registerListener`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the stop_tunnel command without any pre-configured scope.
|
||||
Denies the registerListener command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`vpn:allow-start-vpn`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the start_vpn command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`vpn:deny-start-vpn`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the start_vpn command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`vpn:allow-stop-vpn`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the stop_vpn command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`vpn:deny-stop-vpn`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the stop_vpn command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`vpn:allow-unregisterListener`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the unregisterListener command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`vpn:deny-unregisterListener`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the unregisterListener command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -3,4 +3,7 @@ identifier = "allow-vpn-control"
|
||||
description = "Allows Vpn Control"
|
||||
|
||||
[permission.commands]
|
||||
allow = ["start_vpn", "stop_vpn"]
|
||||
allow = ["start_vpn", "stop_vpn", "check_actual_vpn_status", "registerListener", "unregisterListener"]
|
||||
|
||||
[permission.events]
|
||||
allow = ["status-change"]
|
||||
@@ -295,28 +295,64 @@
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the start_tunnel command without any pre-configured scope.",
|
||||
"description": "Enables the check_actual_vpn_status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-start-tunnel",
|
||||
"markdownDescription": "Enables the start_tunnel command without any pre-configured scope."
|
||||
"const": "allow-check-actual-vpn-status",
|
||||
"markdownDescription": "Enables the check_actual_vpn_status command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the start_tunnel command without any pre-configured scope.",
|
||||
"description": "Denies the check_actual_vpn_status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-start-tunnel",
|
||||
"markdownDescription": "Denies the start_tunnel command without any pre-configured scope."
|
||||
"const": "deny-check-actual-vpn-status",
|
||||
"markdownDescription": "Denies the check_actual_vpn_status command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the stop_tunnel command without any pre-configured scope.",
|
||||
"description": "Enables the registerListener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-stop-tunnel",
|
||||
"markdownDescription": "Enables the stop_tunnel command without any pre-configured scope."
|
||||
"const": "allow-registerListener",
|
||||
"markdownDescription": "Enables the registerListener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the stop_tunnel command without any pre-configured scope.",
|
||||
"description": "Denies the registerListener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-stop-tunnel",
|
||||
"markdownDescription": "Denies the stop_tunnel command without any pre-configured scope."
|
||||
"const": "deny-registerListener",
|
||||
"markdownDescription": "Denies the registerListener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the start_vpn command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-start-vpn",
|
||||
"markdownDescription": "Enables the start_vpn command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the start_vpn command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-start-vpn",
|
||||
"markdownDescription": "Denies the start_vpn command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the stop_vpn command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-stop-vpn",
|
||||
"markdownDescription": "Enables the stop_vpn command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the stop_vpn command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-stop-vpn",
|
||||
"markdownDescription": "Denies the stop_vpn command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the unregisterListener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-unregisterListener",
|
||||
"markdownDescription": "Enables the unregisterListener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the unregisterListener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-unregisterListener",
|
||||
"markdownDescription": "Denies the unregisterListener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Allows Vpn Control",
|
||||
|
||||
@@ -47,3 +47,14 @@ pub(crate) fn stop_vpn<R: Runtime>(app: AppHandle<R>) -> crate::Result<()> {
|
||||
eprintln!("--- [DEBUG] STOPPING VPN TUNNEL ---");
|
||||
result
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) fn check_actual_vpn_status<R: tauri::Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
) -> crate::Result<StatusResponse> {
|
||||
// StatusResponse должен быть в твоих моделях
|
||||
let vpn = app.state::<Vpn<R>>();
|
||||
|
||||
// Вызываем метод из vpn_impl (в mobile это уйдет в Kotlin)
|
||||
vpn.check_actual_vpn_status()
|
||||
}
|
||||
|
||||
@@ -41,4 +41,16 @@ impl<R: Runtime> VpnInterface<R> for Vpn<R> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_actual_vpn_status(&self) -> crate::Result<StatusResponse> {
|
||||
let lock = self.active_session.lock().unwrap();
|
||||
|
||||
let status = if lock.is_some() {
|
||||
"connected".to_string()
|
||||
} else {
|
||||
"idle".to_string()
|
||||
};
|
||||
|
||||
Ok(StatusResponse { status })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use std::{ops::Deref, sync::atomic::AtomicBool};
|
||||
use std::{
|
||||
ops::Deref,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Manager, Runtime,
|
||||
@@ -16,11 +20,11 @@ mod error;
|
||||
mod models;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
// В lib.rs
|
||||
pub trait VpnInterface<R: Runtime>: Send + Sync + 'static {
|
||||
fn start_vpn(&self, config: TunnelConfig, ip: String, port: i32) -> crate::Result<()>;
|
||||
fn stop_vpn(&self) -> crate::Result<()>;
|
||||
fn check_actual_vpn_status(&self) -> crate::Result<StatusResponse>;
|
||||
}
|
||||
pub struct Vpn<R: Runtime> {
|
||||
inner: Box<dyn VpnInterface<R>>,
|
||||
@@ -48,14 +52,17 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("vpn")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::start_vpn,
|
||||
commands::stop_vpn
|
||||
commands::stop_vpn,
|
||||
commands::check_actual_vpn_status
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
let vpn_impl = mobile::init(app, api)?;
|
||||
#[cfg(desktop)]
|
||||
let vpn_impl = desktop::init(app, api)?;
|
||||
app.manage(Vpn::new(vpn_impl));
|
||||
|
||||
let vpn_instance = Vpn::new(vpn_impl);
|
||||
app.manage(vpn_instance);
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
|
||||
@@ -33,9 +33,29 @@ impl<R: Runtime> VpnInterface<R> for Vpn<R> {
|
||||
fn stop_vpn(&self) -> crate::Result<()> {
|
||||
self.send("stopVpn", StopVpnRequest {})
|
||||
}
|
||||
|
||||
fn check_actual_vpn_status(&self) -> crate::Result<StatusResponse> {
|
||||
println!(">>> [RUST PLUGIN] Calling Kotlin: checkActualVpnStatus");
|
||||
self.0
|
||||
.run_mobile_plugin::<StatusResponse>("checkActualVpnStatus", ())
|
||||
.map(|res| {
|
||||
println!(">>> [RUST PLUGIN] Success from Kotlin: {:?}", res);
|
||||
res
|
||||
})
|
||||
.map_err(|e| {
|
||||
println!(">>> [RUST PLUGIN] Critical Error calling Kotlin: {:?}", e);
|
||||
let err_msg = format!("{:?}", e);
|
||||
if err_msg.contains("CommandNotFound") {
|
||||
println!(
|
||||
"!!! [RUST PLUGIN] ДИАГНОЗ: Метод checkActualVpnStatus не найден в Kotlin."
|
||||
);
|
||||
}
|
||||
|
||||
crate::Error::from(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Приватный метод для уменьшения дублирования кода
|
||||
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)
|
||||
|
||||
@@ -19,3 +19,9 @@ pub struct StartVpnRequest {
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct StopVpnRequest {}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StatusResponse {
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user