Initial commit (Security Hardened Slate)

This commit is contained in:
esefxdz committed 2026-05-29 14:42:24 +03:00
commit 157f60aad2
65 files changed
+3735

No files matched your search

@@ -0,0 +1,66 @@
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"
}
@@ -0,0 +1,5 @@
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
@@ -0,0 +1 @@
rootProject.name = 'wireguard_dart'
@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,320 @@
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) {
checkPermission()
throw Exception("Permissions are not given")
}
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
}