outline-apps
outline-apps copied to clipboard
Support white list
It would be nice if Outline can support white list when directing incoming traffic. This probably can be done by user on the client side with a default white list managed by server administrator.
URLs/IPs on the white list will be connected directly from the client bypassing the VPN server.
This is useful because sometimes we have to switch VPN on and off in order to visit different websites.
We had a go at this. Here is a patch, which you are welcome to use as you wish.
We found we had to set a whitelist for Android, and a blacklist for iOS (which we did from outside the plugin). We didn't try to support any other platforms.
I guess we focused on IPv4 and ignored IPv6. Sorry, but at least this may be a pointer in the right direction!
diff -u -r ./cordova-plugin-outline/android/java/org/outline/OutlinePlugin.java /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/android/java/org/outline/OutlinePlugin.java
--- ./cordova-plugin-outline/android/java/org/outline/OutlinePlugin.java 2019-03-04 10:58:07.000000000 +0800
+++ /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/android/java/org/outline/OutlinePlugin.java 2019-12-09 13:49:30.000000000 +0800
@@ -25,11 +25,13 @@
import android.os.IBinder;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Pair;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -48,6 +50,7 @@
// Actions supported by this plugin.
public enum Action {
+ SET_IP_WHITELIST("setIPWhitelist"),
START("start"),
STOP("stop"),
ON_STATUS_CHANGE("onStatusChange"),
@@ -129,13 +132,14 @@
private static final int REQUEST_CODE_PREPARE_VPN = 100;
private static final int RESULT_OK = -1; // Standard activity result: operation succeeded.
private static final HashSet<String> CONNECTION_INSTANCE_ACTIONS =
- new HashSet<String>(Arrays.asList(Action.START.value, Action.STOP.value,
+ new HashSet<String>(Arrays.asList(Action.SET_IP_WHITELIST.value, Action.START.value, Action.STOP.value,
Action.IS_RUNNING.value, Action.ON_STATUS_CHANGE.value, Action.IS_REACHABLE.value));
private VpnTunnelService vpnTunnelService = null;
private String startRequestConnectionId = null;
private JSONObject startRequestConfig = null;
private Map<Pair<String, String>, CallbackContext> listeners = new ConcurrentHashMap();
+ private List<String> ipWhitelist = null;
// Class to bind to VpnTunnelService.
private ServiceConnection serviceConnection =
@@ -157,6 +161,7 @@
Context context = getBaseContext();
IntentFilter broadcastFilter = new IntentFilter();
+ broadcastFilter.addAction(Action.SET_IP_WHITELIST.value);
broadcastFilter.addAction(Action.START.value);
broadcastFilter.addAction(Action.STOP.value);
broadcastFilter.addAction(Action.ON_STATUS_CHANGE.value);
@@ -214,7 +219,15 @@
public void run() {
try {
// Connection instance actions
- if (Action.START.is(action)) {
+ if (Action.SET_IP_WHITELIST.is(action)) {
+ //LOG.debug("[OutlinePlugin.java] Received whitelist with length " + args.getJSONArray(1).length() + "");
+ JSONArray jsonWhitelist = args.getJSONArray(1);
+ ArrayList<String> _ipWhitelist = new ArrayList<String>();
+ for (int i = 0; i < jsonWhitelist.length(); i++) {
+ _ipWhitelist.add(jsonWhitelist.getString(i));
+ }
+ ipWhitelist = _ipWhitelist;
+ } else if (Action.START.is(action)) {
// Set instance variables in case we need to start the VPN service from
// onActivityResult
startRequestConnectionId = connectionId;
@@ -307,6 +320,7 @@
onVpnTunnelServiceNotBound(Action.START, startRequestConnectionId);
return;
}
+ vpnTunnelService.setIPWhitelist(ipWhitelist);
vpnTunnelService.startConnection(startRequestConnectionId, startRequestConfig);
}
diff -u -r ./cordova-plugin-outline/android/java/org/outline/vpn/VpnTunnel.java /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/android/java/org/outline/vpn/VpnTunnel.java
--- ./cordova-plugin-outline/android/java/org/outline/vpn/VpnTunnel.java 2019-03-04 10:58:07.000000000 +0800
+++ /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/android/java/org/outline/vpn/VpnTunnel.java 2020-01-08 10:54:04.000000000 +0800
@@ -21,6 +21,7 @@
import android.net.VpnService;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -53,6 +54,7 @@
private String dnsResolverAddress;
private ParcelFileDescriptor tunFd;
private Thread tun2socksThread = null;
+ private List<String> ipWhitelist = null;
/**
* Constructor.
@@ -67,6 +69,10 @@
this.vpnService = vpnService;
}
+ public void setIPWhitelist(List<String> _ipWhitelist) {
+ ipWhitelist = _ipWhitelist;
+ }
+
/**
* Establishes a system-wide VPN that routes all device traffic to its TUN interface. Randomly
* selects between OpenDNS and Dyn resolvers to set the VPN's DNS resolvers.
@@ -93,8 +99,10 @@
}
// In absence of an API to remove routes, instead of adding the default route (0.0.0.0/0),
// retrieve the list of subnets that excludes those reserved for special use.
- final ArrayList<Subnet> reservedBypassSubnets = getReservedBypassSubnets();
+ //final ArrayList<Subnet> reservedBypassSubnets = getReservedBypassSubnets();
+ final ArrayList<Subnet> reservedBypassSubnets = getIPWhitelistSubnets();
for (Subnet subnet : reservedBypassSubnets) {
+ LOG.info("Adding whitelist route: " + subnet.address + "/" + subnet.prefix);
builder.addRoute(subnet.address, subnet.prefix);
}
tunFd = builder.establish();
@@ -197,6 +205,20 @@
return subnets;
}
+ /* Returns a list of whitelisted IP ranges. */
+ private ArrayList<Subnet> getIPWhitelistSubnets() {
+ final List<String> subnetStrings = ipWhitelist;
+ ArrayList<Subnet> subnets = new ArrayList<>(subnetStrings.size());
+ for (final String subnetString : subnetStrings) {
+ try {
+ subnets.add(Subnet.parse(subnetString));
+ } catch (Exception e) {
+ LOG.warning(String.format(Locale.ROOT, "Failed to parse subnet: %s", subnetString));
+ }
+ }
+ return subnets;
+ }
+
/* Represents an IP subnet. */
private static class Subnet {
public String address;
diff -u -r ./cordova-plugin-outline/android/java/org/outline/vpn/VpnTunnelService.java /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/android/java/org/outline/vpn/VpnTunnelService.java
--- ./cordova-plugin-outline/android/java/org/outline/vpn/VpnTunnelService.java 2019-12-09 11:27:21.000000000 +0800
+++ /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/android/java/org/outline/vpn/VpnTunnelService.java 2020-01-08 10:54:04.000000000 +0800
@@ -38,6 +38,7 @@
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
+import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -69,6 +70,7 @@
private NetworkConnectivityMonitor networkConnectivityMonitor;
private VpnConnectionStore connectionStore;
private Notification.Builder notificationBuilder;
+ private List<String> ipWhitelist = null;
public class LocalBinder extends Binder {
public VpnTunnelService getService() {
@@ -130,6 +132,10 @@
// Connection API
+ public void setIPWhitelist(List<String> _ipWhitelist) {
+ ipWhitelist = _ipWhitelist;
+ }
+
/**
* Establishes a system-wide VPN connected to a remote Shadowsocks server. All device traffic is
* routed as follows: |VPN TUN interface| <-> |tun2socks| <-> |local Shadowsocks server| <->
@@ -182,6 +188,7 @@
vpnTunnel.disconnectTunnel();
} else {
// Only establish the VPN if this is not a connection restart.
+ vpnTunnel.setIPWhitelist(ipWhitelist);
if (!vpnTunnel.establishVpn()) {
LOG.severe("Failed to establish the VPN");
onVpnStartFailure(OutlinePlugin.ErrorCode.VPN_START_FAILURE);
diff -u -r ./cordova-plugin-outline/apple/src/OutlinePlugin.swift /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/apple/src/OutlinePlugin.swift
--- ./cordova-plugin-outline/apple/src/OutlinePlugin.swift 2019-01-18 10:54:31.000000000 +0800
+++ /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/apple/src/OutlinePlugin.swift 2019-12-09 13:49:30.000000000 +0800
@@ -21,6 +21,8 @@
class OutlinePlugin: CDVPlugin {
private enum Action {
+ static let setIPWhitelist = "setIPWhitelist"
+ static let setIPBlacklist = "setIPBlacklist"
static let start = "start"
static let stop = "stop"
static let onStatusChange = "onStatusChange"
@@ -60,6 +62,24 @@
#endif
}
+ func setIPWhitelist(_ command: CDVInvokedUrlCommand) {
+ DDLogInfo("[OutlinePlugin.swift] Receiving ipWhitelist")
+ guard let ipWhitelist = command.argument(at: 1) as? [String] else {
+ return sendError("Missing ipWhitelist", callbackId: command.callbackId)
+ }
+ OutlineVpn.shared.setIPWhitelist(ipWhitelist)
+ self.sendSuccess(callbackId: command.callbackId)
+ }
+
+ func setIPBlacklist(_ command: CDVInvokedUrlCommand) {
+ DDLogInfo("[OutlinePlugin.swift] Receiving ipBlacklist")
+ guard let ipBlacklist = command.argument(at: 1) as? [String] else {
+ return sendError("Missing ipBlacklist", callbackId: command.callbackId)
+ }
+ OutlineVpn.shared.setIPBlacklist(ipBlacklist)
+ self.sendSuccess(callbackId: command.callbackId)
+ }
+
/**
Starts the VPN. This method is idempotent for a given connection.
- Parameters:
diff -u -r ./cordova-plugin-outline/apple/src/OutlineVpn.swift /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/apple/src/OutlineVpn.swift
--- ./cordova-plugin-outline/apple/src/OutlineVpn.swift 2019-03-04 10:58:07.000000000 +0800
+++ /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/apple/src/OutlineVpn.swift 2019-12-09 13:49:30.000000000 +0800
@@ -31,6 +31,8 @@
private let connectivity: OutlineConnectivity
private enum Action {
+ static let setIPWhitelist = "setIPWhitelist"
+ static let setIPBlacklist = "setIPBlacklist"
static let start = "start"
static let restart = "restart"
static let stop = "stop"
@@ -43,6 +45,8 @@
static let connectionId = "connectionId"
static let config = "config"
static let errorCode = "errorCode"
+ static let ipWhitelist = "ipWhitelist"
+ static let ipBlacklist = "ipBlacklist"
static let host = "host"
static let port = "port"
static let isOnDemand = "is-on-demand"
@@ -86,6 +90,34 @@
// MARK: Interface
+ func setIPWhitelist(_ ipWhitelist: [String]) {
+ DDLogInfo("[OutlineVpn.swift] Receiving ipWhitelist")
+ // Pass the list to PacketTunnelProvider
+ let message = [MessageKey.action: Action.setIPWhitelist,
+ MessageKey.ipWhitelist: ipWhitelist] as [String : Any]
+ sendVpnExtensionMessage(message) { response in
+ guard response == nil else {
+ DDLogInfo("[OutlineVpn.swift] Problem passing ipWhitelist: \(String(describing: response))")
+ return
+ }
+ DDLogInfo("[OutlineVpn.swift] Successfully passed ipWhitelist")
+ }
+ }
+
+ func setIPBlacklist(_ ipBlacklist: [String]) {
+ DDLogInfo("[OutlineVpn.swift] Receiving ipBlacklist")
+ // Pass the list to PacketTunnelProvider
+ let message = [MessageKey.action: Action.setIPBlacklist,
+ MessageKey.ipBlacklist: ipBlacklist] as [String : Any]
+ sendVpnExtensionMessage(message) { response in
+ guard response == nil else {
+ DDLogInfo("[OutlineVpn.swift] Problem passing ipBlacklist: \(String(describing: response))")
+ return
+ }
+ DDLogInfo("[OutlineVpn.swift] Successfully passed ipBlacklist")
+ }
+ }
+
// Starts a VPN connection as specified in the OutlineConnection object.
func start(_ connection: OutlineConnection, _ completion: @escaping (Callback)) {
guard let connectionId = connection.id else {
diff -u -r ./cordova-plugin-outline/apple/vpn/PacketTunnelProvider.m /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/apple/vpn/PacketTunnelProvider.m
--- ./cordova-plugin-outline/apple/vpn/PacketTunnelProvider.m 2019-05-02 13:27:14.000000000 +0800
+++ /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/apple/vpn/PacketTunnelProvider.m 2020-01-08 10:54:05.000000000 +0800
@@ -31,16 +31,22 @@
NSString *const kActionRestart = @"restart";
NSString *const kActionStop = @"stop";
NSString *const kActionGetConnectionId = @"getConnectionId";
+NSString *const kActionSetIPWhitelist = @"setIPWhitelist";
+NSString *const kActionSetIPBlacklist = @"setIPBlacklist";
NSString *const kActionIsReachable = @"isReachable";
NSString *const kMessageKeyAction = @"action";
NSString *const kMessageKeyConnectionId = @"connectionId";
NSString *const kMessageKeyConfig = @"config";
NSString *const kMessageKeyErrorCode = @"errorCode";
+NSString *const kMessageKeyIPWhitelist = @"ipWhitelist";
+NSString *const kMessageKeyIPBlacklist = @"ipBlacklist";
NSString *const kMessageKeyHost = @"host";
NSString *const kMessageKeyPort = @"port";
NSString *const kMessageKeyOnDemand = @"is-on-demand";
NSString *const kDefaultPathKey = @"defaultPath";
static NSDictionary *kVpnSubnetCandidates; // Subnets to bind the VPN.
+static NSArray *ipWhitelist;
+static NSArray *ipBlacklist;
@interface PacketTunnelProvider()
@property (nonatomic) Shadowsocks *shadowsocks;
@@ -250,6 +256,16 @@
options:kNilOptions error:nil];
}
completionHandler(response);
+ } else if ([kActionSetIPWhitelist isEqualToString:action]) {
+ DDLogInfo(@"[PacketTunnelProvider.m] receiving ipWhitelist");
+ NSArray *ipWhitelistIn = message[kMessageKeyIPWhitelist];
+ ipWhitelist = ipWhitelistIn;
+ completionHandler(nil);
+ } else if ([kActionSetIPBlacklist isEqualToString:action]) {
+ DDLogInfo(@"[PacketTunnelProvider.m] receiving ipBlacklist");
+ NSArray *ipBlacklistIn = message[kMessageKeyIPBlacklist];
+ ipBlacklist = ipBlacklistIn;
+ completionHandler(nil);
} else if ([kActionIsReachable isEqualToString:action]) {
NSString *host = message[kMessageKeyHost];
NSNumber *port = message[kMessageKeyPort];
@@ -312,8 +328,14 @@
NSString *vpnAddress = [self selectVpnAddress];
NEIPv4Settings *ipv4Settings =
[[NEIPv4Settings alloc] initWithAddresses:@[ vpnAddress ] subnetMasks:@[ @"255.255.255.0" ]];
+ // Original code:
+ //ipv4Settings.includedRoutes = @[[NEIPv4Route defaultRoute]];
+ //ipv4Settings.excludedRoutes = [self getExcludedIpv4Routes];
+ // Our modifications:
+ // No matter what we set for includedRoutes, it seems to include every address which is not excluded!
+ // Therefore we include everything, but exclude addresses we don't want.
ipv4Settings.includedRoutes = @[[NEIPv4Route defaultRoute]];
- ipv4Settings.excludedRoutes = [self getExcludedIpv4Routes];
+ ipv4Settings.excludedRoutes = [self getBlacklistedIpv4Routes];
// Although we don't support proxying IPv6 traffic, we need to set IPv6 routes so that the DNS
// settings are respected on IPv6-only networks. Bind to a random unique local address (ULA).
@@ -334,14 +356,37 @@
- (NSArray *)getExcludedIpv4Routes {
NSMutableArray *excludedIpv4Routes = [[NSMutableArray alloc] init];
+ DDLogInfo(@"[PacketTunnelProvider.m] Getting excluded routes");
for (Subnet *subnet in [Subnet getReservedSubnets]) {
- NEIPv4Route *route =
- [[NEIPv4Route alloc] initWithDestinationAddress:subnet.address subnetMask:subnet.mask];
- [excludedIpv4Routes addObject:route];
+ [excludedIpv4Routes addObject:[self getRouteFromSubnet:subnet]];
}
return excludedIpv4Routes;
}
+- (NSArray *)getWhitelistedIpv4Routes {
+ NSMutableArray *whitelistedRoutes = [[NSMutableArray alloc] init];
+ DDLogInfo(@"[PacketTunnelProvider.m] Getting whitelisted routes");
+ for (Subnet *subnet in [Subnet buildSubnetsList:ipWhitelist]) {
+ [whitelistedRoutes addObject:[self getRouteFromSubnet:subnet]];
+ }
+ return whitelistedRoutes;
+}
+
+- (NSArray *)getBlacklistedIpv4Routes {
+ NSMutableArray *blacklistedRoutes = [[NSMutableArray alloc] init];
+ DDLogInfo(@"[PacketTunnelProvider.m] Getting blacklisted routes");
+ for (Subnet *subnet in [Subnet buildSubnetsList:ipBlacklist]) {
+ [blacklistedRoutes addObject:[self getRouteFromSubnet:subnet]];
+ }
+ return blacklistedRoutes;
+}
+
+- (NEIPv4Route *)getRouteFromSubnet:(Subnet *)subnet {
+ NEIPv4Route *route =
+ [[NEIPv4Route alloc] initWithDestinationAddress:subnet.address subnetMask:subnet.mask];
+ return route;
+}
+
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, id> *)change
diff -u -r ./cordova-plugin-outline/apple/vpn/Subnet.swift /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/apple/vpn/Subnet.swift
--- ./cordova-plugin-outline/apple/vpn/Subnet.swift 2019-03-04 10:58:07.000000000 +0800
+++ /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/apple/vpn/Subnet.swift 2019-12-09 13:49:30.000000000 +0800
@@ -60,6 +60,16 @@
return subnets
}
+ static func buildSubnetsList(_ stringArray: NSArray) -> [Subnet] {
+ var subnets: [Subnet] = []
+ for cidrSubnet in stringArray {
+ if let subnet = self.parse(cidrSubnet as! NSString as String) {
+ subnets.append(subnet)
+ }
+ }
+ return subnets
+ }
+
public var address: String
public var prefix: UInt16
public var mask: String
diff -u -r ./cordova-plugin-outline/outlinePlugin.js /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/outlinePlugin.js
--- ./cordova-plugin-outline/outlinePlugin.js 2019-01-18 10:54:31.000000000 +0800
+++ /home/joey/outline-client-with-whitelisting/cordova-plugin-outline/outlinePlugin.js 2019-12-09 13:49:30.000000000 +0800
@@ -97,6 +97,16 @@
exec(success, error, PLUGIN_NAME, cmd, [this.id_].concat(args));
};
+// Not used on iOS
+Connection.prototype.setIPWhitelist = function(whitelist) {
+ return this._promiseExec('setIPWhitelist', [whitelist]);
+};
+
+// Only used on iOS
+Connection.prototype.setIPBlacklist = function(blacklist) {
+ return this._promiseExec('setIPBlacklist', [blacklist]);
+};
+
Connection.prototype.start = function() {
return this._promiseExec('start', [this.config]);
};
Related (possibly): #556 #624 #582
Here is a pull request for the above patch: https://github.com/Jigsaw-Code/outline-client/compare/master...joeysino:ip-whitelist?expand=1
I hurriedly extracted this code from our project, so please test it before merging!
Usage:
const whitelist = [ '12.7.44.0/24', '34.22.0.0/16' ];
if (device.platform === 'Android') {
// Android accepts a whitelist
server.setIPWhitelist(whitelist);
} else if (devlice.platform === 'iOS') {
// But iOS cannot accept a whitelist, it must be passed a blacklist
const blacklist = cidrTools.exclude('0.0.0.0/0', whitelist);
server.setIPBlacklist(blacklist);
}
// Then do
server.connect();
Perhaps in future, both functions could work on both OSes, by inverting the CIDR list internally when necessary.
My .02, we've talked about selective proxying features like this. We probably want to think about what we want UX to be for something like this.
Need help?
Any updates?
Closing as duplicate of #887