# Pipeline реализации Flutter Desktop VPN (Windows + Linux) — исправленный Ниже production-friendly пайплайн для desktop-приложения на Flutter с запуском `sing-box`. ## 0) Что исправлено относительно исходного варианта - Запуск процесса исправлен: используется путь к бинарнику, а не команда `run` как исполняемый файл. - Для Linux `pkexec` вызывается корректно: `pkexec run -c `. - Убраны невалидные части Dart-кода (`killPort`) и добавлены недостающие импорты. - Добавлен шаг извлечения бинарника из assets в writable-директорию и `chmod +x` на Linux. - Исправлена модель прав: вместо небезопасного `.pkla` с `ResultAny=yes` — узкое polkit rule. - Уточнен runtime lifecycle (tray, hide-on-close, graceful stop, проверка exit code). --- ## 1) Инициализация проекта ```bash flutter create singbox_vpn --platforms=windows,linux cd singbox_vpn ``` `pubspec.yaml`: ```yaml name: singbox_vpn dependencies: flutter: sdk: flutter flutter_riverpod: ^2.5.1 tray_manager: ^0.2.1 window_manager: ^0.3.8 path_provider: ^2.1.3 flutter: assets: - assets/sing-box/linux-amd64/sing-box - assets/sing-box/windows-amd64/sing-box.exe ``` Примечания: - `permission_handler` для desktop здесь обычно не нужен. - Фиксируй версию `sing-box` явно, без `latest` в URL. --- ## 2) Скачивание и подготовка бинарников ```bash mkdir -p assets/sing-box/linux-amd64 assets/sing-box/windows-amd64 # Linux x64 wget -O /tmp/sing-box-linux-amd64.tar.gz \ https://github.com/SagerNet/sing-box/releases/download/v1.9.3/sing-box-1.9.3-linux-amd64.tar.gz tar -xzf /tmp/sing-box-linux-amd64.tar.gz -C /tmp cp /tmp/sing-box-1.9.3-linux-amd64/sing-box assets/sing-box/linux-amd64/sing-box chmod +x assets/sing-box/linux-amd64/sing-box # Windows x64 wget -O /tmp/sing-box-windows-amd64.zip \ https://github.com/SagerNet/sing-box/releases/download/v1.9.3/sing-box-1.9.3-windows-amd64.zip unzip -o /tmp/sing-box-windows-amd64.zip -d /tmp/sing-box-win cp /tmp/sing-box-win/sing-box-1.9.3-windows-amd64/sing-box.exe assets/sing-box/windows-amd64/sing-box.exe ``` Рекомендуется: - проверять SHA256 перед копированием в `assets`; - хранить версии и checksum в отдельном `tool/versions.json`. --- ## 3) Структура проекта ```text lib/ ├── main.dart ├── services/ │ ├── singbox_runtime.dart # извлечение бинарника + пути │ ├── singbox_service.dart # запуск/остановка процесса │ └── tray_service.dart ├── providers/ │ └── vpn_provider.dart └── screens/ └── home_screen.dart ``` --- ## 4) Runtime: извлечение бинарника из assets На старте приложения: 1. Определить runtime-директорию: `getApplicationSupportDirectory()`. 2. Скопировать туда бинарник из assets, если отсутствует или версия изменилась. 3. На Linux выставить `chmod 755`. 4. Использовать только этот абсолютный путь в `Process.start`. Минимальный пример (`lib/services/singbox_runtime.dart`): ```dart import 'dart:io'; import 'package:flutter/services.dart'; import 'package:path_provider/path_provider.dart'; class SingBoxRuntime { Future ensureBinary() async { final dir = await getApplicationSupportDirectory(); final fileName = Platform.isWindows ? 'sing-box.exe' : 'sing-box'; final outPath = '${dir.path}${Platform.pathSeparator}$fileName'; final outFile = File(outPath); final assetPath = Platform.isWindows ? 'assets/sing-box/windows-amd64/sing-box.exe' : 'assets/sing-box/linux-amd64/sing-box'; final bytes = await rootBundle.load(assetPath); await outFile.writeAsBytes(bytes.buffer.asUint8List(), flush: true); if (Platform.isLinux) { await Process.run('chmod', ['755', outPath]); } return outPath; } } ``` --- ## 5) Сервис запуска `sing-box` (исправленный) ```dart import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:path_provider/path_provider.dart'; class SingBoxService { Process? _process; final _logController = StreamController.broadcast(); Stream get logs => _logController.stream; bool get isRunning => _process != null; Future start({ required String binaryPath, required String configJson, }) async { if (_process != null) return true; final appDir = await getApplicationSupportDirectory(); final configPath = '${appDir.path}${Platform.pathSeparator}config.json'; await File(configPath).writeAsString(configJson, flush: true); List command; if (Platform.isLinux) { command = ['pkexec', binaryPath, 'run', '-c', configPath]; } else if (Platform.isWindows) { command = [binaryPath, 'run', '-c', configPath]; } else { throw UnsupportedError('Only Windows and Linux are supported'); } try { _process = await Process.start(command.first, command.sublist(1)); _process!.stdout .transform(utf8.decoder) .listen((data) => _logController.add(data)); _process!.stderr .transform(utf8.decoder) .listen((data) => _logController.add('ERROR: $data')); _process!.exitCode.then((code) { _logController.add('sing-box exited with code $code'); _process = null; }); return true; } catch (e) { _logController.add('Start failed: $e'); _process = null; return false; } } Future stop() async { final p = _process; if (p == null) return; p.kill(ProcessSignal.sigterm); await p.exitCode.timeout(const Duration(seconds: 3), onTimeout: () { p.kill(ProcessSignal.sigkill); return -1; }); _process = null; } Future dispose() async { await stop(); await _logController.close(); } } ``` Важно для Windows elevation: - `Process.start` не поднимет права автоматически. - Нужна отдельная стратегия: `requireAdministrator` в `runner.exe.manifest` или сервис/launcher. --- ## 6) `main.dart`: tray + window lifecycle Минимум: - `import 'dart:io';` - инициализация `window_manager` только на desktop; - `setPreventClose(true)` + обработка close в hide-to-tray; - действия tray (`show`, `hide`, `quit`) с корректным завершением VPN перед `exit`. --- ## 7) Linux Polkit (безопаснее) Не используй широкое правило `ResultAny=yes` для всех команд. Создай правило `/etc/polkit-1/rules.d/10-singbox.rules`: ```javascript polkit.addRule(function(action, subject) { if (action.id == "org.freedesktop.policykit.exec" && action.lookup("program") == "/opt/singbox-vpn/sing-box" && subject.isInGroup("singbox")) { return polkit.Result.AUTH_ADMIN; } }); ``` Примечания: - добавь пользователя в группу `singbox`; - используй фиксированный путь к бинарнику в установленном приложении. --- ## 8) Сборка ```bash # Linux flutter build linux --release # Windows flutter build windows --release ``` Не делай ручной `cp` после каждой сборки. Вместо этого: - либо оставь бинарник только в assets и извлекай при первом запуске (предпочтительно); - либо добавь копирование в platform build scripts (`linux/CMakeLists.txt`, `windows/CMakeLists.txt`). --- ## 9) Тестирование (smoke + lifecycle) ```bash flutter run -d linux flutter run -d windows ``` Проверка: 1. Старт VPN запускает `sing-box` и пишет логи в UI. 2. Linux: появляется polkit prompt при старте. 3. Закрытие окна прячет приложение в tray, процесс жив. 4. `Stop` завершает `sing-box`, повторный `Start` работает. 5. `Quit` из tray останавливает процесс и завершает приложение. --- ## 10) Дистрибутивы ### Linux (AppImage) - Собери корректный AppDir (desktop file, icon, executable). - Убедись, что `sing-box` либо в assets + runtime extraction, либо в ожидаемом пути для polkit-правила. ### Windows (NSIS) - Добавь uninstall, ярлыки, версию, подпись и UAC-логику. - Если нужен запуск с elevated правами всегда — зафиксируй это в манифесте/инсталляторе. --- ## 11) Реалистичная оценка - PoC: 4-8 часов. - Стабильный desktop release: 2-5 дней (elevation, упаковка, smoke/regression). --- ## 12) Следующий шаг 1. Внедрить `SingBoxRuntime.ensureBinary()` и запуск только через абсолютный путь. 2. Добавить lifecycle tray/window и graceful shutdown. 3. Проверить Linux polkit rule и выбранную Windows elevation-стратегию.