# `tauri-plugin-vpn`

Кастомный Tauri-плагин — единственная точка входа к VPN-движку из фронтенда.
Сам движок (`netrunner_client`, UniFFI-крейт из приватного репозитория
`netrunner-core`) сюда не входит — плагин лишь адаптирует его под интерфейс
Tauri-команд и различия desktop/mobile.

## Слои

```
guest-js/index.ts   — JS API, которым пользуется фронтенд (invoke Tauri-команд)
src/commands.rs      — #[command]-обвязка (start_vpn, stop_vpn, ...)
src/lib.rs           — trait VpnInterface + выбор desktop/mobile реализации
src/desktop.rs        ├─ вызывает netrunner_client::SessionManager напрямую
src/mobile.rs          └─ шлёт команды в нативный Android/iOS плагин
src/models.rs        — общие DTO (TunnelConfig, StartVpnRequest, ...)
android/              — Kotlin-часть, см. android/README.md
```

## Rust-файлы

| Файл | Назначение |
|---|---|
| `src/lib.rs` | `trait VpnInterface<R>` — общий контракт (`start_vpn`, `stop_vpn`, `check_actual_vpn_status`, `get_installed_apps`, `launch_app`). `Vpn<R>` хранит `Box<dyn VpnInterface<R>>` и реализует `Deref` на него — благодаря этому `commands.rs` вызывает `vpn.start_vpn(...)`, не зная, десктопная это реализация или мобильная. `init()` в `setup()` выбирает `desktop::init` / `mobile::init` по `#[cfg(desktop)]`/`#[cfg(mobile)]`. |
| `src/commands.rs` | Tauri-команды. `start_vpn` держит `AtomicBool is_running` с `compare_exchange` — защита от повторного запуска сессии при повторном вызове (двойной клик, React double-invoke), а не мьютекс, т.к. проверка/установка должны быть одной атомарной операцией. |
| `src/desktop.rs` | Оборачивает `netrunner_client::SessionManager::spawn_session(remote_address, sni, tun_fd, cache_dir, killswitch, excluded_apps, excluded_domains)` — порядок аргументов **обязан** совпадать с `.udl`-описанием в `netrunner-core`. `tun_fd = None` (на десктопе TUN создаётся самим движком, fd не нужен). Эмитит `status-change`/`traffic-stats` через `AppHandle::emit`, статистика — в фоновом потоке с опросом раз в секунду. |
| `src/mobile.rs` | Тонкий мост: сериализует те же параметры в `StartVpnRequest` (camelCase) и шлёт через `PluginHandle::run_mobile_plugin` в Kotlin/Swift. |
| `src/models.rs` | DTO с `#[serde(rename_all = "camelCase")]` — формат обмена с JS/Kotlin-стороной. |

## `guest-js/index.ts`

Обёртка над `invoke("plugin:vpn|...")`. `startVpn(config, ip, port, sni)` —
`sni` обязателен и приходит от выбранного узла (`ServerNode.sni` во
фронтенде) — это decoy-домен, под который маскируется TLS ClientHello именно
для этого сервера; **не константа**, для каждого сервера свой.

## Android

См. [android/README.md](android/README.md) — там же задокументирован
известный источник поломки мобильной сборки (гитигнорённые UniFFI-биндинги).
