Add decoy SNI plumbing, bundle wintun.dll, fix flag icons, add docs

- Прокидываем sni (decoy-хост) по обновлённому UDL через весь стек:
  guest-js -> Tauri-команда -> desktop/mobile -> Kotlin spawnSession.
  Список серверов пока статичный (backend еще не отдает sni), но логика
  похода в реальный /api/v1/nodes оставлена под флагом на будущее.
- Бандлим wintun.dll в Windows-инсталлятор через bundle.resources в
  tauri.windows.conf.json - раньше он копировался только для
  локального cargo run/tauri dev, но не попадал в NSIS/MSI.
- Чиним падение мобильной сборки: гитигнорённые UniFFI Kotlin-биндинги
  отстали от обновленного UDL (не хватало параметра sni).
- Флаги стран - локальные SVG вместо react-country-flag в CDN-режиме:
  тот бился об CSP и не грузился офлайн.
- Добавляем дерево README.md по структуре проекта и точечные
  комментарии в неочевидных местах кода.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kirill
2026-07-02 19:29:29 +07:00
parent 16760c16ea
commit c852315d34
27 changed files with 464 additions and 57 deletions
+23 -5
View File
@@ -10,6 +10,17 @@ import ReactCountryFlag from "react-country-flag";
import { cn } from "@/lib/utils";
import { useNodesStore } from "@/store/useNodesStore";
import { useVpnStore } from "@/store/useVpnStore";
import flagNl from "@/assets/flags/nl.svg";
import flagSg from "@/assets/flags/sg.svg";
// react-country-flag в режиме svg тянет иконки с внешнего CDN — это бьётся об
// CSP (img-src) и вообще не грузится без сети. Для известных статических
// узлов используем забандленные локальные SVG, а для остальных кодов —
// эмодзи-режим той же либы (без сети, просто текстовый глиф).
const LOCAL_FLAGS: Record<string, string> = {
nl: flagNl,
sg: flagSg,
};
export function CountrySelect() {
const { t } = useTranslation();
@@ -57,11 +68,18 @@ export function CountrySelect() {
className="cursor-pointer hover:bg-white/10 focus:bg-white/20 transition-colors"
>
<div className="flex items-center gap-3">
<ReactCountryFlag
countryCode={node.countryCode.toUpperCase()}
svg
className="size-5 rounded-sm"
/>
{LOCAL_FLAGS[node.countryCode.toLowerCase()] ? (
<img
src={LOCAL_FLAGS[node.countryCode.toLowerCase()]}
alt=""
className="size-5 rounded-sm object-cover"
/>
) : (
<ReactCountryFlag
countryCode={node.countryCode.toUpperCase()}
className="size-5 rounded-sm"
/>
)}
<span className="font-medium text-foreground">{node.name}</span>
<span className="ml-auto text-xs text-muted-foreground">
{node.currentLoad}%
+15
View File
@@ -0,0 +1,15 @@
# `src/features/`
Крупные самостоятельные UI-блоки (в отличие от `components/ui` — не
переиспользуемые примитивы, а конкретные фичи приложения).
| Файл | Назначение |
|---|---|
| `VpnControl.tsx` | Главная кнопка подключения. Берёт выбранный узел из `useNodesStore`, собирает `TunnelConfig` из `useSettingsStore` и вызывает `startVpn(config, ip, port, sni)` из `vpn-plugin`. Слушает Tauri-event `status-change`. |
| `CountrySelect.tsx` | Выпадающий список серверов (`useNodesStore`). Флаги — локальные SVG (`assets/flags`) для известных статических узлов, иначе эмодзи-фолбэк через `react-country-flag` (**намеренно без** `svg`-режима этой либы — он тянет иконки с внешнего CDN, что бьётся об CSP и не грузится офлайн). Заблокирован во время подключения. |
| `VpnStats.tsx` | RX/TX счётчики трафика из `useTrafficStore`, форматирование байт в человекочитаемый вид. |
| `Header.tsx` | Шапка: бургер-кнопка, лого, переключатели темы/языка. |
| `BurgerMenu.tsx` | Выезжающее боковое меню с навигацией (Home/Settings/Telegram/About). |
| `ThemeToggle.tsx` / `ToggleLanguge.tsx` | Точечные переключатели темы и языка (RU/EN), делегируют в `ThemeProvider`/`i18n`. |
| `SplashScreen.tsx` | Интро-заставка при старте (~5 сек), скрывается по таймеру. |
| `AppExclusions.tsx` | Диалог выбора приложений для исключения из VPN-туннеля. На Android дёргает `getInstalledApps()` из `vpn-plugin`, на остальных платформах — заглушка. Результат пишется в `useSettingsStore.excludedApps`. |
+1 -1
View File
@@ -38,7 +38,7 @@ export function VpnControl() {
excludedApps: excludedApps,
excludedDomains: excludedDomains,
};
await startVpn(config, node.ipAddress, node.port);
await startVpn(config, node.ipAddress, node.port, node.sni);
} else {
await stopVpn();
}