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:
parent
9dd5b9cad9
commit
37758d4a01
|
@ -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"
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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=
|
||||||
|
|
Loading…
Reference in New Issue