diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml
new file mode 100644
index 0000000..140dc22
--- /dev/null
+++ b/.github/actions/setup-rust/action.yml
@@ -0,0 +1,22 @@
+name: Setup Rust
+description: >
+ Install the Rust toolchain with platform targets and a build cache, so
+ cargokit can compile the kolibri native library during the Flutter build.
+
+inputs:
+ targets:
+ description: Comma-separated rustup targets for the platform being built.
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: ${{ inputs.targets }}
+
+ - name: Cache Rust build
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: third_party/kolibri/kolibri-dart/rust
diff --git a/.github/workflows/build-android-fcm.yml b/.github/workflows/build-android-fcm.yml
index 015969d..3fef1cb 100644
--- a/.github/workflows/build-android-fcm.yml
+++ b/.github/workflows/build-android-fcm.yml
@@ -25,6 +25,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
@@ -39,6 +41,11 @@ jobs:
channel: 'stable'
cache: true
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
+
- name: Get dependencies
run: flutter pub get
diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml
index 50cef79..390a215 100644
--- a/.github/workflows/build-android.yml
+++ b/.github/workflows/build-android.yml
@@ -25,6 +25,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
@@ -39,6 +41,11 @@ jobs:
channel: 'stable'
cache: true
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
+
- name: Get dependencies
run: flutter pub get
diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml
index 3f1b8ce..8ee23ef 100644
--- a/.github/workflows/build-ios.yml
+++ b/.github/workflows/build-ios.yml
@@ -28,6 +28,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
@@ -35,6 +37,11 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios
+
- name: Get dependencies
run: flutter pub get
diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml
index 650c4b5..d47216f 100644
--- a/.github/workflows/build-linux.yml
+++ b/.github/workflows/build-linux.yml
@@ -28,6 +28,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
@@ -40,6 +42,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libsecret-1-dev libjsoncpp-dev libmpv-dev mpv libopus-dev libogg-dev
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-unknown-linux-gnu
+
- name: Get dependencies
run: flutter pub get
diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml
index 62d1888..0c07ea5 100644
--- a/.github/workflows/build-macos.yml
+++ b/.github/workflows/build-macos.yml
@@ -28,6 +28,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
@@ -35,6 +37,11 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-darwin,x86_64-apple-darwin
+
- name: Install create-dmg
run: brew install create-dmg
diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml
index cad04ce..ede96ba 100644
--- a/.github/workflows/build-windows.yml
+++ b/.github/workflows/build-windows.yml
@@ -28,6 +28,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
@@ -35,6 +37,11 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-pc-windows-msvc
+
- name: Get dependencies
run: flutter pub get
diff --git a/.github/workflows/flutter-dev.yml b/.github/workflows/flutter-dev.yml
index eff518a..c3a09fa 100644
--- a/.github/workflows/flutter-dev.yml
+++ b/.github/workflows/flutter-dev.yml
@@ -12,6 +12,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
@@ -27,6 +29,11 @@ jobs:
channel: 'stable'
cache: true
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
+
- name: Install dependencies
run: flutter pub get
diff --git a/.github/workflows/flutter-main.yml b/.github/workflows/flutter-main.yml
index b6f240a..3e065f9 100644
--- a/.github/workflows/flutter-main.yml
+++ b/.github/workflows/flutter-main.yml
@@ -11,6 +11,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
@@ -26,6 +28,11 @@ jobs:
channel: 'stable'
cache: true
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
+
- name: Install dependencies
run: flutter pub get
@@ -64,11 +71,13 @@ jobs:
- name: Build Android APK
run: flutter build apk --release --flavor komet
- web-linux:
+ linux:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Set up Flutter
uses: subosito/flutter-action@v2
@@ -81,12 +90,14 @@ jobs:
sudo apt-get update
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libsecret-1-dev libjsoncpp-dev
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-unknown-linux-gnu
+
- name: Install dependencies
run: flutter pub get
- - name: Build Web
- run: flutter build web --release
-
- name: Build Linux
run: flutter build linux --release
@@ -95,6 +106,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Set up Flutter
uses: subosito/flutter-action@v2
@@ -102,6 +115,11 @@ jobs:
flutter-version: '3.44.3'
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-pc-windows-msvc
+
- name: Install dependencies
run: flutter pub get
@@ -113,6 +131,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Set up Flutter
uses: subosito/flutter-action@v2
@@ -120,6 +140,11 @@ jobs:
flutter-version: '3.44.3'
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios,aarch64-apple-darwin,x86_64-apple-darwin
+
- name: Install dependencies
run: flutter pub get
diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml
index ee4f5ca..6471534 100644
--- a/.github/workflows/release-dev.yml
+++ b/.github/workflows/release-dev.yml
@@ -26,6 +26,8 @@ jobs:
flavor: [komet, oneme]
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
@@ -40,6 +42,11 @@ jobs:
channel: 'stable'
cache: true
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
+
- name: Get dependencies
run: flutter pub get
@@ -101,11 +108,17 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-pc-windows-msvc
- name: Get dependencies
run: flutter pub get
- name: Build Windows
@@ -127,6 +140,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
@@ -136,6 +151,10 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libsecret-1-dev libjsoncpp-dev libmpv-dev mpv libopus-dev libogg-dev
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-unknown-linux-gnu
- name: Get dependencies
run: flutter pub get
- name: Build Linux
@@ -156,11 +175,17 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-darwin,x86_64-apple-darwin
- name: Get dependencies
run: flutter pub get
- name: Fix opus/ogg libs for macOS
@@ -184,11 +209,17 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios
- name: Get dependencies
run: flutter pub get
- name: Build iOS (no codesign)
diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml
index 18e45cc..7827214 100644
--- a/.github/workflows/release-main.yml
+++ b/.github/workflows/release-main.yml
@@ -21,6 +21,8 @@ jobs:
flavor: [komet, oneme]
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
@@ -35,6 +37,11 @@ jobs:
channel: 'stable'
cache: true
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
+
- name: Get dependencies
run: flutter pub get
@@ -96,11 +103,17 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-pc-windows-msvc
- name: Get dependencies
run: flutter pub get
- name: Build Windows
@@ -122,6 +135,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
@@ -131,6 +146,10 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libsecret-1-dev libjsoncpp-dev
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-unknown-linux-gnu
- name: Get dependencies
run: flutter pub get
- name: Build Linux
@@ -151,11 +170,17 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-darwin,x86_64-apple-darwin
- name: Get dependencies
run: flutter pub get
- name: Build macOS
@@ -177,11 +202,17 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios
- name: Get dependencies
run: flutter pub get
- name: Build iOS (no codesign)
@@ -238,6 +269,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Download all artifacts
uses: actions/download-artifact@v4
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..4032a51
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "third_party/rlottie"]
+ path = third_party/rlottie
+ url = https://github.com/Samsung/rlottie.git
+[submodule "third_party/kolibri"]
+ path = third_party/kolibri
+ url = https://github.com/KometTeam/kolibri.git
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index b424e3e..169f484 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -37,6 +37,18 @@ android {
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
+
+ externalNativeBuild {
+ cmake {
+ arguments += listOf("-DBUILD_SHARED_LIBS=ON")
+ }
+ }
+ }
+
+ externalNativeBuild {
+ cmake {
+ path = file("src/main/cpp/CMakeLists.txt")
+ }
}
flavorDimensions += "distribution"
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index e93a31e..6c621f0 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -33,7 +33,7 @@
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher">
+ android:targetActivity="ru.komet.app.MainActivity">
diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..fd0b9b7
--- /dev/null
+++ b/android/app/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,7 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(komet_rlottie LANGUAGES C CXX ASM)
+
+add_subdirectory(
+ "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../third_party/rlottie_build"
+ "${CMAKE_BINARY_DIR}/rlottie")
diff --git a/android/app/src/main/kotlin/ru/komet/app/MainActivity.kt b/android/app/src/main/kotlin/ru/komet/app/MainActivity.kt
index 1257145..30f6d86 100644
--- a/android/app/src/main/kotlin/ru/komet/app/MainActivity.kt
+++ b/android/app/src/main/kotlin/ru/komet/app/MainActivity.kt
@@ -52,7 +52,11 @@ import java.util.concurrent.atomic.AtomicBoolean
class MainActivity : FlutterActivity() {
private val channelName = "ru.komet.app/vpn_bypass"
- private val iconComponents = listOf("MainActivity", "MinimalIcon")
+ private val iconPackage = MainActivity::class.java.name.substringBeforeLast('.')
+ private val iconComponents = mapOf(
+ "MainActivity" to "$iconPackage.MainActivity",
+ "MinimalIcon" to "$iconPackage.MinimalIcon",
+ )
private var nfcAdapter: NfcAdapter? = null
private var nfcEvents: EventChannel.EventSink? = null
@@ -86,8 +90,8 @@ class MainActivity : FlutterActivity() {
private fun applyIcon(name: String) {
val pm = packageManager
- for (alias in iconComponents) {
- val component = ComponentName(packageName, "$packageName.$alias")
+ for ((alias, className) in iconComponents) {
+ val component = ComponentName(packageName, className)
val state = if (alias == name) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
@@ -165,7 +169,7 @@ class MainActivity : FlutterActivity() {
when (call.method) {
"setAppIcon" -> {
val name = call.argument("name")
- if (name == null || !iconComponents.contains(name)) {
+ if (name == null || !iconComponents.containsKey(name)) {
result.error("INVALID_ICON", "Unknown icon: $name", null)
return@setMethodCallHandler
}
diff --git a/assets/wallpapers/patterns/bubbles.svg b/assets/wallpapers/patterns/bubbles.svg
new file mode 100644
index 0000000..590fc10
--- /dev/null
+++ b/assets/wallpapers/patterns/bubbles.svg
@@ -0,0 +1,12 @@
+
diff --git a/assets/wallpapers/patterns/hearts.svg b/assets/wallpapers/patterns/hearts.svg
new file mode 100644
index 0000000..ac5f0d8
--- /dev/null
+++ b/assets/wallpapers/patterns/hearts.svg
@@ -0,0 +1,11 @@
+
diff --git a/assets/wallpapers/patterns/planes.svg b/assets/wallpapers/patterns/planes.svg
new file mode 100644
index 0000000..525b9dd
--- /dev/null
+++ b/assets/wallpapers/patterns/planes.svg
@@ -0,0 +1,11 @@
+
diff --git a/assets/wallpapers/patterns/plus.svg b/assets/wallpapers/patterns/plus.svg
new file mode 100644
index 0000000..fb558b6
--- /dev/null
+++ b/assets/wallpapers/patterns/plus.svg
@@ -0,0 +1,11 @@
+
diff --git a/assets/wallpapers/patterns/rings.svg b/assets/wallpapers/patterns/rings.svg
new file mode 100644
index 0000000..cbd62a4
--- /dev/null
+++ b/assets/wallpapers/patterns/rings.svg
@@ -0,0 +1,13 @@
+
diff --git a/assets/wallpapers/patterns/stars.svg b/assets/wallpapers/patterns/stars.svg
new file mode 100644
index 0000000..88c305a
--- /dev/null
+++ b/assets/wallpapers/patterns/stars.svg
@@ -0,0 +1,12 @@
+
diff --git a/ios/Podfile b/ios/Podfile
index 57209b3..fc81fe8 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -62,6 +62,7 @@ flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
+ pod 'rlottie', :path => '../third_party'
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
diff --git a/lib/backend/api.dart b/lib/backend/api.dart
index 0be7e66..fa39b16 100644
--- a/lib/backend/api.dart
+++ b/lib/backend/api.dart
@@ -1,46 +1,52 @@
import 'dart:async';
-import 'dart:typed_data';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:device_info_plus/device_info_plus.dart';
+import 'package:flutter_timezone/flutter_timezone.dart';
+import 'package:kolibri/kolibri.dart';
+import 'package:timezone/data/latest_all.dart' as tz;
import '../core/cache/self_presence.dart';
import '../core/config/config.dart';
import '../core/config/countries.dart';
import '../core/config/komet_settings.dart';
+import '../core/config/proxy_config.dart';
import '../core/protocol/opcode_map.dart';
import '../core/protocol/packet.dart';
import '../core/storage/device_identity.dart';
import '../core/storage/spoofing_service.dart';
-import '../core/transport/connection.dart';
import '../core/transport/dispatcher.dart';
-import '../core/transport/receiver.dart';
-import '../core/transport/sender.dart';
+import '../core/transport/tls_config.dart';
import '../core/transport/traffic_monitor.dart';
import '../core/transport/vpn_bypass.dart';
import '../core/utils/debug_session_log.dart';
import '../core/utils/logger.dart';
-import 'package:device_info_plus/device_info_plus.dart';
-import 'package:flutter_timezone/flutter_timezone.dart';
-import 'package:timezone/data/latest_all.dart' as tz;
-import 'dart:io';
-
enum SessionState { disconnected, connecting, connected, online }
/// Клиент API.
///
-/// Подключение, хэндшейк, пинг, реконнект.
+/// Тонкий адаптер над Rust-ядром [KolibriSession] (пакет kolibri): подключение,
+/// хэндшейк, пинг и реконнект живут в ядре, здесь — оркестрация жизненного цикла
+/// и сохранение прежнего интерфейса для модулей (Packet/пуши/стримы).
class Api {
- final Connection _connection = Connection();
- final PacketReceiver _receiver = PacketReceiver();
- final PacketSender _sender = PacketSender();
+ KolibriSession? _session;
+
+ /// Роутер пушей (ответы на запросы ядро матчит само, диспетчер держим только
+ /// ради registerHandler/pushStream).
final PacketDispatcher _dispatcher = PacketDispatcher();
+ StreamSubscription<(int, Map)>? _pushSub;
+ StreamSubscription? _wireLogSub;
SessionState _sessionState = SessionState.disconnected;
final _stateController = StreamController.broadcast();
final _sessionExpiredController =
StreamController.broadcast();
final _handshakeSuccessController = StreamController.broadcast();
- Map? _userAgent;
+ final _errorController = StreamController.broadcast();
+ Map? _userAgent;
Map? get userAgent => _userAgent;
int? _callsSeed;
@@ -49,6 +55,9 @@ class Api {
int? get callsSeed => _callsSeed;
String? get deviceId => _deviceId;
+ /// Сырой доступ к сессии ядра — для медиа-загрузок (data-plane).
+ KolibriSession? get session => _session;
+
String? spoofScope;
static bool _tzInitialized = false;
@@ -63,28 +72,25 @@ class Api {
_sessionExpiredController.stream;
Stream get handshakeSuccessStream =>
_handshakeSuccessController.stream;
- Stream get errorStream => _dispatcher.errorStream;
+ Stream get errorStream => _errorController.stream;
SessionState get state => _sessionState;
- StreamSubscription? _dataSubscription;
- StreamSubscription? _socketStateSubscription;
- Timer? _pingTimer;
+ Timer? _livenessTimer;
Timer? _reconnectTimer;
Timer? _connectWatchdog;
int _connectGen = 0;
int _reconnectAttempts = 0;
bool _autoReconnect = false;
int _sessionEpoch = 0;
+ bool? _lastInteractive;
static const Duration _connectWatchdogTimeout = Duration(seconds: 75);
static const Duration _shouldArmTimeout = Duration(seconds: 5);
static const Duration _endpointTimeout = Duration(seconds: 5);
+ static const Duration _livenessInterval = Duration(seconds: 5);
int get sessionEpoch => _sessionEpoch;
- /// Залипает на время сессии: VPN-путь не сработал — идём мимо туннеля.
- bool _bypassActive = false;
-
// Публичное API
/// Подключается к серверу, шлёт хэндшейк, запускает пинг.
@@ -100,31 +106,17 @@ class Api {
_armConnectWatchdog(gen);
try {
- _dataSubscription = _connection.dataStream.listen(_onDataReceived);
- _socketStateSubscription = _connection.stateStream.listen((socketState) {
- if (socketState == SocketState.disconnected &&
- _sessionState != SessionState.disconnected) {
- _onDisconnected();
- }
- });
-
- bool bypassArmed;
+ bool useBypass;
try {
- bypassArmed = await VpnBypassService.instance.shouldArm().timeout(
+ useBypass = await VpnBypassService.instance.shouldArm().timeout(
_shouldArmTimeout,
);
} catch (e) {
logger.w('connect: shouldArm завис/упал ($e) — без обхода VPN');
- bypassArmed = false;
+ useBypass = false;
}
if (gen != _connectGen) return;
- if (!bypassArmed) _bypassActive = false;
- final useBypass = _bypassActive && bypassArmed;
- final attemptTimeout = bypassArmed && !useBypass
- ? const Duration(seconds: 8)
- : null;
-
({String host, int port}) endpoint;
try {
endpoint = await ServerConfig.loadEndpoint().timeout(_endpointTimeout);
@@ -137,80 +129,68 @@ class Api {
}
if (gen != _connectGen) return;
+ final (session, wireLog) = await _buildSessionOptions(endpoint);
+ if (gen != _connectGen) return;
+
logger.i(
'connect: endpoint ${endpoint.host}:${endpoint.port}, bypass=$useBypass',
);
- try {
- await _connection.connect(
- endpoint.host,
- endpoint.port,
- bypassVpn: useBypass,
- timeout: attemptTimeout,
- );
- } catch (e) {
- if (gen != _connectGen) return;
- await _handleConnectFailure(
- e,
- phase: 'Не удалось подключиться',
- bypassArmed: bypassArmed,
- useBypass: useBypass,
- bypassWhy: 'подключение не удалось',
- );
- return;
+
+ if (useBypass) {
+ try {
+ await VpnBypassService.instance.bind();
+ } catch (e) {
+ logger.w('connect: VPN bind не удался ($e)');
+ }
}
- if (gen != _connectGen) return;
+
+ _session = session;
+ // Подписываемся на wire-лог ядра ДО connect(), чтобы поймать пакеты
+ // SESSION_INIT-хендшейка (иначе они уходят до listen и теряются).
+ _wireLogSub = wireLog.listen(_onWireLog);
+ _pushSub = session.pushesMap().listen(_onPush);
+ TrafficMonitor.instance.recordEvent(
+ 'connect',
+ endpoint: '${endpoint.host}:${endpoint.port}',
+ );
_setSessionState(SessionState.connected);
_reconnectAttempts = 0;
+ HandshakeInfo info;
try {
logger.i('connect: сокет готов, отправляю хэндшейк');
- final response = await sendHandshake();
- if (gen != _connectGen) return;
- if (response.isOk) {
- _callsSeed = response.payload['callsSeed'] as int?;
- _registrationCountries = _parseRegistrationCountries(
- response.payload,
- );
- _sessionState = SessionState.online;
- _sessionEpoch++;
- _cancelConnectWatchdog();
- _startPinging();
- logger.i('Сессия онлайн, хэндшейк ок');
- if (_onReconnectCallback != null) {
- try {
- await _onReconnectCallback!();
- } catch (e) {
- logger.w('Авто-логин при хэндшейке не удался: $e');
- }
- }
- if (_sessionState == SessionState.online) {
- _stateController.add(SessionState.online);
- _handshakeSuccessController.add(
- response.payload['device_name'] as String? ?? 'Unknown',
- );
- }
- } else {
- logger.e('Хэндшейк отклонён: ${response.payload}');
- await _handleConnectFailure(
- StateError('хэндшейк отклонён сервером'),
- phase: 'Хэндшейк отклонён',
- bypassArmed: bypassArmed,
- useBypass: useBypass,
- bypassWhy: 'хэндшейк отклонён',
- disconnectSocket: true,
- );
- }
+ info = await session.connect();
} catch (e) {
if (gen != _connectGen) return;
- await _handleConnectFailure(
- e,
- phase: 'Ошибка хэндшейка',
- bypassArmed: bypassArmed,
- useBypass: useBypass,
- bypassWhy: 'хэндшейк не прошёл',
- disconnectSocket: true,
- );
+ await _handleConnectFailure(e, phase: 'Ошибка хэндшейка');
+ return;
+ } finally {
+ if (useBypass) {
+ try {
+ await VpnBypassService.instance.restoreDefault();
+ } catch (_) {}
+ }
+ }
+ if (gen != _connectGen) return;
+
+ _callsSeed = info.callsSeed?.toInt();
+ _registrationCountries = _parseRegistrationCountries(info.payloadMap);
+ _sessionState = SessionState.online;
+ _sessionEpoch++;
+ _cancelConnectWatchdog();
+ _startLiveness();
+ logger.i('Сессия онлайн, хэндшейк ок');
+ if (_onReconnectCallback != null) {
+ try {
+ await _onReconnectCallback!();
+ } catch (e) {
+ logger.w('Авто-логин при хэндшейке не удался: $e');
+ }
+ }
+ if (_sessionState == SessionState.online) {
+ _stateController.add(SessionState.online);
+ _handshakeSuccessController.add(info.deviceName ?? 'Unknown');
}
} catch (e, st) {
logger.e('connect: непредвиденная ошибка: $e\n$st');
@@ -244,9 +224,6 @@ class Api {
_connectGen++;
_cancelConnectWatchdog();
_cleanup();
- try {
- await _connection.disconnect();
- } catch (_) {}
_setSessionState(SessionState.disconnected);
if (_autoReconnect) _scheduleReconnect();
}
@@ -254,37 +231,22 @@ class Api {
Future _handleConnectFailure(
Object error, {
required String phase,
- required bool bypassArmed,
- required bool useBypass,
- required String bypassWhy,
- bool disconnectSocket = false,
}) async {
logger.e('$phase: $error');
_cancelConnectWatchdog();
if (_sessionState != SessionState.disconnected) {
_cleanup();
- if (disconnectSocket) await _connection.disconnect();
_setSessionState(SessionState.disconnected);
- _armBypassIfPossible(bypassArmed, useBypass, bypassWhy);
_scheduleReconnect();
}
}
- void _armBypassIfPossible(bool armed, bool alreadyBypassing, String why) {
- if (armed && !alreadyBypassing && !_bypassActive) {
- _bypassActive = true;
- logger.w('VPN bypass: $why — следующая попытка мимо VPN');
- }
- }
-
/// Отключается без автореконнекта.
Future disconnect() async {
_autoReconnect = false;
- _bypassActive = false;
_connectGen++;
_reconnectTimer?.cancel();
_cleanup();
- await _connection.disconnect();
_setSessionState(SessionState.disconnected);
}
@@ -303,7 +265,101 @@ class Api {
}
}
- Future sendHandshake() async {
+ /// Отправляет запрос и ждёт ответ от сервера.
+ Future sendRequest(int opcode, Map payload) async {
+ final session = _session;
+ if (session == null) {
+ throw StateError('Нет соединения (${Opcode.name(opcode)})');
+ }
+ // Лог запроса/ответа ведётся из wire-лога ядра (_onWireLog) по настоящему
+ // проводному seq, поэтому здесь ничего не пишем.
+ final KolibriResponse resp = await session
+ .requestMapFull(opcode, Map.from(payload))
+ .timeout(
+ ServerConfig.requestTimeout,
+ onTimeout: () =>
+ throw TimeoutException('${Opcode.name(opcode)} таймаут'),
+ );
+
+ final packet = Packet(
+ cmd: resp.cmd,
+ opcode: resp.opcode,
+ payload: resp.payload,
+ );
+
+ if (packet.isError) {
+ if (isSessionExpiredPayload(packet.payload)) {
+ final ex = SessionExpiredException(
+ messageFromErrorPayload(packet.payload),
+ );
+ _sessionExpiredController.add(ex);
+ throw ex;
+ }
+ final text = _serverErrorText(packet.payload);
+ if (text != null) _errorController.add(text);
+ final err = PacketError(
+ messageFromErrorPayload(packet.payload),
+ errorKey: resp.errorKey,
+ );
+ throw err;
+ }
+ return packet;
+ }
+
+ Future