Initial commit (Security Hardened Slate)
This commit is contained in:
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"
|
||||
}
|
||||
+5
@@ -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>
|
||||
+320
@@ -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
|
||||
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user