From 37758d4a019ee3c1f8b77e399980f61b9c7880f3 Mon Sep 17 00:00:00 2001 From: John Maguire Date: Fri, 18 Nov 2022 14:34:45 -0500 Subject: [PATCH] Request VPN permissions on site start (#92) Previously VPN permissions were requested when the UI was loaded. If the user denied the permissions it would have to be force stopped and reopened to get another permission request grant. Additionally, when requesting VPN permissions Android will kill any other running VPN service. This avoids that behavior unless a site is explicitly started. Also disables the app from showing up in the "Always On" settings. --- android/app/src/main/AndroidManifest.xml | 3 +- .../net/defined/mobile_nebula/MainActivity.kt | 76 ++++++++----------- lib/screens/MainScreen.dart | 37 --------- nebula/go.mod | 2 + nebula/go.sum | 3 + 5 files changed, 38 insertions(+), 83 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ca5782e..54cbbdf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -25,6 +25,8 @@ + - when(call.method) { - "android.requestPermissions" -> androidPermissions(result) "android.registerActiveSite" -> registerActiveSite(result) "nebula.parseCerts" -> nebulaParseCerts(call, result) @@ -242,25 +244,16 @@ class MainActivity: FlutterActivity() { return result.error("required_argument", "id is a required argument", null) } - val siteContainer: SiteContainer = sites!!.getSite(id!!) ?: return result.error("unknown_site", "No site with that id exists", null) + startingSiteContainer = sites!!.getSite(id!!) ?: return result.error("unknown_site", "No site with that id exists", null) + startingSiteContainer!!.updater.setState(true, "Initializing...") - siteContainer.updater.setState(true, "Initializing...") - - var intent = VpnService.prepare(this) + startResult = result + val 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) - intent.putExtra("id", siteContainer.site.id) startActivityForResult(intent, VPN_START_CODE) - } else { - 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) + onActivityResult(VPN_START_CODE, Activity.RESULT_OK, null) } - - result.success(null) } private fun stopSite() { @@ -400,45 +393,38 @@ class MainActivity: FlutterActivity() { outMessenger?.send(msg) } - private fun androidPermissions(result: MethodChannel.Result) { - val intent = VpnService.prepare(this) - if (intent != null) { - permResult = result - return startActivityForResult(intent, VPN_PERMISSIONS_CODE) - } - - // We already have the permission - result.success(null) - } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { // This is where activity results come back to us (startActivityForResult) - if (requestCode == VPN_PERMISSIONS_CODE && permResult != null) { - // We are processing a response for vpn permissions and the UI is waiting for feedback - //TODO: unlikely we ever register multiple attempts but this could be a trouble spot if we did - val result = permResult!! - permResult = null - if (resultCode == Activity.RESULT_OK) { - return result.success(null) + if (requestCode == VPN_START_CODE) { + // If we are processing a result for VPN permissions and don't get them, let the UI know + val result = startResult!! + val siteContainer = startingSiteContainer!! + startResult = null + startingSiteContainer = null + if (resultCode != Activity.RESULT_OK) { + // The user did not grant permissions + siteContainer.updater.setState(false, "Disconnected") + return result.error("permissions", "Please grant VPN permissions to the app when requested", null) } - //NOTE: flutter side doesn't care about the message currently, only the code - 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) - startService(data) + // Start the VPN service + val intent = Intent(this, NebulaVpnService::class.java).apply { + putExtra("path", siteContainer.site.path) + putExtra("id", siteContainer.site.id) + } + startService(intent) if (outMessenger == null) { - bindService(data, connection, 0) + bindService(intent, connection, 0) } - return + + return result.success(null) } // The file picker needs us to super super.onActivityResult(requestCode, resultCode, data) } + /** Defines callbacks for service binding, passed to bindService() */ private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { diff --git a/lib/screens/MainScreen.dart b/lib/screens/MainScreen.dart index 9dfb02b..06a9cc8 100644 --- a/lib/screens/MainScreen.dart +++ b/lib/screens/MainScreen.dart @@ -284,43 +284,6 @@ class _MainScreenState extends State { } _loadSites() async { - if (Platform.isAndroid) { - try { - await platform.invokeMethod("android.requestPermissions"); - } on PlatformException catch (err) { - if (err.code == "PERMISSIONS") { - setState(() { - error = [ - Text("Permissions Required", style: TextStyle(fontWeight: FontWeight.bold)), - Text( - "VPN permissions are required for nebula to run, click the button below request and accept the appropriate permissions.", - textAlign: TextAlign.center), - ElevatedButton( - onPressed: () { - error = null; - _loadSites(); - }, - child: Text("Request Permissions")), - ]; - }); - } else { - setState(() { - error = [ - Text("Unknown Error", style: TextStyle(fontWeight: FontWeight.bold)), - Text(err.message ?? 'An unknown error occurred', textAlign: TextAlign.center) - ]; - }); - } - } catch (err) { - setState(() { - error = [ - Text("Unknown Error", style: TextStyle(fontWeight: FontWeight.bold)), - Text(err.toString(), textAlign: TextAlign.center) - ]; - }); - } - } - //TODO: This can throw, we need to show an error dialog Map rawSites = jsonDecode(await platform.invokeMethod('listSites')); bool hasErrors = false; diff --git a/nebula/go.mod b/nebula/go.mod index e2061c7..0b18d44 100644 --- a/nebula/go.mod +++ b/nebula/go.mod @@ -34,8 +34,10 @@ require ( github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect github.com/vishvananda/netlink v1.1.0 // indirect github.com/vishvananda/netns v0.0.1 // indirect + golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect golang.org/x/mod v0.7.0 // indirect golang.org/x/net v0.2.0 // indirect + golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.2.0 // indirect golang.org/x/term v0.2.0 // indirect golang.org/x/tools v0.3.0 // indirect diff --git a/nebula/go.sum b/nebula/go.sum index 7ab9647..319d941 100644 --- a/nebula/go.sum +++ b/nebula/go.sum @@ -275,6 +275,8 @@ golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPI golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20221110043201-43a038452099 h1:aIu0lKmfdgtn2uTj7JI2oN4TUrQvgB+wzTPO23bCKt8= +golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -340,6 +342,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=