Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions client/resources/original_messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,38 @@
}
}
},
"splitTunnelingSettingButtonLabel": {
"description": "Label for a button or link in settings to open the app exclusion/split tunneling configuration dialog.",
"message": "App Exclusion"
},
"splitTunnelingDialogTitle": {
"description": "Title for the dialog where users can select which apps should use the VPN.",
"message": "App Exclusion"
},
"splitTunnelingDialogSaveButton": {
"description": "Text for the 'Save' button in the app exclusion dialog.",
"message": "Save"
},
"splitTunnelingDialogCancelButton": {
"description": "Text for the 'Cancel' button in the app exclusion dialog.",
"message": "Cancel"
},
"splitTunnelingDialogDescription": {
"description": "Descriptive text explaining how the app exclusion (split tunneling) feature works.",
"message": "When App Exclusion is on, only apps selected here will use the VPN. All other apps will bypass the VPN. If no apps are selected, all apps will use the VPN."
},
"splitTunnelingDialogAllAppsLabel": {
"description": "A label shown above the list of applications in the app exclusion dialog.",
"message": "All installed apps"
},
"splitTunnelingFeatureNotSupported": {
"description": "Message shown if the app exclusion feature is not supported on the current device/platform.",
"message": "App exclusion is not supported on this device."
},
"splitTunnelingNoAppsFound": {
"description": "Message shown in the app exclusion dialog if no configurable apps are found on the device.",
"message": "No apps found to configure."
},
"yes": {
"description": "Affirmative answer to a form question.",
"message": "Yes"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.logging.Level;
Expand Down Expand Up @@ -114,6 +115,7 @@ public enum MessageData {
private NetworkConnectivityMonitor networkConnectivityMonitor;
private VpnTunnelStore tunnelStore;
private Notification.Builder notificationBuilder;
private List<String> allowedApplications;

private final IVpnTunnelService.Stub binder = new IVpnTunnelService.Stub() {
@Override
Expand All @@ -135,6 +137,11 @@ public boolean isTunnelActive(String tunnelId) {
public void initErrorReporting(String apiKey) {
VpnTunnelService.this.initErrorReporting(apiKey);
}

@Override
public void setAllowedApplications(List<String> packageNames) {
VpnTunnelService.this.setAllowedApplications(packageNames);
}
};

@Override
Expand Down Expand Up @@ -194,6 +201,12 @@ public void onDestroy() {

// Tunnel API

/** Sets the list of allowed applications for split tunneling. */
public void setAllowedApplications(List<String> packageNames) {
LOG.info(String.format(Locale.ROOT, "Setting allowed applications: %s", packageNames));
this.allowedApplications = packageNames;
}

/** This is the entry point when called from the Outline plugin, via the service IPC. */
private DetailedJsonError startTunnel(@NonNull final TunnelConfig config) {
return Errors.toDetailedJsonError(startTunnel(config, false));
Expand Down Expand Up @@ -275,8 +288,23 @@ private synchronized PlatformError startTunnel(
// TODO(fortuna): dynamically select it.
.addAddress("10.111.222.1", 24)
.addDnsServer(dnsResolver)
.setBlocking(true)
.addDisallowedApplication(this.getPackageName());
.setBlocking(true);
// Always disallow this application, as it is the VPN controller.
builder.addDisallowedApplication(this.getPackageName());

if (this.allowedApplications != null && !this.allowedApplications.isEmpty()) {
LOG.info(String.format(Locale.ROOT, "Adding %d allowed applications.", this.allowedApplications.size()));
for (String packageName : this.allowedApplications) {
try {
builder.addAllowedApplication(packageName);
LOG.fine(String.format(Locale.ROOT, "Added allowed application: %s", packageName));
} catch (PackageManager.NameNotFoundException e) {
LOG.warning(String.format(Locale.ROOT, "Package not found, cannot add to allowed list: %s", packageName));
}
}
} else {
LOG.info("No allowed applications configured, all applications will use the VPN.");
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setMetered(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.VpnService;
import android.os.IBinder;
import android.os.RemoteException;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
Expand Down Expand Up @@ -62,7 +66,8 @@ public enum Action {
IS_RUNNING("isRunning"),
INIT_ERROR_REPORTING("initializeErrorReporting"),
REPORT_EVENTS("reportEvents"),
QUIT("quitApplication");
QUIT("quitApplication"),
GET_INSTALLED_APPS("getInstalledApps");

private final static Map<String, Action> actions = new HashMap<>();
static {
Expand Down Expand Up @@ -91,9 +96,13 @@ public boolean is(final String action) {
private static class StartVpnRequest {
public final JSONArray args;
public final CallbackContext callback;
public StartVpnRequest(JSONArray args, CallbackContext callback) {
// Store allowedApplications separately as JSONArray doesn't directly support List<String>
public final List<String> allowedApplications;

public StartVpnRequest(JSONArray args, CallbackContext callback, List<String> allowedApplications) {
this.args = args;
this.callback = callback;
this.allowedApplications = allowedApplications;
}
}

Expand Down Expand Up @@ -173,11 +182,24 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
// Prepare the VPN before spawning a new thread. Fall through if it's already prepared.
try {
if (!prepareVpnService()) {
startVpnRequest = new StartVpnRequest(args, callbackContext);
List<String> allowedApplications = null;
if (args.length() > 3) {
JSONArray allowedAppsJson = args.optJSONArray(3);
if (allowedAppsJson != null) {
allowedApplications = new ArrayList<>();
for (int i = 0; i < allowedAppsJson.length(); ++i) {
allowedApplications.add(allowedAppsJson.getString(i));
}
}
}
startVpnRequest = new StartVpnRequest(args, callbackContext, allowedApplications);
return true;
}
} catch (ActivityNotFoundException e) {
sendActionResult(callbackContext, new PlatformError(Platerrors.InternalError, e.toString()));
} catch (JSONException e) {
LOG.log(Level.SEVERE, "Failed to parse allowedApplications from JSONArray", e);
sendActionResult(callbackContext, new PlatformError(Platerrors.InvalidOutlineInvite, e.toString()));
return true;
}
}
Expand Down Expand Up @@ -209,7 +231,17 @@ private void executeAsync(
final String tunnelId = args.getString(0);
final String serverName = args.getString(1);
final String transportConfig = args.getString(2);
sendActionResult(callback, startVpnTunnel(tunnelId, transportConfig, serverName));
List<String> allowedApplications = null;
if (args.length() > 3) {
JSONArray allowedAppsJson = args.optJSONArray(3);
if (allowedAppsJson != null) {
allowedApplications = new ArrayList<>();
for (int i = 0; i < allowedAppsJson.length(); ++i) {
allowedApplications.add(allowedAppsJson.getString(i));
}
}
}
sendActionResult(callback, startVpnTunnel(tunnelId, transportConfig, serverName, allowedApplications));
} else if (Action.STOP.is(action)) {
final String tunnelId = args.getString(0);
LOG.info(String.format(Locale.ROOT, "Stopping VPN tunnel %s", tunnelId));
Expand All @@ -230,6 +262,8 @@ private void executeAsync(
final String uuid = args.getString(0);
SentryErrorReporter.send(uuid);
callback.success();
} else if (Action.GET_INSTALLED_APPS.is(action)) {
callback.success(getInstalledApplications());
} else {
throw new IllegalArgumentException(
String.format(Locale.ROOT, "Unexpected action %s", action));
Expand Down Expand Up @@ -272,9 +306,20 @@ public void onActivityResult(int request, int result, Intent data) {
}

private DetailedJsonError startVpnTunnel(
final String tunnelId, final String transportConfig, final String serverName
final String tunnelId, final String transportConfig, final String serverName, @Nullable final List<String> allowedApplications
) throws RemoteException {
LOG.info(String.format(Locale.ROOT, "Starting VPN tunnel %s for server %s", tunnelId, serverName));
if (vpnTunnelService == null) {
return Errors.toDetailedJsonError(new PlatformError(Platerrors.IllegalState, "VPN service not connected"));
}
if (allowedApplications != null && !allowedApplications.isEmpty()) {
LOG.info(String.format(Locale.ROOT, "Setting %d allowed applications", allowedApplications.size()));
vpnTunnelService.setAllowedApplications(allowedApplications);
} else {
// Explicitly clear if null or empty to reset previous settings
LOG.info("Clearing allowed applications list.");
vpnTunnelService.setAllowedApplications(new ArrayList<>());
}
final TunnelConfig tunnelConfig = new TunnelConfig();
tunnelConfig.id = tunnelId;
tunnelConfig.name = serverName;
Expand Down Expand Up @@ -354,4 +399,28 @@ private void sendActionResult(final CallbackContext callback, @Nullable Detailed
callback.error(error.errorJson);
}
}

private JSONArray getInstalledApplications() {
PackageManager pm = cordova.getActivity().getPackageManager();
List<ApplicationInfo> packages = pm.getInstalledApplications(PackageManager.GET_META_DATA);
JSONArray apps = new JSONArray();
for (ApplicationInfo appInfo : packages) {
try {
// Filter out system apps to provide a cleaner list to the user.
// Also filter out the Outline app itself.
if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ||
appInfo.packageName.equals(this.cordova.getActivity().getPackageName())) {
continue;
}
JSONObject app = new JSONObject();
app.put("packageName", appInfo.packageName);
app.put("label", pm.getApplicationLabel(appInfo).toString());
apps.put(app);
} catch (JSONException e) {
LOG.log(Level.WARNING, "Failed to serialize app info to JSON", e);
}
}
LOG.info(String.format(Locale.ROOT, "Found %d installed non-system applications.", apps.length()));
return apps;
}
}
Loading
Loading