v1.0.0: AWG -> VLESS migration — ReVoltVPN debut

This commit is contained in:
esefxdz committed 2026-07-24 20:56:13 +03:00
1 parent b679ba4d8f
commit aaf9887f49
29 files changed
+1071 -1401

No files matched your search

-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 PT BILLION JAYA GROUP
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,66 +0,0 @@
group 'billion.group.wireguard_flutter'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.8.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 21
}
namespace 'billion.group.wireguard_flutter'
packaging {
resources {
excludes += ['META-INF/**']
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.zaneschepke:amneziawg-android:2.3.7'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2"
implementation 'com.beust:klaxon:5.5'
implementation 'androidx.appcompat:appcompat:1.6.1'
/// to run wireguard on Android 25 or lower
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.3"
}
@@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
@@ -1 +0,0 @@
rootProject.name = 'wireguard_dart'
@@ -1,8 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The AmneziaWG GoBackend internally creates a VpnService.
FOREGROUND_SERVICE permission must be present so the
VPN notification and lifecycle work correctly. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
</manifest>
@@ -1,325 +0,0 @@
package billion.group.wireguard_flutter
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.PluginRegistry
import android.app.Activity
import io.flutter.embedding.android.FlutterActivity
import android.content.Intent
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.util.Log
import com.beust.klaxon.Klaxon
import org.amnezia.awg.backend.*
import org.amnezia.awg.crypto.Key
import org.amnezia.awg.crypto.KeyPair
import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.*
import java.util.*
import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream
/** WireguardFlutterPlugin */
const val PERMISSIONS_REQUEST_CODE = 10014
const val METHOD_CHANNEL_NAME = "billion.group.wireguard_flutter/wgcontrol"
const val METHOD_EVENT_NAME = "billion.group.wireguard_flutter/wgstage"
class WireguardFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
PluginRegistry.ActivityResultListener {
private lateinit var channel: MethodChannel
private lateinit var events: EventChannel
private lateinit var tunnelName: String
private val futureBackend = CompletableDeferred<Backend>()
private var vpnStageSink: EventChannel.EventSink? = null
private val scope = CoroutineScope(Job() + Dispatchers.Main.immediate)
private var backend: Backend? = null
private var havePermission = false
private lateinit var context: Context
private var activity: Activity? = null
private var config: org.amnezia.awg.config.Config? = null
private var tunnel: WireGuardTunnel? = null
private val TAG = "NVPN"
var isVpnChecked = false
companion object {
private var state: String = "no_connection"
fun getStatus(): String {
return state
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
this.havePermission =
(requestCode == PERMISSIONS_REQUEST_CODE) && (resultCode == Activity.RESULT_OK)
return havePermission
}
override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) {
this.activity = activityPluginBinding.activity as FlutterActivity
}
override fun onDetachedFromActivityForConfigChanges() {
this.activity = null
}
override fun onReattachedToActivityForConfigChanges(activityPluginBinding: ActivityPluginBinding) {
this.activity = activityPluginBinding.activity as FlutterActivity
}
override fun onDetachedFromActivity() {
this.activity = null
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, METHOD_CHANNEL_NAME)
events = EventChannel(flutterPluginBinding.binaryMessenger, METHOD_EVENT_NAME)
context = flutterPluginBinding.applicationContext
scope.launch(Dispatchers.IO) {
try {
backend = createBackend()
futureBackend.complete(backend!!)
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
}
channel.setMethodCallHandler(this)
events.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
isVpnChecked = false
vpnStageSink = events
}
override fun onCancel(arguments: Any?) {
isVpnChecked = false
vpnStageSink = null
}
})
}
private fun createBackend(): Backend {
if (backend == null) {
backend = GoBackend(context, object : TunnelActionHandler {
override fun runPreUp(commands: Collection<String>) {}
override fun runPostUp(commands: Collection<String>) {}
override fun runPreDown(commands: Collection<String>) {}
override fun runPostDown(commands: Collection<String>) {}
})
}
return backend as Backend
}
private fun flutterSuccess(result: Result, o: Any) {
scope.launch(Dispatchers.Main) {
result.success(o)
}
}
private fun flutterError(result: Result, error: String) {
scope.launch(Dispatchers.Main) {
result.error(error, null, null)
}
}
private fun flutterNotImplemented(result: Result) {
scope.launch(Dispatchers.Main) {
result.notImplemented()
}
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"initialize" -> setupTunnel(call.argument<String>("localizedDescription").toString(), result)
"start" -> {
connect(call.argument<String>("wgQuickConfig").toString(), result)
if (!isVpnChecked) {
if (isVpnActive()) {
state = "connected"
isVpnChecked = true
println("VPN is active")
} else {
state = "disconnected"
isVpnChecked = true
println("VPN is not active")
}
}
}
"stop" -> {
disconnect(result)
}
"stage" -> {
result.success(getStatus())
}
"checkPermission" -> {
checkPermission()
result.success(null)
}
else -> flutterNotImplemented(result)
}
}
private fun isVpnActive(): Boolean {
try {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val activeNetwork = connectivityManager.activeNetwork
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
return networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
} else {
return false
}
} catch (e: Exception) {
Log.e(TAG, "isVpnActive - ERROR - ${e.message}")
return false
}
}
private fun updateStage(stage: String?) {
scope.launch(Dispatchers.Main) {
val updatedStage = stage ?: "no_connection"
state = updatedStage
vpnStageSink?.success(updatedStage.lowercase(Locale.ROOT))
}
}
private fun updateStageFromState(state: Tunnel.State) {
scope.launch(Dispatchers.Main) {
when (state) {
Tunnel.State.UP -> updateStage("connected")
Tunnel.State.DOWN -> updateStage("disconnected")
else -> updateStage("wait_connection")
}
}
}
private fun disconnect(result: Result) {
scope.launch(Dispatchers.IO) {
try {
if (futureBackend.await().getRunningTunnelNames().isEmpty()) {
updateStage("disconnected")
throw Exception("Tunnel is not running")
}
updateStage("disconnecting")
futureBackend.await().setState(
tunnel(tunnelName) { state ->
scope.launch(Dispatchers.Main) {
Log.i(TAG, "onStateChange - $state")
updateStageFromState(state)
}
}, Tunnel.State.DOWN, config
)
Log.i(TAG, "Disconnect - success!")
flutterSuccess(result, "")
} catch (e: BackendException) {
Log.e(TAG, "Disconnect - BackendException - ERROR - ${e.reason}", e)
flutterError(result, e.reason.toString())
} catch (e: Throwable) {
Log.e(TAG, "Disconnect - Can't disconnect from tunnel: ${e.message}")
flutterError(result, e.message.toString())
}
}
}
private fun connect(wgQuickConfig: String, result: Result) {
scope.launch(Dispatchers.IO) {
try {
if (!havePermission) {
// The permission dialog was already shown by setupTunnel /
// checkPermission. Tell Flutter to wait and retry rather
// than throwing a confusing error while the system dialog
// is still on screen.
checkPermission()
flutterError(result, "VPN_PERMISSION_REQUESTED")
return@launch
}
updateStage("prepare")
val inputStream = ByteArrayInputStream(wgQuickConfig.toByteArray())
config = org.amnezia.awg.config.Config.parse(inputStream)
updateStage("connecting")
futureBackend.await().setState(
tunnel(tunnelName) { state ->
scope.launch(Dispatchers.Main) {
Log.i(TAG, "onStateChange - $state")
updateStageFromState(state)
}
}, Tunnel.State.UP, config
)
Log.i(TAG, "Connect - success!")
flutterSuccess(result, "")
} catch (e: BackendException) {
Log.e(TAG, "Connect - BackendException - ERROR - ${e.reason}", e)
flutterError(result, e.reason.toString())
} catch (e: Throwable) {
Log.e(TAG, "Connect - Can't connect to tunnel: $e", e)
flutterError(result, e.message.toString())
}
}
}
private fun setupTunnel(localizedDescription: String, result: Result) {
scope.launch(Dispatchers.IO) {
if (Tunnel.isNameInvalid(localizedDescription)) {
flutterError(result, "Invalid Name")
return@launch
}
tunnelName = localizedDescription
checkPermission()
result.success(null)
}
}
private fun checkPermission() {
val intent = android.net.VpnService.prepare(this.activity)
if (intent != null) {
havePermission = false
this.activity?.startActivityForResult(intent, PERMISSIONS_REQUEST_CODE)
} else {
havePermission = true
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
events.setStreamHandler(null)
isVpnChecked = false
}
private fun tunnel(name: String, callback: StateChangeCallback? = null): WireGuardTunnel {
if (tunnel == null) {
tunnel = WireGuardTunnel(name, callback)
}
return tunnel as WireGuardTunnel
}
}
typealias StateChangeCallback = (Tunnel.State) -> Unit
class WireGuardTunnel(
private val name: String, private val onStateChanged: StateChangeCallback? = null
) : Tunnel {
override fun getName() = name
override fun onStateChange(newState: Tunnel.State) {
onStateChanged?.invoke(newState)
}
override fun isIpv4ResolutionPreferred(): Boolean? = true
override fun isMetered(): Boolean? = false
}
@@ -1,117 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:process_run/shell.dart';
import '../wireguard_flutter_platform_interface.dart';
class WireGuardFlutterLinux extends WireGuardFlutterInterface {
String? name;
File? configFile;
VpnStage _stage = VpnStage.noConnection;
final _stageController = StreamController<VpnStage>.broadcast();
void _setStage(VpnStage stage) {
_stage = stage;
_stageController.add(stage);
}
final shell = Shell(runInShell: true, verbose: kDebugMode);
@override
Future<void> initialize({required String interfaceName}) async {
name = interfaceName.replaceAll(' ', '_');
await refreshStage();
}
Future<String> get filePath async {
final tempDir = await getTemporaryDirectory();
return '${tempDir.path}${Platform.pathSeparator}$name.conf';
}
@override
Future<void> startVpn({
required String serverAddress,
required String wgQuickConfig,
required String providerBundleIdentifier,
}) async {
final isAlreadyConnected = await isConnected();
if (!isAlreadyConnected) {
_setStage(VpnStage.preparing);
} else {
debugPrint('Already connected');
}
try {
configFile = await File(await filePath).create();
await configFile!.writeAsString(wgQuickConfig);
} on PathAccessException {
debugPrint('Denied to write file. Trying to start interface');
if (isAlreadyConnected) {
return _setStage(VpnStage.connected);
}
try {
await shell.run('sudo wg-quick up $name');
} catch (_) {
} finally {
_setStage(VpnStage.denied);
}
}
if (!isAlreadyConnected) {
_setStage(VpnStage.connecting);
await shell.run('sudo wg-quick up ${configFile?.path ?? await filePath}');
_setStage(VpnStage.connected);
}
}
@override
Future<void> stopVpn() async {
assert(
(await isConnected()),
'Bad state: vpn has not been started. Call startVpn',
);
_setStage(VpnStage.disconnecting);
try {
await shell
.run('sudo wg-quick down ${configFile?.path ?? (await filePath)}');
} catch (e) {
await refreshStage();
rethrow;
}
await refreshStage();
}
@override
Future<VpnStage> stage() async => _stage;
@override
Stream<VpnStage> get vpnStageSnapshot => _stageController.stream;
@override
Future<void> refreshStage() async {
if (await isConnected()) {
_setStage(VpnStage.connected);
} else if (name == null) {
_setStage(VpnStage.waitingConnection);
} else if (configFile == null) {
_setStage(VpnStage.noConnection);
} else {
_setStage(VpnStage.disconnected);
}
}
@override
Future<bool> isConnected() async {
assert(
name != null,
'Bad state: not initialized. Call "initialize" before calling this command',
);
final processResultList = await shell.run('sudo wg');
final process = processResultList.first;
return process.outLines.any((line) => line.trim() == 'interface: $name');
}
}
@@ -1,62 +0,0 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:wireguard_flutter/linux/wireguard_flutter_linux.dart';
import 'package:wireguard_flutter/wireguard_flutter_method_channel.dart';
import 'wireguard_flutter_platform_interface.dart';
export 'wireguard_flutter_platform_interface.dart' show VpnStage;
class WireGuardFlutter extends WireGuardFlutterInterface {
static WireGuardFlutterInterface? __instance;
static WireGuardFlutterInterface get _instance => __instance!;
static WireGuardFlutterInterface get instance {
registerWith();
return _instance;
}
static void registerWith() {
if (__instance == null) {
if (kIsWeb) {
throw UnsupportedError('The web platform is not supported');
} else if (Platform.isLinux) {
__instance = WireGuardFlutterLinux();
} else {
__instance = WireGuardFlutterMethodChannel();
}
}
}
WireGuardFlutter._();
@override
Stream<VpnStage> get vpnStageSnapshot => _instance.vpnStageSnapshot;
@override
Future<void> initialize({required String interfaceName}) {
return _instance.initialize(interfaceName: interfaceName);
}
@override
Future<void> startVpn({
required String serverAddress,
required String wgQuickConfig,
required String providerBundleIdentifier,
}) async {
return _instance.startVpn(
serverAddress: serverAddress,
wgQuickConfig: wgQuickConfig,
providerBundleIdentifier: providerBundleIdentifier,
);
}
@override
Future<void> stopVpn() => _instance.stopVpn();
@override
Future<void> refreshStage() => _instance.refreshStage();
@override
Future<VpnStage> stage() => _instance.stage();
}
@@ -1,60 +0,0 @@
import 'package:flutter/services.dart';
import 'wireguard_flutter_platform_interface.dart';
class WireGuardFlutterMethodChannel extends WireGuardFlutterInterface {
static const _methodChannelVpnControl =
"billion.group.wireguard_flutter/wgcontrol";
static const _methodChannel = MethodChannel(_methodChannelVpnControl);
static const _eventChannelVpnStage =
'billion.group.wireguard_flutter/wgstage';
static const _eventChannel = EventChannel(_eventChannelVpnStage);
@override
Stream<VpnStage> get vpnStageSnapshot =>
_eventChannel.receiveBroadcastStream().map(
(event) => event == VpnStage.denied.code
? VpnStage.disconnected
: VpnStage.values.firstWhere(
(stage) => stage.code == event,
orElse: () => VpnStage.noConnection,
),
);
@override
Future<void> initialize({required String interfaceName}) {
return _methodChannel.invokeMethod("initialize", {
"localizedDescription": interfaceName,
"win32ServiceName": interfaceName,
});
}
@override
Future<void> startVpn({
required String serverAddress,
required String wgQuickConfig,
required String providerBundleIdentifier,
}) async {
return _methodChannel.invokeMethod("start", {
"serverAddress": serverAddress,
"wgQuickConfig": wgQuickConfig,
"providerBundleIdentifier": providerBundleIdentifier,
});
}
@override
Future<void> stopVpn() => _methodChannel.invokeMethod('stop');
@override
Future<void> refreshStage() => _methodChannel.invokeMethod("refresh");
@override
Future<VpnStage> stage() => _methodChannel.invokeMethod("stage").then(
(value) => value != null
? VpnStage.values.firstWhere(
(stage) => stage.code == value.toString(),
orElse: () => VpnStage.disconnected,
)
: VpnStage.disconnected,
);
}
@@ -1,36 +0,0 @@
abstract class WireGuardFlutterInterface {
Stream<VpnStage> get vpnStageSnapshot;
Future<void> initialize({required String interfaceName});
Future<void> startVpn({
required String serverAddress,
required String wgQuickConfig,
required String providerBundleIdentifier,
});
Future<void> stopVpn();
Future<void> refreshStage();
Future<VpnStage> stage();
Future<bool> isConnected() =>
stage().then((stage) => stage == VpnStage.connected);
}
enum VpnStage {
connected('connected'),
connecting('connecting'),
disconnecting('disconnecting'),
disconnected('disconnected'),
waitingConnection('wait_connection'),
authenticating('authenticating'),
reconnect('reconnect'),
noConnection('no_connection'),
preparing('prepare'),
denied('denied'),
exiting('exiting');
final String code;
const VpnStage(this.code);
}
@@ -1,28 +0,0 @@
name: wireguard_flutter
description: Wireguard Flutter SDK
version: 0.1.3
homepage: https://flutterflux.com
repository: https://github.com/Caqil/wireguard_flutter
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: '>=3.13.7'
dependencies:
flutter:
sdk: flutter
path_provider: ^2.1.2
plugin_platform_interface: ^2.0.2
process_run: ^0.14.0+1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
plugin:
platforms:
android:
package: billion.group.wireguard_flutter
pluginClass: WireguardFlutterPlugin