iOS-интеграция AmneziaWG уже работает и состоит из 6 компонентов:
| Компонент | Файл | Роль |
|---|---|---|
| Vendor plugin | plugins/withAmneziaWGVendor.js | Клонирует amneziawg-apple в vendor/ |
| NE target plugin | plugins/withWgnext.js | Создаёт Network Extension, SPM, Go bridge |
| Native bridge plugin | plugins/withRNWireGuardModule.js | Копирует ObjC модуль в ios/ |
| ObjC bridge | modules/wireguard/RNWireGuardModule.m | NativeModule: initialize, connect, disconnect, getStatus, getNetworkStats |
| Tunnel provider | WGNEXT_SOURCE/PacketTunnelProvider.swift | Читает AWG-параметры (jc, jmin, jmax, s1, s2, h1-h4), запускает WireGuardAdapter |
| TypeScript адаптер | src/services/vpn/WireGuardProvider.ts | Пробрасывает obfuscation params, маппит состояния |
[!IMPORTANT] Текущий WireGuardProvider.ts содержит проверку
Platform.OS !== 'ios'и не поддерживает Android.
Создать Expo config plugin plugins/withAmneziaWGAndroid.js по аналогии с withAmneziaWGVendor.js:
https://github.com/amnezia-vpn/amneziawg-android.git в vendor/amneziawg-android/Подключить vendor/amneziawg-android/tunnel как Gradle-модуль:
include ':tunnel' с projectDirimplementation project(':tunnel')build.gradle.kts), а проект — Groovy DSL (build.gradle)Решение: Gradle поддерживает смешанные DSL. Подключение include ':tunnel' работает независимо от DSL формата субмодуля. Нужно только убедиться, что gradle.properties содержит amneziawgPackageName=com.vpnapp.mobile.tunnel (требуется tunnel'ом).
Решение: На CI/локальной машине нужен Go 1.19+ (тот же Go, что используется для iOS). CMake автоматически скачивает зависимости через go mod. Для EAS Build — добавить Go в eas.json через "build": { "android": { "image": "ubuntu-22.04-jdk-17-ndk-25" } } и предустановить Go.
Решение: Expo prebuild уже настраивает NDK. Нужно убедиться что ndkVersion в android/build.gradle совместим с CMakeLists.txt из tunnel. Текущий NDK в проекте должен работать (проверить версию ≥ r25).
Создать plugins/withAmneziaWGAndroid.js который при prebuild:
:tunnelimplementation project(':tunnel')amneziawgPackageName в gradle.propertiesСоздать AmneziaWGVpnService.java / .kt, который:
android.net.VpnServicetunnel модуля (GoBackend, Backend interfaces)Intent extras или SharedPreferencesVpnService.prepare() — это показывает системный диалог, требующий Activity contextРешение: В RNWireGuardModule вызывать VpnService.prepare(activity), получая Activity через getCurrentActivity(). Если prepare() возвращает Intent — стартовать его через startActivityForResult и обрабатывать onActivityResult. Для React Native использовать ActivityEventListener.
POST_NOTIFICATIONS для foreground service notificationРешение: Запрашивать POST_NOTIFICATIONS runtime permission перед первым подключением. Добавить в native module метод requestNotificationPermission(), или интегрировать с expo-notifications.
Добавить через Expo config plugin plugins/withAndroidVpnPermissions.js:
<!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- VpnService declaration -->
<service
android:name=".AmneziaWGVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="false">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
Решение: Использовать withAndroidManifest из expo/config-plugins — это мержит изменения корректно, аналогично withEntitlementsPlist на iOS. Не редактировать AndroidManifest.xml вручную.
Создать Android-версию native module с тем же API, что и iOS:
| Метод | Описание |
|---|---|
initialize() |
Проверяет/запрашивает VPN permission, инициализирует GoBackend |
| connect(config: ReadableMap) | Парсит конфиг (включая AWG params), строит tunnel config, стартует VpnService |
| disconnect() | Останавливает VpnService |
| getStatus() | Возвращает {isConnected, tunnelState} |
getNetworkStats() |
Возвращает {rxBytes, txBytes} через TrafficStats или tunnel API |
Отправка событий — через RCTDeviceEventEmitter:
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("VPNStateWG", params)
Создать RNWireGuardPackage.java/kt → зарегистрировать в MainApplication через autolink или вручную.
Решение: Текущий проект (newArchEnabled: true в app.json) использует новую архитектуру. Рекомендуется реализовать модуль совместимым с обеими архитектурами. Начать с Bridge-based модуля (ReactContextBaseJavaModule), который работает в обоих режимах — New Arch fallback.
Решение: На iOS параметры передаются через providerConfiguration dict → InterfaceConfiguration свойства. На Android — через tunnel API. Нужно изучить, какие API tunnel-модуля amneziawg-android принимают AWG параметры. В amneziawg-android конфиг парсится из .conf файла — вероятно, нужно будет программно создать Config объект с AWG-расширениями. Fallback: если tunnel API не экспортирует AWG setters напрямую — генерировать .conf строку на лету и парсить её через Config.parse().
Исходные файлы поместить в modules/wireguard/android/:
modules/wireguard/android/
├── RNWireGuardModule.kt
├── RNWireGuardPackage.kt
└── AmneziaWGVpnService.kt
По аналогии с iOS (источники в modules/wireguard/), Expo plugin будет копировать файлы в android/app/src/main/java/com/vpnapp/mobile/.
Убрать проверку Platform.OS !== 'ios':
- if (Platform.OS !== 'ios') {
- console.warn('[WireGuardProvider] Currently only supported on iOS with native bridge');
- return;
- }
+ // Поддерживается iOS и Android
buildNativeConfig() уже корректно формирует объект с AWG-параметрами — этот код универсален для обеих платформ.
На iOS статусы приходят из NEVPNStatus (числовые коды 0-6). На Android — из tunnel API (свои состояния: UP, DOWN, TOGGLE). Нужно маппить оба формата в VpnState.
Решение: Нативные модули обеих платформ должны отправлять события в одинаковом формате: { state: "0"|"1"|"2"|"3"|"-1" }. Маппинг в унифицированный формат делать на нативной стороне, а не в TypeScript.
| Plugin | Файл | Назначение |
|---|---|---|
| VPN permissions | plugins/withAndroidVpnPermissions.js |
Добавляет permissions + VpnService в manifest |
| Vendor clone | plugins/withAmneziaWGAndroid.js |
Клонирует amneziawg-android, патчит Gradle |
| Native module copy | plugins/withRNWireGuardModule.js (modify) | Добавить Android-ветку: копировать .kt файлы |
"plugins": [
"./plugins/withAmneziaWGAndroid.js",
"./plugins/withAndroidVpnPermissions.js",
// существующие iOS плагины продолжают работать
]
Решение: В app.json плагин withAmneziaWGAndroid.js должен идти ПЕРЕД другими Android-специфичными плагинами. Expo выполняет плагины в порядке объявления. Vendor-клонирование происходит в withDangerousMod (synchronous), поэтому к моменту Gradle sync файлы уже будут на месте.
npx expo prebuild --platform android — успешноgetNetworkStats() возвращает корректные rx/txVPNStateWG приходят в TypeScriptРешение: Тестировать ТОЛЬКО на реальном устройстве. Android эмулятор не поддерживает VPN tunneling из-за ограничений виртуальной сети. Использовать adb logcat для дебага.
Решение: Это нормально — Go модули (wireguard-go + amneziawg patches) компилируются для всех ABI (arm64-v8a, armeabi-v7a, x86_64). Последующие билды используют кеш. Для ускорения dev-билдов можно временно ограничить ABI в build.gradle:
ndk {
abiFilters 'arm64-v8a' // только для тестового устройства
}
| # | Проблема | Критичность | Решение |
|---|---|---|---|
| 1 | Kotlin DSL vs Groovy DSL | 🟡 Низкая | Gradle поддерживает смешанные DSL |
| 2 | Go toolchain на CI | 🟡 Средняя | Установить Go 1.19+ на CI, аналогично iOS |
| 3 | NDK для Go cross-compilation | 🟡 Средняя | Проверить совместимость NDK версии (≥ r25) |
| 4 | VPN permission dialog (Activity) | 🔴 Высокая | ActivityEventListener + startActivityForResult |
| 5 | POST_NOTIFICATIONS (Android 13+) | 🟡 Средняя | Runtime permission request |
| 6 | Manifest при prebuild | 🟢 Низкая | withAndroidManifest из expo/config-plugins |
| 7 | New Architecture vs Bridge | 🟡 Средняя | Bridge-based модуль (совместим с обоими) |
| 8 | AWG params → Go tunnel | 🔴 Высокая | Config.parse() или прямые setters в tunnel API |
| 9 | Разные форматы событий iOS/Android | 🟡 Средняя | Унифицировать на нативной стороне |
| 10 | Порядок плагинов | 🟢 Низкая | Vendor plugin первым в массиве |
| 11 | Эмулятор не поддерживает VPN | 🟡 Средняя | Только реальное устройство |
| 12 | Долгая Go-компиляция | 🟢 Низкая | abiFilters для dev-билдов |
| Фаза | Описание | Срок | Зависимости |
|---|---|---|---|
| 1 | Vendor + Gradle | 3–5 дн | Go toolchain, NDK |
| 2 | VpnService | 3–5 дн | Фаза 1 |
| 3 | Native Module | 5–7 дн | Фаза 2 |
| 4 | TypeScript | 1–2 дн | Фаза 3 |
| 5 | Expo Plugins | 2–3 дн | Параллельно с Фазами 2-3 |
| 6 | Тестирование | 3–5 дн | Все фазы |
| Итого | ~3–4 недели |
| Аспект | iOS | Android |
|---|---|---|
| Vendor | amneziawg-apple (SPM) | amneziawg-android/tunnel (Gradle) |
| Go bridge | Aggregate Target + Makefile | CMake + NDK |
| Tunnel API | WireGuardAdapter (Swift) |
GoBackend / Backend (Java) |
| VPN framework | NetworkExtension + NEPacketTunnelProvider |
VpnService (Android) |
| Permission | Capabilities + Entitlements | BIND_VPN_SERVICE + system dialog |
| Native bridge | ObjC (RCTEventEmitter) |
Kotlin (ReactContextBaseJavaModule) |
| Process model | Separate App Extension process | Same app process (VpnService) |
| Package manager | SPM (local) | Gradle subproject |