Support DN host enrollment (#86)
Co-authored-by: Nate Brown <nbrown.us@gmail.com>
This commit is contained in:
parent
c3f5c39d83
commit
c7a53c3905
|
@ -41,7 +41,7 @@ Update `version` in `pubspec.yaml` to reflect this release, then
|
|||
|
||||
## Android
|
||||
|
||||
`flutter build appbundle --no-shrink`
|
||||
`flutter build appbundle`
|
||||
|
||||
This will create an android app bundle at `build/app/outputs/bundle/release/`
|
||||
|
||||
|
|
|
@ -78,9 +78,12 @@ flutter {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
|
||||
implementation "androidx.security:security-crypto:1.0.0"
|
||||
implementation "androidx.work:work-runtime-ktx:$workVersion"
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation "com.google.guava:guava:31.0.1-android"
|
||||
implementation project(':mobileNebula')
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="net.defined.mobile_nebula">
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
|
@ -8,8 +9,14 @@
|
|||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="mailto" />
|
||||
</intent>
|
||||
</queries>
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:name="MyApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<service android:name=".NebulaVpnService"
|
||||
|
@ -32,6 +39,15 @@
|
|||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<!-- App linking -->
|
||||
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="http" android:host="api.defined.net" android:pathPrefix="/v1/mobile-enrollment"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<receiver android:name=".ShareReceiver" android:exported="false"/>
|
||||
<provider
|
||||
|
@ -43,6 +59,18 @@
|
|||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths"/>
|
||||
</provider>
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<!-- If you are using androidx.startup to initialize other components -->
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package net.defined.mobile_nebula
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
|
||||
class InvalidCredentialsException(): Exception("Invalid credentials")
|
||||
|
||||
class APIClient(context: Context) {
|
||||
private val packageInfo = PackageInfo(context)
|
||||
private val client = mobileNebula.MobileNebula.newAPIClient(
|
||||
"%s/%s (Android %s)".format(
|
||||
packageInfo.getName(),
|
||||
packageInfo.getVersion(),
|
||||
packageInfo.getSystemVersion(),
|
||||
))
|
||||
private val gson = Gson()
|
||||
|
||||
fun enroll(code: String): IncomingSite {
|
||||
val res = client.enroll(code)
|
||||
return decodeIncomingSite(res.site)
|
||||
}
|
||||
|
||||
fun tryUpdate(siteName: String, hostID: String, privateKey: String, counter: Long, trustedKeys: String): IncomingSite? {
|
||||
val res: mobileNebula.TryUpdateResult
|
||||
try {
|
||||
res = client.tryUpdate(siteName, hostID, privateKey, counter, trustedKeys)
|
||||
} catch (e: Exception) {
|
||||
// type information from Go is not available, use string matching instead
|
||||
if (e.message == "invalid credentials") {
|
||||
throw InvalidCredentialsException()
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
|
||||
if (res.fetchedUpdate) {
|
||||
return decodeIncomingSite(res.site)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun decodeIncomingSite(jsonSite: String): IncomingSite {
|
||||
return gson.fromJson(jsonSite, IncomingSite::class.java)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package net.defined.mobile_nebula
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
class DNUpdateWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DNUpdateWorker"
|
||||
}
|
||||
|
||||
private val context = applicationContext
|
||||
private val apiClient: APIClient = APIClient(ctx)
|
||||
private val updater = DNSiteUpdater(context, apiClient)
|
||||
private val sites = SiteList(context)
|
||||
|
||||
override fun doWork(): Result {
|
||||
var failed = false
|
||||
|
||||
sites.getSites().values.forEach { site ->
|
||||
try {
|
||||
updateSite(site)
|
||||
} catch (e: Exception) {
|
||||
failed = true
|
||||
Log.e(TAG, "Error while updating site ${site.id}: ${e.stackTraceToString()}")
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
|
||||
return if (failed) Result.failure() else Result.success();
|
||||
}
|
||||
|
||||
fun updateSite(site: Site) {
|
||||
try {
|
||||
DNUpdateLock(site).use {
|
||||
if (updater.updateSite(site)) {
|
||||
// Reload Nebula if this is the currently active site
|
||||
Intent().also { intent ->
|
||||
intent.action = NebulaVpnService.ACTION_RELOAD
|
||||
intent.putExtra("id", site.id)
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
Intent().also { intent ->
|
||||
intent.action = MainActivity.ACTION_REFRESH_SITES
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: java.nio.channels.OverlappingFileLockException) {
|
||||
Log.w(TAG, "Can't lock site ${site.name}, skipping it...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DNUpdateLock(private val site: Site): Closeable {
|
||||
private val fileChannel = FileChannel.open(
|
||||
Paths.get(site.path+"/update.lock"),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.WRITE,
|
||||
)
|
||||
private val fileLock = fileChannel.tryLock()
|
||||
|
||||
override fun close() {
|
||||
fileLock.close()
|
||||
fileChannel.close()
|
||||
}
|
||||
}
|
||||
|
||||
class DNSiteUpdater(
|
||||
private val context: Context,
|
||||
private val apiClient: APIClient,
|
||||
) {
|
||||
fun updateSite(site: Site): Boolean {
|
||||
if (!site.managed) {
|
||||
return false
|
||||
}
|
||||
|
||||
val credentials = site.getDNCredentials(context)
|
||||
|
||||
val newSite: IncomingSite?
|
||||
try {
|
||||
newSite = apiClient.tryUpdate(
|
||||
site.name,
|
||||
credentials.hostID,
|
||||
credentials.privateKey,
|
||||
credentials.counter.toLong(),
|
||||
credentials.trustedKeys,
|
||||
)
|
||||
} catch (e: InvalidCredentialsException) {
|
||||
if (!credentials.invalid) {
|
||||
site.invalidateDNCredentials(context)
|
||||
Log.d(TAG, "Invalidated credentials in site ${site.name}")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (newSite != null) {
|
||||
newSite.save(context)
|
||||
Log.d(TAG, "Updated site ${site.id}: ${site.name}")
|
||||
return true
|
||||
}
|
||||
|
||||
if (credentials.invalid) {
|
||||
site.validateDNCredentials(context)
|
||||
Log.d(TAG, "Revalidated credentials in site ${site.id}: ${site.name}")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,35 +1,53 @@
|
|||
package net.defined.mobile_nebula
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.net.VpnService
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.work.*
|
||||
import com.google.common.base.Throwables
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.FutureCallback
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
const val TAG = "nebula"
|
||||
const val VPN_PERMISSIONS_CODE = 0x0F
|
||||
const val VPN_START_CODE = 0x10
|
||||
const val CHANNEL = "net.defined.mobileNebula/NebulaVpnService"
|
||||
const val UPDATE_WORKER = "dnUpdater"
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
private var sites: Sites? = null
|
||||
private var permResult: MethodChannel.Result? = null
|
||||
|
||||
private var inMessenger: Messenger? = Messenger(IncomingHandler())
|
||||
private var outMessenger: Messenger? = null
|
||||
|
||||
private var apiClient: APIClient? = null
|
||||
private var sites: Sites? = null
|
||||
private var permResult: MethodChannel.Result? = null
|
||||
|
||||
private var ui: MethodChannel? = null
|
||||
|
||||
private var activeSiteId: String? = null
|
||||
|
||||
private val workManager = WorkManager.getInstance(application)
|
||||
private val refreshReceiver: BroadcastReceiver = RefreshReceiver()
|
||||
|
||||
companion object {
|
||||
const val ACTION_REFRESH_SITES = "net.defined.mobileNebula.REFRESH_SITES"
|
||||
|
||||
private var appContext: Context? = null
|
||||
fun getContext(): Context? { return appContext }
|
||||
}
|
||||
|
@ -38,10 +56,11 @@ class MainActivity: FlutterActivity() {
|
|||
appContext = context
|
||||
//TODO: Initializing in the constructor leads to a context lacking info we need, figure out the right way to do this
|
||||
sites = Sites(flutterEngine)
|
||||
|
||||
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
ui = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||
ui!!.setMethodCallHandler { call, result ->
|
||||
when(call.method) {
|
||||
"android.requestPermissions" -> androidPermissions(result)
|
||||
"android.registerActiveSite" -> registerActiveSite(result)
|
||||
|
@ -51,6 +70,8 @@ class MainActivity: FlutterActivity() {
|
|||
"nebula.renderConfig" -> nebulaRenderConfig(call, result)
|
||||
"nebula.verifyCertAndKey" -> nebulaVerifyCertAndKey(call, result)
|
||||
|
||||
"dn.enroll" -> dnEnroll(call, result)
|
||||
|
||||
"listSites" -> listSites(result)
|
||||
"deleteSite" -> deleteSite(call, result)
|
||||
"saveSite" -> saveSite(call, result)
|
||||
|
@ -71,6 +92,30 @@ class MainActivity: FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
apiClient = APIClient(context)
|
||||
|
||||
registerReceiver(refreshReceiver, IntentFilter(ACTION_REFRESH_SITES))
|
||||
|
||||
enqueueDNUpdater()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
unregisterReceiver(refreshReceiver)
|
||||
}
|
||||
|
||||
private fun enqueueDNUpdater() {
|
||||
val workRequest = PeriodicWorkRequestBuilder<DNUpdateWorker>(15, TimeUnit.MINUTES).build()
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
UPDATE_WORKER,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
workRequest)
|
||||
}
|
||||
|
||||
// This is called by the UI _after_ it has finished rendering the site list to avoid a race condition with detecting
|
||||
// the current active site and attaching site specific event channels in the event the UI app was quit
|
||||
private fun registerActiveSite(result: MethodChannel.Result) {
|
||||
|
@ -124,6 +169,28 @@ class MainActivity: FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun dnEnroll(call: MethodCall, result: MethodChannel.Result) {
|
||||
val code = call.arguments as String
|
||||
if (code == "") {
|
||||
return result.error("required_argument", "code is a required argument", null)
|
||||
}
|
||||
|
||||
val site: IncomingSite
|
||||
val siteDir: File
|
||||
try {
|
||||
site = apiClient!!.enroll(code)
|
||||
siteDir = site.save(context)
|
||||
} catch (err: Exception) {
|
||||
return result.error("unhandled_error", err.message, null)
|
||||
}
|
||||
|
||||
if (!validateOrDeleteSite(siteDir)) {
|
||||
return result.error("failure", "Enrollment failed due to invalid config", null)
|
||||
}
|
||||
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun listSites(result: MethodChannel.Result) {
|
||||
sites!!.refreshSites(activeSiteId)
|
||||
val sites = sites!!.getSites()
|
||||
|
@ -143,40 +210,50 @@ class MainActivity: FlutterActivity() {
|
|||
|
||||
private fun saveSite(call: MethodCall, result: MethodChannel.Result) {
|
||||
val site: IncomingSite
|
||||
val siteDir: File
|
||||
try {
|
||||
val gson = Gson()
|
||||
site = gson.fromJson(call.arguments as String, IncomingSite::class.java)
|
||||
site.save(context)
|
||||
siteDir = site.save(context)
|
||||
|
||||
} catch (err: Exception) {
|
||||
//TODO: is toString the best or .message?
|
||||
return result.error("failure", err.toString(), null)
|
||||
}
|
||||
|
||||
val siteDir = context.filesDir.resolve("sites").resolve(site.id)
|
||||
try {
|
||||
// Try to render a full site, if this fails the config was bad somehow
|
||||
Site(siteDir)
|
||||
} catch (err: Exception) {
|
||||
siteDir.deleteRecursively()
|
||||
if (!validateOrDeleteSite(siteDir)) {
|
||||
return result.error("failure", "Site config was incomplete, please review and try again", null)
|
||||
}
|
||||
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun validateOrDeleteSite(siteDir: File): Boolean {
|
||||
try {
|
||||
// Try to render a full site, if this fails the config was bad somehow
|
||||
val site = Site(context, siteDir)
|
||||
} catch(err: java.io.FileNotFoundException) {
|
||||
Log.e(TAG, "Site not found at ${siteDir}")
|
||||
return false
|
||||
} catch(err: Exception) {
|
||||
Log.e(TAG, "Deleting site at ${siteDir} due to error: ${err}")
|
||||
siteDir.deleteRecursively()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun startSite(call: MethodCall, result: MethodChannel.Result) {
|
||||
val id = call.argument<String>("id")
|
||||
if (id == "") {
|
||||
return result.error("required_argument", "id is a required argument", null)
|
||||
}
|
||||
|
||||
var siteContainer: SiteContainer = sites!!.getSite(id!!) ?: return result.error("unknown_site", "No site with that id exists", null)
|
||||
val siteContainer: SiteContainer = sites!!.getSite(id!!) ?: return result.error("unknown_site", "No site with that id exists", null)
|
||||
|
||||
siteContainer.site.connected = true
|
||||
siteContainer.site.status = "Initializing..."
|
||||
siteContainer.updater.setState(true, "Initializing...")
|
||||
|
||||
val intent = VpnService.prepare(this)
|
||||
var intent = VpnService.prepare(this)
|
||||
if (intent != null) {
|
||||
//TODO: ensure this boots the correct bit, I bet it doesn't and we need to go back to the active symlink
|
||||
intent.putExtra("path", siteContainer.site.path)
|
||||
|
@ -184,7 +261,7 @@ class MainActivity: FlutterActivity() {
|
|||
startActivityForResult(intent, VPN_START_CODE)
|
||||
|
||||
} else {
|
||||
val intent = Intent(this, NebulaVpnService::class.java)
|
||||
intent = Intent(this, NebulaVpnService::class.java)
|
||||
intent.putExtra("path", siteContainer.site.path)
|
||||
intent.putExtra("id", siteContainer.site.id)
|
||||
onActivityResult(VPN_START_CODE, Activity.RESULT_OK, intent)
|
||||
|
@ -254,7 +331,7 @@ class MainActivity: FlutterActivity() {
|
|||
}
|
||||
|
||||
val pending = call.argument<Boolean>("pending") ?: false
|
||||
|
||||
|
||||
if (outMessenger == null || activeSiteId == null || activeSiteId != id) {
|
||||
return result.success(null)
|
||||
}
|
||||
|
@ -302,7 +379,7 @@ class MainActivity: FlutterActivity() {
|
|||
})
|
||||
outMessenger?.send(msg)
|
||||
}
|
||||
|
||||
|
||||
private fun activeCloseTunnel(call: MethodCall, result: MethodChannel.Result) {
|
||||
val id = call.argument<String>("id")
|
||||
if (id == "") {
|
||||
|
@ -355,7 +432,8 @@ class MainActivity: FlutterActivity() {
|
|||
return result.error("PERMISSIONS", "User did not grant permission", null)
|
||||
|
||||
} else if (requestCode == VPN_START_CODE) {
|
||||
// We are processing a response for permissions while starting the VPN (or reusing code in the event we already have perms)
|
||||
// We are processing a response for permissions while starting the VPN
|
||||
// (or reusing code in the event we already have perms)
|
||||
startService(data)
|
||||
if (outMessenger == null) {
|
||||
bindService(data, connection, 0)
|
||||
|
@ -368,14 +446,15 @@ class MainActivity: FlutterActivity() {
|
|||
}
|
||||
|
||||
/** Defines callbacks for service binding, passed to bindService() */
|
||||
val connection = object : ServiceConnection {
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
outMessenger = Messenger(service)
|
||||
|
||||
// We want to monitor the service for as long as we are connected to it.
|
||||
try {
|
||||
val msg = Message.obtain(null, NebulaVpnService.MSG_REGISTER_CLIENT)
|
||||
msg.replyTo = inMessenger
|
||||
outMessenger?.send(msg)
|
||||
outMessenger!!.send(msg)
|
||||
|
||||
} catch (e: RemoteException) {
|
||||
// In this case the service has crashed before we could even
|
||||
|
@ -386,7 +465,7 @@ class MainActivity: FlutterActivity() {
|
|||
}
|
||||
|
||||
val msg = Message.obtain(null, NebulaVpnService.MSG_IS_RUNNING)
|
||||
outMessenger?.send(msg)
|
||||
outMessenger!!.send(msg)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg0: ComponentName) {
|
||||
|
@ -416,7 +495,7 @@ class MainActivity: FlutterActivity() {
|
|||
private fun isRunning(site: SiteContainer, msg: Message) {
|
||||
var status = "Disconnected"
|
||||
var connected = false
|
||||
|
||||
|
||||
if (msg.arg1 == 1) {
|
||||
status = "Connected"
|
||||
connected = true
|
||||
|
@ -429,6 +508,32 @@ class MainActivity: FlutterActivity() {
|
|||
private fun serviceExited(site: SiteContainer, msg: Message) {
|
||||
activeSiteId = null
|
||||
site.updater.setState(false, "Disconnected", msg.data.getString("error"))
|
||||
unbindVpnService()
|
||||
}
|
||||
}
|
||||
|
||||
private fun unbindVpnService() {
|
||||
if (outMessenger != null) {
|
||||
// Unregister ourselves
|
||||
val msg = Message.obtain(null, NebulaVpnService.MSG_UNREGISTER_CLIENT)
|
||||
msg.replyTo = inMessenger
|
||||
outMessenger!!.send(msg)
|
||||
// Unbind
|
||||
unbindService(connection)
|
||||
}
|
||||
outMessenger = null
|
||||
}
|
||||
|
||||
inner class RefreshReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent?.getAction() != ACTION_REFRESH_SITES) return
|
||||
if (sites == null) return
|
||||
|
||||
Log.d(TAG, "Refreshing sites in MainActivity")
|
||||
|
||||
sites?.refreshSites(activeSiteId)
|
||||
ui?.invokeMethod("refreshSites", null)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package net.defined.mobile_nebula
|
||||
|
||||
import io.flutter.view.FlutterMain
|
||||
import android.app.Application
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
|
||||
class MyApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// In order to use the WorkManager from the nebulaVpnBg process (i.e. NebulaVpnService)
|
||||
// we must explicitly initialize this rather than using the default initializer.
|
||||
val myConfig = Configuration.Builder().build()
|
||||
WorkManager.initialize(this, myConfig)
|
||||
|
||||
FlutterMain.startInitialization(applicationContext)
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import android.os.*
|
|||
import android.system.OsConstants
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.work.*
|
||||
import mobileNebula.CIDR
|
||||
import java.io.File
|
||||
|
||||
|
@ -17,8 +18,11 @@ import java.io.File
|
|||
class NebulaVpnService : VpnService() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NebulaVpnService"
|
||||
const val TAG = "NebulaVpnService"
|
||||
|
||||
const val ACTION_STOP = "net.defined.mobile_nebula.STOP"
|
||||
const val ACTION_RELOAD = "net.defined.mobile_nebula.RELOAD"
|
||||
|
||||
const val MSG_REGISTER_CLIENT = 1
|
||||
const val MSG_UNREGISTER_CLIENT = 2
|
||||
const val MSG_IS_RUNNING = 3
|
||||
|
@ -36,6 +40,10 @@ class NebulaVpnService : VpnService() {
|
|||
private lateinit var messenger: Messenger
|
||||
private val mClients = ArrayList<Messenger>()
|
||||
|
||||
private val reloadReceiver: BroadcastReceiver = ReloadReceiver()
|
||||
private var workManager: WorkManager? = null
|
||||
|
||||
private var path: String? = null
|
||||
private var running: Boolean = false
|
||||
private var site: Site? = null
|
||||
private var nebula: mobileNebula.Nebula? = null
|
||||
|
@ -43,13 +51,17 @@ class NebulaVpnService : VpnService() {
|
|||
private var didSleep = false
|
||||
private var networkCallback: NetworkCallback = NetworkCallback()
|
||||
|
||||
override fun onCreate() {
|
||||
workManager = WorkManager.getInstance(this)
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent?.getAction() == ACTION_STOP) {
|
||||
stopVpn()
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
val path = intent?.getStringExtra("path")
|
||||
val id = intent?.getStringExtra("id")
|
||||
|
||||
if (running) {
|
||||
|
@ -63,9 +75,10 @@ class NebulaVpnService : VpnService() {
|
|||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
path = intent?.getStringExtra("path")
|
||||
//TODO: if we fail to start, android will attempt a restart lacking all the intent data we need.
|
||||
// Link active site config in Main to avoid this
|
||||
site = Site(File(path))
|
||||
site = Site(this, File(path))
|
||||
|
||||
if (site!!.cert == null) {
|
||||
announceExit(id, "Site is missing a certificate")
|
||||
|
@ -73,6 +86,10 @@ class NebulaVpnService : VpnService() {
|
|||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
// Kick off a site update
|
||||
val workRequest = OneTimeWorkRequestBuilder<DNUpdateWorker>().build()
|
||||
workManager!!.enqueue(workRequest)
|
||||
|
||||
// We don't actually start here. In order to properly capture boot errors we wait until an IPC connection is made
|
||||
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
|
@ -117,6 +134,7 @@ class NebulaVpnService : VpnService() {
|
|||
}
|
||||
|
||||
registerNetworkCallback()
|
||||
registerReloadReceiver()
|
||||
//TODO: There is an open discussion around sleep killing tunnels or just changing mobile to tear down stale tunnels
|
||||
//registerSleep()
|
||||
|
||||
|
@ -173,12 +191,26 @@ class NebulaVpnService : VpnService() {
|
|||
registerReceiver(receiver, IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED))
|
||||
}
|
||||
|
||||
private fun registerReloadReceiver() {
|
||||
registerReceiver(reloadReceiver, IntentFilter(ACTION_RELOAD))
|
||||
}
|
||||
|
||||
private fun unregisterReloadReceiver() {
|
||||
unregisterReceiver(reloadReceiver)
|
||||
}
|
||||
|
||||
private fun reload() {
|
||||
site = Site(this, File(path))
|
||||
nebula?.reload(site!!.config, site!!.getKey(this))
|
||||
}
|
||||
|
||||
private fun stopVpn() {
|
||||
if (nebula == null) {
|
||||
return stopSelf()
|
||||
}
|
||||
|
||||
unregisterNetworkCallback()
|
||||
unregisterReloadReceiver()
|
||||
nebula?.stop()
|
||||
nebula = null
|
||||
running = false
|
||||
|
@ -207,6 +239,18 @@ class NebulaVpnService : VpnService() {
|
|||
send(msg, id)
|
||||
}
|
||||
|
||||
inner class ReloadReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent?.getAction() != ACTION_RELOAD) return
|
||||
if (!running) return
|
||||
if (intent?.getStringExtra("id") != site!!.id) return
|
||||
|
||||
Log.d(TAG, "Reloading Nebula")
|
||||
|
||||
reload()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler of incoming messages from clients.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package net.defined.mobile_nebula
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.Build
|
||||
|
||||
class PackageInfo(val context: Context) {
|
||||
private val pInfo: PackageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0)
|
||||
private val appInfo: ApplicationInfo = context.getApplicationInfo()
|
||||
|
||||
fun getVersion(): String {
|
||||
val version: String = pInfo.versionName
|
||||
val build: Int = pInfo.versionCode
|
||||
return "%s-%d".format(version, build)
|
||||
}
|
||||
|
||||
fun getName(): String {
|
||||
val stringId = appInfo.labelRes
|
||||
return if (stringId == 0) appInfo.nonLocalizedLabel.toString() else context.getString(stringId)
|
||||
}
|
||||
|
||||
fun getSystemVersion(): String {
|
||||
return Build.VERSION.RELEASE;
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import com.google.gson.annotations.SerializedName
|
|||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
data class SiteContainer(
|
||||
|
@ -16,7 +17,7 @@ data class SiteContainer(
|
|||
)
|
||||
|
||||
class Sites(private var engine: FlutterEngine) {
|
||||
private var sites: HashMap<String, SiteContainer> = HashMap()
|
||||
private var containers: HashMap<String, SiteContainer> = HashMap()
|
||||
|
||||
init {
|
||||
refreshSites()
|
||||
|
@ -24,65 +25,111 @@ class Sites(private var engine: FlutterEngine) {
|
|||
|
||||
fun refreshSites(activeSite: String? = null) {
|
||||
val context = MainActivity.getContext()!!
|
||||
val sitesDir = context.filesDir.resolve("sites")
|
||||
|
||||
if (!sitesDir.isDirectory) {
|
||||
sitesDir.delete()
|
||||
sitesDir.mkdir()
|
||||
}
|
||||
|
||||
sites = HashMap()
|
||||
sitesDir.listFiles().forEach { siteDir ->
|
||||
try {
|
||||
val site = Site(siteDir)
|
||||
|
||||
// Make sure we can load the private key
|
||||
site.getKey(context)
|
||||
|
||||
val updater = SiteUpdater(site, engine)
|
||||
if (site.id == activeSite) {
|
||||
updater.setState(true, "Connected")
|
||||
}
|
||||
|
||||
this.sites[site.id] = SiteContainer(site, updater)
|
||||
|
||||
} catch (err: Exception) {
|
||||
siteDir.deleteRecursively()
|
||||
Log.e(TAG, "Deleting non conforming site ${siteDir.absolutePath}", err)
|
||||
val sites = SiteList(context)
|
||||
val containers: HashMap<String, SiteContainer> = HashMap()
|
||||
sites.getSites().values.forEach { site ->
|
||||
// Don't create a new SiteUpdater or we will lose subscribers
|
||||
var updater = this.containers[site.id]?.updater
|
||||
if (updater != null) {
|
||||
updater.setSite(site)
|
||||
} else {
|
||||
updater = SiteUpdater(site, engine)
|
||||
}
|
||||
|
||||
if (site.id == activeSite) {
|
||||
updater.setState(true, "Connected")
|
||||
}
|
||||
|
||||
containers[site.id] = SiteContainer(site, updater)
|
||||
}
|
||||
this.containers = containers
|
||||
}
|
||||
|
||||
fun getSites(): Map<String, Site> {
|
||||
return sites.mapValues { it.value.site }
|
||||
return containers.mapValues { it.value.site }
|
||||
}
|
||||
|
||||
fun deleteSite(id: String) {
|
||||
sites.remove(id)
|
||||
val siteDir = MainActivity.getContext()!!.filesDir.resolve("sites").resolve(id)
|
||||
siteDir.deleteRecursively()
|
||||
refreshSites()
|
||||
//TODO: make sure you stop the vpn
|
||||
//TODO: make sure you relink the active site if this is the active site
|
||||
}
|
||||
|
||||
|
||||
fun getSite(id: String): SiteContainer? {
|
||||
return sites[id]
|
||||
return containers[id]
|
||||
}
|
||||
}
|
||||
|
||||
class SiteList(context: Context) {
|
||||
private var sites: Map<String, Site>
|
||||
|
||||
init {
|
||||
val nebulaSites = getSites(context, context.filesDir)
|
||||
val dnSites = getSites(context, context.noBackupFilesDir)
|
||||
|
||||
// In case of a conflict, dnSites will take precedence.
|
||||
sites = nebulaSites + dnSites
|
||||
}
|
||||
|
||||
fun getSites(): Map<String, Site> {
|
||||
return sites
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getSites(context: Context, directory: File): HashMap<String, Site> {
|
||||
val sites = HashMap<String, Site>()
|
||||
|
||||
val sitesDir = directory.resolve("sites")
|
||||
|
||||
if (!sitesDir.isDirectory) {
|
||||
sitesDir.delete()
|
||||
sitesDir.mkdir()
|
||||
}
|
||||
|
||||
sitesDir.listFiles()?.forEach { siteDir ->
|
||||
try {
|
||||
val site = Site(context, siteDir)
|
||||
|
||||
// Make sure we can load the private key
|
||||
site.getKey(context)
|
||||
|
||||
// Make sure we can load the DN credentials if managed
|
||||
if (site.managed) {
|
||||
site.getDNCredentials(context)
|
||||
}
|
||||
|
||||
sites[site.id] = site
|
||||
} catch (err: Exception) {
|
||||
siteDir.deleteRecursively()
|
||||
Log.e(TAG, "Deleting non conforming site ${siteDir.absolutePath}", err)
|
||||
}
|
||||
}
|
||||
|
||||
return sites
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SiteUpdater(private var site: Site, engine: FlutterEngine): EventChannel.StreamHandler {
|
||||
private val gson = Gson()
|
||||
// eventSink is how we send info back up to flutter
|
||||
private var eventChannel: EventChannel = EventChannel(engine.dartExecutor.binaryMessenger, "net.defined.nebula/${site.id}")
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
|
||||
|
||||
fun setSite(site: Site) {
|
||||
this.site = site
|
||||
}
|
||||
|
||||
fun setState(connected: Boolean, status: String, err: String? = null) {
|
||||
site.connected = connected
|
||||
site.status = status
|
||||
val d = mapOf("connected" to site.connected, "status" to site.status)
|
||||
if (err != null) {
|
||||
eventSink?.error("", err, d)
|
||||
eventSink?.error("", err, gson.toJson(site))
|
||||
} else {
|
||||
eventSink?.success(d)
|
||||
eventSink?.success(gson.toJson(site))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,7 +177,24 @@ data class CertificateValidity(
|
|||
@SerializedName("Reason") val reason: String
|
||||
)
|
||||
|
||||
class Site {
|
||||
data class DNCredentials(
|
||||
val hostID: String,
|
||||
val privateKey: String,
|
||||
val counter: Int,
|
||||
val trustedKeys: String,
|
||||
var invalid: Boolean,
|
||||
) {
|
||||
fun save(context: Context, siteDir: File) {
|
||||
val jsonCreds = Gson().toJson(this)
|
||||
|
||||
val credsFile = siteDir.resolve("dnCredentials")
|
||||
credsFile.delete()
|
||||
|
||||
EncFile(context).openWrite(credsFile).use { it.write(jsonCreds) }
|
||||
}
|
||||
}
|
||||
|
||||
class Site(context: Context, siteDir: File) {
|
||||
val name: String
|
||||
val id: String
|
||||
val staticHostmap: HashMap<String, StaticHosts>
|
||||
|
@ -142,21 +206,25 @@ class Site {
|
|||
val mtu: Int
|
||||
val cipher: String
|
||||
val sortKey: Int
|
||||
var logVerbosity: String
|
||||
val logVerbosity: String
|
||||
var connected: Boolean?
|
||||
var status: String?
|
||||
val logFile: String?
|
||||
var errors: ArrayList<String> = ArrayList()
|
||||
|
||||
val managed: Boolean
|
||||
// The following fields are present when managed = true
|
||||
val rawConfig: String?
|
||||
val lastManagedUpdate: String?
|
||||
|
||||
// Path to this site on disk
|
||||
@Expose(serialize = false)
|
||||
@Transient
|
||||
val path: String
|
||||
|
||||
// Strong representation of the site config
|
||||
@Expose(serialize = false)
|
||||
@Transient
|
||||
val config: String
|
||||
|
||||
constructor(siteDir: File) {
|
||||
|
||||
init {
|
||||
val gson = Gson()
|
||||
config = siteDir.resolve("config.json").readText()
|
||||
val incomingSite = gson.fromJson(config, IncomingSite::class.java)
|
||||
|
@ -173,6 +241,9 @@ class Site {
|
|||
sortKey = incomingSite.sortKey ?: 0
|
||||
logFile = siteDir.resolve("log").absolutePath
|
||||
logVerbosity = incomingSite.logVerbosity ?: "info"
|
||||
rawConfig = incomingSite.rawConfig
|
||||
managed = incomingSite.managed ?: false
|
||||
lastManagedUpdate = incomingSite.lastManagedUpdate
|
||||
|
||||
connected = false
|
||||
status = "Disconnected"
|
||||
|
@ -211,6 +282,10 @@ class Site {
|
|||
errors.add("Error while loading certificate authorities: ${err.message}")
|
||||
}
|
||||
|
||||
if (managed && getDNCredentials(context).invalid) {
|
||||
errors.add("Unable to fetch updates - please re-enroll the device")
|
||||
}
|
||||
|
||||
if (errors.isEmpty()) {
|
||||
try {
|
||||
mobileNebula.MobileNebula.testConfig(config, getKey(MainActivity.getContext()!!))
|
||||
|
@ -220,12 +295,31 @@ class Site {
|
|||
}
|
||||
}
|
||||
|
||||
fun getKey(context: Context): String? {
|
||||
fun getKey(context: Context): String {
|
||||
val f = EncFile(context).openRead(File(path).resolve("key"))
|
||||
val k = f.readText()
|
||||
f.close()
|
||||
return k
|
||||
}
|
||||
|
||||
fun getDNCredentials(context: Context): DNCredentials {
|
||||
val filepath = File(path).resolve("dnCredentials")
|
||||
val f = EncFile(context).openRead(filepath)
|
||||
val cfg = f.use { it.readText() }
|
||||
return Gson().fromJson(cfg, DNCredentials::class.java)
|
||||
}
|
||||
|
||||
fun invalidateDNCredentials(context: Context) {
|
||||
val creds = getDNCredentials(context)
|
||||
creds.invalid = true
|
||||
creds.save(context, File(path))
|
||||
}
|
||||
|
||||
fun validateDNCredentials(context: Context) {
|
||||
val creds = getDNCredentials(context)
|
||||
creds.invalid = false
|
||||
creds.save(context, File(path))
|
||||
}
|
||||
}
|
||||
|
||||
data class StaticHosts(
|
||||
|
@ -251,13 +345,18 @@ class IncomingSite(
|
|||
val mtu: Int?,
|
||||
val cipher: String,
|
||||
val sortKey: Int?,
|
||||
var logVerbosity: String?,
|
||||
@Expose(serialize = false)
|
||||
var key: String?
|
||||
val logVerbosity: String?,
|
||||
var key: String?,
|
||||
val managed: Boolean?,
|
||||
// The following fields are present when managed = true
|
||||
val lastManagedUpdate: String?,
|
||||
val rawConfig: String?,
|
||||
var dnCredentials: DNCredentials?,
|
||||
) {
|
||||
|
||||
fun save(context: Context) {
|
||||
val siteDir = context.filesDir.resolve("sites").resolve(id)
|
||||
fun save(context: Context): File {
|
||||
// Don't allow backups of DN-managed sites
|
||||
val baseDir = if(managed == true) context.noBackupFilesDir else context.filesDir
|
||||
val siteDir = baseDir.resolve("sites").resolve(id)
|
||||
if (!siteDir.exists()) {
|
||||
siteDir.mkdir()
|
||||
}
|
||||
|
@ -269,10 +368,14 @@ class IncomingSite(
|
|||
encFile.use { it.write(key) }
|
||||
encFile.close()
|
||||
}
|
||||
|
||||
key = null
|
||||
val gson = Gson()
|
||||
|
||||
dnCredentials?.save(context, siteDir)
|
||||
dnCredentials = null
|
||||
|
||||
val confFile = siteDir.resolve("config.json")
|
||||
confFile.writeText(gson.toJson(this))
|
||||
confFile.writeText(Gson().toJson(this))
|
||||
|
||||
return siteDir
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.6.10'
|
||||
ext {
|
||||
workVersion = "2.7.1"
|
||||
kotlinVersion = '1.6.10'
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
@ -7,7 +11,7 @@ buildscript {
|
|||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="53" height="62" viewBox="0 0 53 62" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M42.1128 61.2016H25.8226C30.4449 55.8553 42.14 32.9921 36.5151 23.1053C32.4774 15.9477 19.5464 12.8338 0 14.1999V0.323899C25.6196 -1.42992 41.6675 3.94663 48.6585 16.2567C57.4851 31.9077 47.3469 52.4022 42.1128 61.2016Z" fill="white"/>
|
||||
<path d="M0 61.2106H13.9245V21.6453L0 14.0424V61.2106Z" fill="#6E7D91"/>
|
||||
</svg>
|
After Width: | Height: | Size: 421 B |
|
@ -0,0 +1,4 @@
|
|||
<svg width="53" height="62" viewBox="0 0 53 62" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M42.1128 61.2016H25.8226C30.4449 55.8553 42.14 32.9921 36.5151 23.1053C32.4774 15.9477 19.5464 12.8338 0 14.1999V0.323899C25.6196 -1.42992 41.6675 3.94663 48.6585 16.2567C57.4851 31.9077 47.3469 52.4022 42.1128 61.2016Z" fill="#0B0D0F"/>
|
||||
<path d="M0 61.2106H13.9245V21.6453L0 14.0424V61.2106Z" fill="#6E7D91"/>
|
||||
</svg>
|
After Width: | Height: | Size: 423 B |
|
@ -3,17 +3,21 @@ import Foundation
|
|||
let groupName = "group.net.defined.mobileNebula"
|
||||
|
||||
class KeyChain {
|
||||
class func save(key: String, data: Data) -> Bool {
|
||||
let query: [String: Any] = [
|
||||
class func save(key: String, data: Data, managed: Bool) -> Bool {
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String : kSecClassGenericPassword as String,
|
||||
kSecAttrAccount as String : key,
|
||||
kSecValueData as String : data,
|
||||
kSecAttrAccessGroup as String: groupName,
|
||||
]
|
||||
|
||||
if (managed) {
|
||||
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||
}
|
||||
|
||||
SecItemDelete(query as CFDictionary)
|
||||
let val = SecItemAdd(query as CFDictionary, nil)
|
||||
return val == 0
|
||||
// Attempt to delete an existing key to allow for an overwrite
|
||||
_ = self.delete(key: key)
|
||||
return SecItemAdd(query as CFDictionary, nil) == 0
|
||||
}
|
||||
|
||||
class func load(key: String) -> Data? {
|
||||
|
@ -38,10 +42,8 @@ class KeyChain {
|
|||
|
||||
class func delete(key: String) -> Bool {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String : kSecClassGenericPassword,
|
||||
kSecClass as String : kSecClassGenericPassword as String,
|
||||
kSecAttrAccount as String : key,
|
||||
kSecReturnData as String : kCFBooleanTrue!,
|
||||
kSecMatchLimit as String : kSecMatchLimitOne,
|
||||
kSecAttrAccessGroup as String: groupName,
|
||||
]
|
||||
|
||||
|
|
|
@ -7,18 +7,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
private var networkMonitor: NWPathMonitor?
|
||||
|
||||
private var site: Site?
|
||||
private var _log = OSLog(subsystem: "net.defined.mobileNebula", category: "PacketTunnelProvider")
|
||||
private var log = Logger(subsystem: "net.defined.mobileNebula", category: "PacketTunnelProvider")
|
||||
private var nebula: MobileNebulaNebula?
|
||||
private var dnUpdater = DNUpdater()
|
||||
private var didSleep = false
|
||||
private var cachedRouteDescription: String?
|
||||
|
||||
// This is the system completionHandler, only set when we expect the UI to ask us to actually start so that errors can flow back to the UI
|
||||
private var startCompleter: ((Error?) -> Void)?
|
||||
|
||||
private func log(_ message: StaticString, _ args: Any...) {
|
||||
os_log(message, log: _log, args)
|
||||
}
|
||||
|
||||
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
|
||||
// There is currently no way to get initialization errors back to the UI via completionHandler here
|
||||
// `expectStart` is sent only via the UI which means we should wait for the real start command which has another completion handler the UI can intercept
|
||||
|
@ -39,16 +36,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
var key: String
|
||||
|
||||
do {
|
||||
config = proto.providerConfiguration?["config"] as! Data
|
||||
site = try Site(proto: proto)
|
||||
config = try site!.getConfig()
|
||||
} catch {
|
||||
//TODO: need a way to notify the app
|
||||
log("Failed to render config from vpn object")
|
||||
log.error("Failed to render config from vpn object")
|
||||
return completionHandler(error)
|
||||
}
|
||||
|
||||
let _site = site!
|
||||
_log = OSLog(subsystem: "net.defined.mobileNebula:\(_site.name)", category: "PacketTunnelProvider")
|
||||
|
||||
do {
|
||||
key = try _site.getKey()
|
||||
|
@ -96,14 +92,27 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
self.startNetworkMonitor()
|
||||
|
||||
if err != nil {
|
||||
self.log.error("We had an error starting up: \(err, privacy: .public)")
|
||||
return completionHandler(err!)
|
||||
}
|
||||
|
||||
|
||||
self.nebula!.start()
|
||||
self.dnUpdater.updateSingleLoop(site: self.site!, onUpdate: self.handleDNUpdate)
|
||||
|
||||
completionHandler(nil)
|
||||
})
|
||||
}
|
||||
|
||||
private func handleDNUpdate(newSite: Site) {
|
||||
do {
|
||||
self.site = newSite
|
||||
try self.nebula?.reload(String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey())
|
||||
|
||||
} catch {
|
||||
self.log.error("Got an error while updating nebula \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Sleep/wake get called aggresively and do nothing to help us here, we should locate why that is and make these work appropriately
|
||||
// override func sleep(completionHandler: @escaping () -> Void) {
|
||||
// nebula!.sleep()
|
||||
|
@ -156,7 +165,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
|
||||
override func handleAppMessage(_ data: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
||||
guard let call = try? JSONDecoder().decode(IPCRequest.self, from: data) else {
|
||||
log("Failed to decode IPCRequest from network extension")
|
||||
log.error("Failed to decode IPCRequest from network extension")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -196,7 +205,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
|
||||
if nebula == nil {
|
||||
// Respond with an empty success message in the event a command comes in before we've truly started
|
||||
log("Received command but do not have a nebula instance")
|
||||
log.warning("Received command but do not have a nebula instance")
|
||||
return completionHandler!(try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil)))
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ class IPCResponse: Codable {
|
|||
var type: IPCResponseType
|
||||
//TODO: change message to data?
|
||||
var message: JSON?
|
||||
|
||||
|
||||
init(type: IPCResponseType, message: JSON?) {
|
||||
self.type = type
|
||||
self.message = message
|
||||
|
@ -23,12 +23,12 @@ class IPCResponse: Codable {
|
|||
class IPCRequest: Codable {
|
||||
var command: String
|
||||
var arguments: JSON?
|
||||
|
||||
|
||||
init(command: String, arguments: JSON?) {
|
||||
self.command = command
|
||||
self.arguments = arguments
|
||||
}
|
||||
|
||||
|
||||
init(command: String) {
|
||||
self.command = command
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ struct CertificateInfo: Codable {
|
|||
var cert: Certificate
|
||||
var rawCert: String
|
||||
var validity: CertificateValidity
|
||||
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case cert = "Cert"
|
||||
case rawCert = "RawCert"
|
||||
|
@ -50,7 +50,7 @@ struct Certificate: Codable {
|
|||
var fingerprint: String
|
||||
var signature: String
|
||||
var details: CertificateDetails
|
||||
|
||||
|
||||
/// An empty initilizer to make error reporting easier
|
||||
init() {
|
||||
fingerprint = ""
|
||||
|
@ -69,7 +69,7 @@ struct CertificateDetails: Codable {
|
|||
var subnets: [String]
|
||||
var isCa: Bool
|
||||
var issuer: String
|
||||
|
||||
|
||||
/// An empty initilizer to make error reporting easier
|
||||
init() {
|
||||
name = ""
|
||||
|
@ -87,7 +87,7 @@ struct CertificateDetails: Codable {
|
|||
struct CertificateValidity: Codable {
|
||||
var valid: Bool
|
||||
var reason: String
|
||||
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case valid = "Valid"
|
||||
case reason = "Reason"
|
||||
|
@ -117,7 +117,7 @@ class Site: Codable {
|
|||
// Stored in manager
|
||||
var name: String
|
||||
var id: String
|
||||
|
||||
|
||||
// Stored in proto
|
||||
var staticHostmap: Dictionary<String, StaticHosts>
|
||||
var unsafeRoutes: [UnsafeRoute]
|
||||
|
@ -132,13 +132,21 @@ class Site: Codable {
|
|||
var connected: Bool? //TODO: active is a better name
|
||||
var status: String?
|
||||
var logFile: String?
|
||||
|
||||
var managed: Bool
|
||||
// The following fields are present if managed = true
|
||||
var lastManagedUpdate: String?
|
||||