9.7 KiB
9.7 KiB
Pipeline реализации Flutter Desktop VPN (Windows + Linux) — исправленный
Ниже production-friendly пайплайн для desktop-приложения на Flutter с запуском sing-box.
0) Что исправлено относительно исходного варианта
- Запуск процесса исправлен: используется путь к бинарнику, а не команда
runкак исполняемый файл. - Для Linux
pkexecвызывается корректно:pkexec <binary> run -c <config>. - Убраны невалидные части Dart-кода (
killPort) и добавлены недостающие импорты. - Добавлен шаг извлечения бинарника из assets в writable-директорию и
chmod +xна Linux. - Исправлена модель прав: вместо небезопасного
.pklaсResultAny=yes— узкое polkit rule. - Уточнен runtime lifecycle (tray, hide-on-close, graceful stop, проверка exit code).
1) Инициализация проекта
flutter create singbox_vpn --platforms=windows,linux
cd singbox_vpn
pubspec.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) Скачивание и подготовка бинарников
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) Структура проекта
lib/
├── main.dart
├── services/
│ ├── singbox_runtime.dart # извлечение бинарника + пути
│ ├── singbox_service.dart # запуск/остановка процесса
│ └── tray_service.dart
├── providers/
│ └── vpn_provider.dart
└── screens/
└── home_screen.dart
4) Runtime: извлечение бинарника из assets
На старте приложения:
- Определить runtime-директорию:
getApplicationSupportDirectory(). - Скопировать туда бинарник из assets, если отсутствует или версия изменилась.
- На Linux выставить
chmod 755. - Использовать только этот абсолютный путь в
Process.start.
Минимальный пример (lib/services/singbox_runtime.dart):
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
class SingBoxRuntime {
Future<String> 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 (исправленный)
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class SingBoxService {
Process? _process;
final _logController = StreamController<String>.broadcast();
Stream<String> get logs => _logController.stream;
bool get isRunning => _process != null;
Future<bool> 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<String> 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<void> 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<void> 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:
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) Сборка
# Linux
flutter build linux --release
# Windows
flutter build windows --release
Не делай ручной cp после каждой сборки. Вместо этого:
- либо оставь бинарник только в assets и извлекай при первом запуске (предпочтительно);
- либо добавь копирование в platform build scripts (
linux/CMakeLists.txt,windows/CMakeLists.txt).
9) Тестирование (smoke + lifecycle)
flutter run -d linux
flutter run -d windows
Проверка:
- Старт VPN запускает
sing-boxи пишет логи в UI. - Linux: появляется polkit prompt при старте.
- Закрытие окна прячет приложение в tray, процесс жив.
Stopзавершаетsing-box, повторныйStartработает.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) Следующий шаг
- Внедрить
SingBoxRuntime.ensureBinary()и запуск только через абсолютный путь. - Добавить lifecycle tray/window и graceful shutdown.
- Проверить Linux polkit rule и выбранную Windows elevation-стратегию.