killswitch and domain list, also aexcluded apps
This commit is contained in:
@@ -1,21 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="app.netrunner.vpn">
|
||||
|
||||
<uses-permission android:name="android.permission.BIND_VPN_SERVICE" />
|
||||
<!-- Базовые сетевые разрешения -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.BIND_VPN_SERVICE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<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" />
|
||||
|
||||
<!-- Разрешение для уведомлений (Android 13+) -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!--
|
||||
Видимость пакетов для функции Split Tunneling.
|
||||
Позволяет PackageManager видеть приложения, имеющие иконку в меню (Launcher).
|
||||
-->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name=".TunVpnService"
|
||||
android:process=":vpn"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
android:foregroundServiceType="specialUse"> <!-- ИСПРАВЛЕНИЕ ЗДЕСЬ -->
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
@@ -23,6 +41,11 @@
|
||||
<meta-data
|
||||
android:name="android.net.VpnService.SUPPORTS_USER_CONTROL"
|
||||
android:value="true" />
|
||||
|
||||
<!-- Это свойство критично для specialUse -->
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_TYPE"
|
||||
android:value="vpn" />
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -13,6 +13,7 @@ import app.tauri.annotation.ActivityCallback
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.JSArray
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
@@ -38,7 +39,10 @@ data class TunnelConfig @JsonCreator constructor(
|
||||
@JsonProperty("address") val address: String,
|
||||
@JsonProperty("prefix") val prefix: Int,
|
||||
@JsonProperty("dns") val dns: String,
|
||||
@JsonProperty("mtu") val mtu: Int
|
||||
@JsonProperty("mtu") val mtu: Int,
|
||||
@JsonProperty("killswitchEnabled") val killswitchEnabled: Boolean,
|
||||
@JsonProperty("excludedApps") val excludedApps: List<String>,
|
||||
@JsonProperty("excludedDomains") val excludedDomains: List<String>
|
||||
) : Parcelable
|
||||
|
||||
class TunVpnService : VpnService() {
|
||||
@@ -76,6 +80,17 @@ class TunVpnService : VpnService() {
|
||||
private fun startVpn(config: TunnelConfig, ip: String, port: Int) {
|
||||
val builder = Builder().addAddress(config.address, config.prefix)
|
||||
.addDnsServer(config.dns).addRoute("0.0.0.0", 0).setMtu(config.mtu)
|
||||
|
||||
for (appPkg in config.excludedApps) {
|
||||
if (appPkg.isNotBlank()) {
|
||||
try {
|
||||
builder.addDisallowedApplication(appPkg.trim())
|
||||
Log.d(TAG, "Excluded app from VPN tunnel: $appPkg")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to exclude app $appPkg (possibly not installed)", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try { builder.addDisallowedApplication(packageName) } catch (e: Exception) { Log.e(TAG, "Exclude fail") }
|
||||
|
||||
@@ -84,11 +99,18 @@ class TunVpnService : VpnService() {
|
||||
|
||||
try {
|
||||
val manager = SessionManager()
|
||||
activeSession = manager.spawnSession("$ip:$port", fd, cacheDir.absolutePath)
|
||||
// Передаем параметры в ядро Rust
|
||||
activeSession = manager.spawnSession(
|
||||
"$ip:$port",
|
||||
fd,
|
||||
cacheDir.absolutePath,
|
||||
config.killswitchEnabled,
|
||||
config.excludedApps,
|
||||
config.excludedDomains
|
||||
)
|
||||
triggerVibration(true)
|
||||
sendStatusBroadcast("connected")
|
||||
|
||||
// ДОБАВЛЕНО: Запускаем цикл статистики при успешном коннекте
|
||||
startStatsLoop()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Rust fail", e)
|
||||
@@ -399,6 +421,42 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
val status = getHardwareVpnStatus()
|
||||
invoke.resolve(JSObject().apply { put("status", status) })
|
||||
}
|
||||
|
||||
@Command
|
||||
fun getInstalledApps(invoke: Invoke) {
|
||||
val pm = activity.packageManager
|
||||
// Используем флаг GET_META_DATA для более полной информации
|
||||
val packages = pm.getInstalledPackages(0)
|
||||
val jsArray = JSArray()
|
||||
|
||||
for (pkg in packages) {
|
||||
val info = pkg.applicationInfo ?: continue
|
||||
|
||||
// Условие 1: Это не наше собственное приложение
|
||||
if (pkg.packageName == activity.packageName) continue
|
||||
|
||||
// Условие 2: Это пользовательское приложение ИЛИ оно имеет иконку в лаунчере
|
||||
val isSystem = (info.flags and android.content.pm.ApplicationInfo.FLAG_SYSTEM) != 0
|
||||
val hasLauncher = pm.getLaunchIntentForPackage(pkg.packageName) != null
|
||||
|
||||
// Если это не системное приложение (установлено юзером) ИЛИ у него есть лаунчер
|
||||
if (!isSystem || hasLauncher) {
|
||||
val appName = info.loadLabel(pm).toString()
|
||||
|
||||
val obj = JSObject()
|
||||
obj.put("appName", appName)
|
||||
obj.put("packageName", pkg.packageName)
|
||||
|
||||
jsArray.put(obj)
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Found ${jsArray.length()} apps for exclusion list")
|
||||
|
||||
val result = JSObject()
|
||||
result.put("apps", jsArray)
|
||||
invoke.resolve(result)
|
||||
}
|
||||
|
||||
private fun updateStatus(status: String) {
|
||||
trigger("status-change", JSObject().apply { put("status", status) })
|
||||
|
||||
@@ -4,6 +4,9 @@ export interface TunnelConfig {
|
||||
prefix: number;
|
||||
dns: string;
|
||||
mtu: number;
|
||||
killswitchEnabled: boolean;
|
||||
excludedApps: string[];
|
||||
excludedDomains: string[];
|
||||
}
|
||||
|
||||
export interface TrafficStatsResponse {
|
||||
@@ -13,6 +16,11 @@ export interface TrafficStatsResponse {
|
||||
txPackets: number;
|
||||
}
|
||||
|
||||
export interface AppInfo {
|
||||
appName: string;
|
||||
packageName: string;
|
||||
}
|
||||
|
||||
export async function startVpn(config: TunnelConfig, ip: string, port: number) {
|
||||
return await invoke("plugin:vpn|start_vpn", {
|
||||
config,
|
||||
@@ -54,3 +62,8 @@ export const setupTrafficStatsListener = async (
|
||||
callback(payload as TrafficStatsResponse);
|
||||
});
|
||||
};
|
||||
|
||||
export async function getInstalledApps(): Promise<AppInfo[]> {
|
||||
// Rust теперь сам распаковывает "apps" и отдает нам готовый массив
|
||||
return await invoke<AppInfo[]>("plugin:vpn|get_installed_apps");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ identifier = "allow-vpn-control"
|
||||
description = "Allows Vpn Control"
|
||||
|
||||
[permission.commands]
|
||||
allow = ["start_vpn", "stop_vpn", "get_traffic_stats", "check_actual_vpn_status", "registerListener", "unregisterListener"]
|
||||
allow = ["start_vpn", "stop_vpn", "get_traffic_stats", "check_actual_vpn_status", "registerListener", "unregisterListener", "get_installed_apps"]
|
||||
|
||||
[permission.events]
|
||||
allow = ["status-change"]
|
||||
@@ -58,3 +58,8 @@ pub(crate) fn check_actual_vpn_status<R: tauri::Runtime>(
|
||||
// Вызываем метод из vpn_impl (в mobile это уйдет в Kotlin)
|
||||
vpn.check_actual_vpn_status()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) fn get_installed_apps<R: Runtime>(app: AppHandle<R>) -> crate::Result<Vec<AppInfo>> {
|
||||
app.state::<Vpn<R>>().get_installed_apps()
|
||||
}
|
||||
|
||||
@@ -47,9 +47,15 @@ impl<R: Runtime> VpnInterface<R> for Vpn<R> {
|
||||
},
|
||||
);
|
||||
|
||||
// Обрати внимание: SessionManager.spawn_session -> SessionManager::new().spawn_session
|
||||
let session =
|
||||
SessionManager::new().spawn_session(format!("{}:{}", ip, port), None, ".".into());
|
||||
// Передаем новые параметры конфига прямо в UniFFI-метод
|
||||
let session = SessionManager::new().spawn_session(
|
||||
format!("{}:{}", ip, port),
|
||||
None,
|
||||
".".into(),
|
||||
config.killswitch_enabled,
|
||||
config.excluded_apps.clone(),
|
||||
config.excluded_domains.clone(),
|
||||
);
|
||||
|
||||
// Сохраняем сессию
|
||||
{
|
||||
@@ -122,4 +128,8 @@ impl<R: Runtime> VpnInterface<R> for Vpn<R> {
|
||||
status: status.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_installed_apps(&self) -> crate::Result<Vec<AppInfo>> {
|
||||
Ok(vec![]) // На десктопе пока отдаем пустоту
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ 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>;
|
||||
fn get_installed_apps(&self) -> crate::Result<Vec<AppInfo>>;
|
||||
}
|
||||
pub struct Vpn<R: Runtime> {
|
||||
inner: Box<dyn VpnInterface<R>>,
|
||||
@@ -51,6 +52,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
commands::start_vpn,
|
||||
commands::stop_vpn,
|
||||
commands::check_actual_vpn_status,
|
||||
commands::get_installed_apps, // <--- НОВОЕ
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
|
||||
@@ -54,6 +54,22 @@ impl<R: Runtime> VpnInterface<R> for Vpn<R> {
|
||||
crate::Error::from(e)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_installed_apps(&self) -> crate::Result<Vec<AppInfo>> {
|
||||
// Создаем временную структуру для распаковки JSObject, который шлет Kotlin
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Wrapper {
|
||||
apps: Vec<AppInfo>,
|
||||
}
|
||||
|
||||
let wrapper: Wrapper = self
|
||||
.0
|
||||
.run_mobile_plugin("getInstalledApps", ())
|
||||
.map_err(crate::error::Error::from)?;
|
||||
|
||||
// Отдаем наружу (в React) чистый массив
|
||||
Ok(wrapper.apps)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Vpn<R> {
|
||||
|
||||
@@ -4,9 +4,13 @@ use serde::{Deserialize, Serialize};
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TunnelConfig {
|
||||
pub address: String,
|
||||
pub prefix: i32, // Соответствует Int в Kotlin
|
||||
pub prefix: i32,
|
||||
pub dns: String,
|
||||
pub mtu: i32,
|
||||
// Новые параметры
|
||||
pub killswitch_enabled: bool,
|
||||
pub excluded_apps: Vec<String>,
|
||||
pub excluded_domains: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
@@ -34,3 +38,10 @@ pub struct TrafficStatsResponse {
|
||||
pub rx_packets: u64,
|
||||
pub tx_packets: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppInfo {
|
||||
pub app_name: String,
|
||||
pub package_name: String,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user