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.
This commit is contained in:
John Maguire 2022-11-18 14:34:45 -05:00 committed by GitHub
parent 9dd5b9cad9
commit 37758d4a01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 38 additions and 83 deletions

View File

@ -25,6 +25,8 @@
<intent-filter> <intent-filter>
<action android:name="android.net.VpnService"/> <action android:name="android.net.VpnService"/>
</intent-filter> </intent-filter>
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="false"/>
</service> </service>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
@ -48,7 +50,6 @@
<data android:scheme="https"/> <data android:scheme="https"/>
</intent-filter> </intent-filter>
</activity> </activity>
<receiver android:name=".ShareReceiver" android:exported="false"/>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"

View File

@ -21,20 +21,23 @@ import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
const val TAG = "nebula" const val TAG = "nebula"
const val VPN_PERMISSIONS_CODE = 0x0F
const val VPN_START_CODE = 0x10 const val VPN_START_CODE = 0x10
const val CHANNEL = "net.defined.mobileNebula/NebulaVpnService" const val CHANNEL = "net.defined.mobileNebula/NebulaVpnService"
const val UPDATE_WORKER = "dnUpdater" const val UPDATE_WORKER = "dnUpdater"
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
private var ui: MethodChannel? = null
private var inMessenger: Messenger? = Messenger(IncomingHandler()) private var inMessenger: Messenger? = Messenger(IncomingHandler())
private var outMessenger: Messenger? = null private var outMessenger: Messenger? = null
private var apiClient: APIClient? = null private var apiClient: APIClient? = null
private var sites: Sites? = null private var sites: Sites? = null
private var permResult: MethodChannel.Result? = null
private var ui: MethodChannel? = null // When starting a site we may need to request VPN permissions. These variables help us
// maintain state while waiting for a permission result.
private var startResult: MethodChannel.Result? = null
private var startingSiteContainer: SiteContainer? = null
private var activeSiteId: String? = null private var activeSiteId: String? = null
@ -58,7 +61,6 @@ class MainActivity: FlutterActivity() {
ui = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) ui = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
ui!!.setMethodCallHandler { call, result -> ui!!.setMethodCallHandler { call, result ->
when(call.method) { when(call.method) {
"android.requestPermissions" -> androidPermissions(result)
"android.registerActiveSite" -> registerActiveSite(result) "android.registerActiveSite" -> registerActiveSite(result)
"nebula.parseCerts" -> nebulaParseCerts(call, result) "nebula.parseCerts" -> nebulaParseCerts(call, result)
@ -242,25 +244,16 @@ class MainActivity: FlutterActivity() {
return result.error("required_argument", "id is a required argument", null) 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...") startResult = result
val intent = VpnService.prepare(this)
var intent = VpnService.prepare(this)
if (intent != null) { 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) startActivityForResult(intent, VPN_START_CODE)
} else { } else {
intent = Intent(this, NebulaVpnService::class.java) onActivityResult(VPN_START_CODE, Activity.RESULT_OK, null)
intent.putExtra("path", siteContainer.site.path)
intent.putExtra("id", siteContainer.site.id)
onActivityResult(VPN_START_CODE, Activity.RESULT_OK, intent)
} }
result.success(null)
} }
private fun stopSite() { private fun stopSite() {
@ -400,45 +393,38 @@ class MainActivity: FlutterActivity() {
outMessenger?.send(msg) 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// This is where activity results come back to us (startActivityForResult) // This is where activity results come back to us (startActivityForResult)
if (requestCode == VPN_PERMISSIONS_CODE && permResult != null) { if (requestCode == VPN_START_CODE) {
// We are processing a response for vpn permissions and the UI is waiting for feedback // If we are processing a result for VPN permissions and don't get them, let the UI know
//TODO: unlikely we ever register multiple attempts but this could be a trouble spot if we did val result = startResult!!
val result = permResult!! val siteContainer = startingSiteContainer!!
permResult = null startResult = null
if (resultCode == Activity.RESULT_OK) { startingSiteContainer = null
return result.success(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 // Start the VPN service
return result.error("PERMISSIONS", "User did not grant permission", null) val intent = Intent(this, NebulaVpnService::class.java).apply {
putExtra("path", siteContainer.site.path)
} else if (requestCode == VPN_START_CODE) { putExtra("id", siteContainer.site.id)
// We are processing a response for permissions while starting the VPN }
// (or reusing code in the event we already have perms) startService(intent)
startService(data)
if (outMessenger == null) { if (outMessenger == null) {
bindService(data, connection, 0) bindService(intent, connection, 0)
} }
return
return result.success(null)
} }
// The file picker needs us to super // The file picker needs us to super
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
} }
/** Defines callbacks for service binding, passed to bindService() */ /** Defines callbacks for service binding, passed to bindService() */
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {

View File

@ -284,43 +284,6 @@ class _MainScreenState extends State<MainScreen> {
} }
_loadSites() async { _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 //TODO: This can throw, we need to show an error dialog
Map<String, dynamic> rawSites = jsonDecode(await platform.invokeMethod('listSites')); Map<String, dynamic> rawSites = jsonDecode(await platform.invokeMethod('listSites'));
bool hasErrors = false; bool hasErrors = false;

View File

@ -34,8 +34,10 @@ require (
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
github.com/vishvananda/netlink v1.1.0 // indirect github.com/vishvananda/netlink v1.1.0 // indirect
github.com/vishvananda/netns v0.0.1 // 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/mod v0.7.0 // indirect
golang.org/x/net v0.2.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/sys v0.2.0 // indirect
golang.org/x/term v0.2.0 // indirect golang.org/x/term v0.2.0 // indirect
golang.org/x/tools v0.3.0 // indirect golang.org/x/tools v0.3.0 // indirect

View File

@ -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/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-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-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.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.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 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-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.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 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-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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=