-
Notifications
You must be signed in to change notification settings - Fork 452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow the user to control which apps can use the VPN (WIP) #56
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,8 @@ | |
import android.os.Handler; | ||
import android.os.Looper; | ||
|
||
import android.util.Log; | ||
|
||
import android.Manifest; | ||
import android.webkit.MimeTypeMap; | ||
|
||
|
@@ -86,17 +88,26 @@ public class App extends Application { | |
|
||
public DnsConfig dns = new DnsConfig(this); | ||
public DnsConfig getDnsConfigObj() { return this.dns; } | ||
public AppsConfig acfg = null; | ||
|
||
@Override public void onCreate() { | ||
super.onCreate(); | ||
Log.v("com.tailscale.ipn", "Loading apps: " + System.currentTimeMillis()/1000L); | ||
try { | ||
acfg = new AppsConfig(this, getEncryptedPrefs()); | ||
//acfg.printConfig(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommend removing leftover debugging code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I left them there because I was worried of the impact in the starting time on the App depending on the number of apps, as you pointed below loading the apps at the start is far from optimal. |
||
} catch (Exception e) { | ||
Log.e("com.tailscale.ipn", "exception", e); | ||
} | ||
Log.v("com.tailscale.ipn", "Apps loaded: " + System.currentTimeMillis()/1000L); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Admittedly this only appears in adb logcat, but if it was just here for your own development recommend removing it (and the one above) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will remove it, thanks. |
||
|
||
// Load and initialize the Go library. | ||
Gio.init(this); | ||
registerNetworkCallback(); | ||
|
||
createNotificationChannel(NOTIFY_CHANNEL_ID, "Notifications", NotificationManagerCompat.IMPORTANCE_DEFAULT); | ||
createNotificationChannel(STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW); | ||
createNotificationChannel(FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT); | ||
|
||
} | ||
|
||
private void registerNetworkCallback() { | ||
|
@@ -123,6 +134,30 @@ public void onLinkPropertiesChanged(Network network, LinkProperties linkProperti | |
}); | ||
} | ||
|
||
public void setupApp(String packageName, boolean allowed){ | ||
acfg.setApp(packageName, allowed); | ||
} | ||
|
||
public int getTotalApps(){ | ||
return acfg.getTotalApps(); | ||
} | ||
|
||
public String getPackageLabel(int i){ | ||
return acfg.getPackageLabel(i); | ||
} | ||
|
||
public String getPackageName(int i){ | ||
return acfg.getPackageName(i); | ||
} | ||
|
||
public boolean appIsAllowed(int i){ | ||
return acfg.appIsAllowed(i); | ||
} | ||
|
||
public String getIcon(String packageName) { | ||
return acfg.getAppIcon(packageName); | ||
} | ||
|
||
public void startVPN() { | ||
Intent intent = new Intent(this, IPNService.class); | ||
intent.setAction(IPNService.ACTION_CONNECT); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,271 @@ | ||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I copy&pasted that, I will fix it. |
||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package com.tailscale.ipn; | ||
|
||
import android.content.Context; | ||
import android.Manifest; | ||
import android.content.SharedPreferences; | ||
import android.util.Base64; | ||
import android.util.Log; | ||
import android.net.VpnService; | ||
import java.lang.reflect.Method; | ||
import android.content.pm.PackageManager; | ||
import android.content.pm.ApplicationInfo; | ||
import android.graphics.drawable.Drawable; | ||
import android.graphics.Bitmap; | ||
import android.graphics.Canvas; | ||
import java.io.ByteArrayOutputStream; | ||
import java.util.Arrays; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.lang.StringBuilder; | ||
|
||
public class AppsConfig { | ||
private class AppConfig { | ||
boolean allowed; | ||
String icon; | ||
String packageName; | ||
String label; | ||
} | ||
|
||
private static final String PREFS_NAME = "disallowedApps"; | ||
|
||
private Context ctx; | ||
private List<AppConfig> config; | ||
private SharedPreferences sp; | ||
|
||
public AppsConfig(Context ctx, SharedPreferences sp) { | ||
this.ctx = ctx; | ||
this.sp = sp; | ||
PackageManager pm = this.ctx.getPackageManager(); | ||
|
||
config = new ArrayList<AppConfig>(); | ||
List<ApplicationInfo> instApps = getInstalledApps(pm); | ||
|
||
for (int i = 0;i < instApps.size(); i++) { | ||
ApplicationInfo appinfo = instApps.get(i); | ||
AppConfig ac = new AppConfig(); | ||
ac.packageName = appinfo.packageName; | ||
ac.allowed = true; | ||
ac.icon = retrieveAppIcon(pm,appinfo); | ||
CharSequence label = pm.getApplicationLabel(appinfo); | ||
if(label != null){ | ||
ac.label = label.toString(); | ||
} else { | ||
ac.label = ac.packageName; | ||
} | ||
config.add(ac); | ||
} | ||
readAppsConfig(); | ||
} | ||
|
||
public VpnService.Builder build(VpnService.Builder b){ | ||
if(b == null) | ||
return b; | ||
|
||
for (int i = 0;i < config.size(); i++) { | ||
AppConfig ac = config.get(i); | ||
if(ac.allowed == false){ | ||
try { | ||
b.addDisallowedApplication(ac.packageName); | ||
} catch (PackageManager.NameNotFoundException e) { | ||
|
||
} | ||
} | ||
} | ||
|
||
return b; | ||
} | ||
|
||
public int getTotalApps(){ | ||
return config.size(); | ||
} | ||
|
||
public boolean appIsAllowed(int i){ | ||
AppConfig ac = config.get(i); | ||
if(ac != null){ | ||
return ac.allowed; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
public String getPackageLabel(int i){ | ||
AppConfig ac = config.get(i); | ||
if(ac != null){ | ||
return ac.label; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public String getPackageName(int i){ | ||
AppConfig ac = config.get(i); | ||
if(ac != null){ | ||
return ac.packageName; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
// Get the icon as a png encoded as a base64 string | ||
public String getAppIcon(String packageName) { | ||
if(config == null) | ||
return null; | ||
|
||
for(int i = 0;i < config.size(); i++) { | ||
AppConfig ac = config.get(i); | ||
if(ac.packageName.equals(packageName)){ | ||
return ac.icon; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
// Allow,Disallow application to connect to the VPN | ||
public void setApp(String packageName, boolean allowed) { | ||
if(config == null || packageName == null || packageName.length() == 0) | ||
return; | ||
|
||
Log.d("com.tailscale.ipn", "Disabling " + packageName); | ||
AppConfig ac = null; | ||
|
||
Log.d("com.tailscale.ipn", "size: " + config.size()); | ||
for (int i = 0;i < config.size(); i++) { | ||
ac = config.get(i); | ||
if(ac.packageName.equals(packageName) && | ||
ac.allowed != allowed){ | ||
//Log.d("com.tailscale.ipn", packageName); | ||
ac.allowed = allowed; | ||
config.set(i, ac); | ||
writeAppsConfig(); | ||
return; | ||
} | ||
} | ||
} | ||
|
||
//returns a String like packageName;packageName;... or "" | ||
private String getDisallowedApps() { | ||
StringBuilder sb = new StringBuilder(); | ||
|
||
for (int i = 0;i < config.size(); i++) { | ||
AppConfig ac = config.get(i); | ||
if(ac.allowed == false){ | ||
sb.append(ac.packageName); | ||
sb.append(";"); | ||
} | ||
} | ||
|
||
//Log.d("com.tailscale.ipn", sb.toString()); | ||
return sb.toString(); | ||
} | ||
|
||
// persists blacklisted apps in the settings | ||
public void writeAppsConfig() { | ||
//Log.d("com.tailscale.ipn", "writeAppsConfig"); | ||
if(sp == null) | ||
return; | ||
//Log.d("com.tailscale.ipn", getDisallowedApps()); | ||
sp.edit().putString(PREFS_NAME, getDisallowedApps()).apply(); | ||
} | ||
|
||
// read apps blacklisted in the settings | ||
private void readAppsConfig() { | ||
if(sp == null) | ||
return; | ||
|
||
String disallowedApps = sp.getString(PREFS_NAME, ""); | ||
|
||
if(disallowedApps.length() != 0){ | ||
List<String> strapps = | ||
Arrays.asList(disallowedApps.split(";")); | ||
|
||
if(strapps.size() > 0){ | ||
for(int i = 0; i < strapps.size(); i++){ | ||
for (int j = 0;j < config.size(); j++) { | ||
AppConfig ac = config.get(j); | ||
if(ac.packageName.equals(strapps.get(i))){ | ||
ac.allowed = false; | ||
config.set(j, ac); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Just for debug purposes prints all the apps and state | ||
public void printConfig() { | ||
if (config != null) { | ||
for (int i = 0;i < config.size(); i++) { | ||
AppConfig ac = config.get(i); | ||
Log.v("com.tailscale.ipn", | ||
ac.packageName + " " + ac.allowed); | ||
Log.v("com.tailscale.ipn", ac.icon); | ||
} | ||
} | ||
} | ||
|
||
// Retrieves the icon in png encoded in base64 from android using a PackageManager | ||
private String retrieveAppIcon(PackageManager pm, ApplicationInfo info) { | ||
Drawable d = pm.getApplicationIcon(info); | ||
Bitmap mutableBitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.ARGB_8888); | ||
Canvas canvas = new Canvas(mutableBitmap); | ||
d.setBounds(0, 0, 48, 48); | ||
d.draw(canvas); | ||
//Bitmap to png to ByteArrayOutputStream to byte[] to base64 String | ||
ByteArrayOutputStream baos = new ByteArrayOutputStream(); | ||
//100 is the quality, but it is ignored for PNG | ||
mutableBitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); | ||
//Return the png encoded as a base64 string | ||
return Base64.encodeToString(baos.toByteArray(),Base64.DEFAULT); | ||
} | ||
|
||
// Returns a sorted list of installed apps with access to The Internet | ||
private List<ApplicationInfo> getInstalledApps(PackageManager pm) { | ||
if(this.ctx == null) | ||
return null; | ||
|
||
if(pm == null) | ||
pm = this.ctx.getPackageManager(); | ||
|
||
// Initialize variables | ||
List<ApplicationInfo> ret = null; | ||
List<ApplicationInfo> insApps = | ||
pm.getInstalledApplications(PackageManager.GET_META_DATA); | ||
|
||
// This is done in the OpenVPN code | ||
int androidSystemUid = 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like Settings_Allowed_Apps.java from OpenVPN, which is GPL. This app is BSD licensed. Did most of this file come from OpenVPN? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not the whole class, but the logic of that method (getInstalledApps) is doing the same basically, not that there are much more alternative ways of doing it that I am aware of anyway. Also a TODO for me here, I guess it makes sense to filter tailscale from the list of apps itself, it doesn't really makes much sense to keep it in the list. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are the implications of this in regards to implementation? Should I be looking to start from scratch to avoid license issues? |
||
ret = new ArrayList<ApplicationInfo>(); | ||
|
||
try { | ||
ApplicationInfo system = | ||
pm.getApplicationInfo("android", PackageManager.GET_META_DATA); | ||
if(system != null){ | ||
ret.add(system); | ||
androidSystemUid = system.uid; | ||
} | ||
} catch (PackageManager.NameNotFoundException e) { | ||
// it's ok if the "android" app doesn't exists | ||
} | ||
|
||
for (int i = 0;i < insApps.size(); i++){ | ||
ApplicationInfo app = insApps.get(i); | ||
if (pm.checkPermission( | ||
Manifest.permission.INTERNET, | ||
app.packageName) | ||
== PackageManager.PERMISSION_GRANTED | ||
&& app.uid != androidSystemUid){ | ||
if(app.packageName != "android") | ||
ret.add(app); | ||
} | ||
} | ||
|
||
Collections.sort(ret, new ApplicationInfo.DisplayNameComparator(pm)); | ||
return ret; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know if adding this will trigger a notification to the user when they install it from the Play Store? If you don't know that is ok, I can build a binary on the Internal Testing track and see what it does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No idea about this one sorry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are guidelines available with a required form to submit to be accepted into the Play Store with this permission, but other VPN apps are currently active with it declared, so I see no reason for it to be rejected. I recall no notifications or popups when installing or using said apps on Android 13, and the fact that it is tied to approval may mean there is no need for a user-facing warning. Regardless, it unfortunately seems to be necessary in order to add split tunneling as none of the package queries are broad enough.