commit efb1c0686d0e7a80b7470020d748be4d0814d7e4
Author: n8fr8 <nathan(a)guardianproject.info>
Date: Wed Sep 4 17:07:12 2019 -0400
implement initial framework for blocking traffic per app
- also add ability to disable tor routing as a per app preference
---
orbotservice/build.gradle | 4 +-
.../torproject/android/service/OrbotConstants.java | 6 +
.../android/service/util/TCPSourceApp.java | 316 +++++++++++++++++++++
.../android/service/vpn/OrbotVpnManager.java | 23 +-
.../torproject/android/service/vpn/Tun2Socks.java | 64 ++++-
5 files changed, 401 insertions(+), 12 deletions(-)
diff --git a/orbotservice/build.gradle b/orbotservice/build.gradle
index a62d4590..a5d07d76 100644
--- a/orbotservice/build.gradle
+++ b/orbotservice/build.gradle
@@ -1,7 +1,7 @@
apply plugin: 'com.android.library'
android {
- compileSdkVersion 28
+ compileSdkVersion 29
buildToolsVersion '28.0.3'
sourceSets {
@@ -12,7 +12,7 @@ android {
defaultConfig {
minSdkVersion 16
- targetSdkVersion 28
+ targetSdkVersion 29
versionCode 16111000
versionName '16.1.1-BETA-2-tor-0.4.0.4-rc-orbotservice'
archivesBaseName = "OrbotService-$versionName"
diff --git a/orbotservice/src/main/java/org/torproject/android/service/OrbotConstants.java b/orbotservice/src/main/java/org/torproject/android/service/OrbotConstants.java
index e452105e..a5dec36e 100644
--- a/orbotservice/src/main/java/org/torproject/android/service/OrbotConstants.java
+++ b/orbotservice/src/main/java/org/torproject/android/service/OrbotConstants.java
@@ -29,4 +29,10 @@ public interface OrbotConstants {
String PREF_PREFER_IPV6 = "pref_prefer_ipv6";
String PREF_DISABLE_IPV4 = "pref_disable_ipv4";
+
+ String APP_TOR_KEY = "_app_tor";
+ String APP_DATA_KEY = "_app_data";
+ String APP_WIFI_KEY = "_app_wifi";
+
+
}
diff --git a/orbotservice/src/main/java/org/torproject/android/service/util/TCPSourceApp.java b/orbotservice/src/main/java/org/torproject/android/service/util/TCPSourceApp.java
new file mode 100644
index 00000000..610e1ab4
--- /dev/null
+++ b/orbotservice/src/main/java/org/torproject/android/service/util/TCPSourceApp.java
@@ -0,0 +1,316 @@
+package org.torproject.android.service.util;
+
+/***********************************************************************
+ *
+ * Copyright (c) 2013, Sebastiano Gottardo
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of the MegaDevs nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL SEBASTIANO GOTTARDO BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+
+/**
+ * Main class for the TCPSourceApp library.
+ * @author Sebastiano Gottardo
+ *
+ */
+public class TCPSourceApp {
+
+ /*
+ * This class represents an Android application. Each application is
+ * uniquely identified by its package name (e.g. com.megadevs.tcpsourceapp)
+ * and its version (e.g. 1.0).
+ */
+ public static class AppDescriptor {
+
+ private String packageName;
+ private String version;
+ private int uid;
+
+ public AppDescriptor(int uid, String pName, String ver) {
+ this.uid = uid;
+ packageName = pName;
+ version = ver;
+ }
+
+ public int getUid () {
+ return uid;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ /*
+ * Override of the 'equals' method, in order to have a proper
+ * comparison between two AppDescriptor objects.
+ *
+ * (non-Javadoc)
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object o) {
+
+ if (o instanceof AppDescriptor) {
+ boolean c1 = ((AppDescriptor) o).packageName.compareTo(this.packageName) == 0;
+ boolean c2 = ((AppDescriptor) o).version.compareTo(this.version) == 0;
+
+ return c1 && c2;
+ }
+
+ return false;
+ }
+
+ }
+
+ /*
+ * In a Linux-based OS, each active TCP socket is mapped in the following
+ * two files. A socket may be mapped in the '/proc/net/tcp' file in case
+ * of a simple IPv4 address, or in the '/proc/net/tcp6' if an IPv6 address
+ * is available.
+ */
+ private static final String TCP_4_FILE_PATH = "/proc/net/tcp";
+ private static final String TCP_6_FILE_PATH = "/proc/net/tcp6";
+
+ /*
+ * Two regular expressions that are able to extract valuable informations
+ * from the two /proc/net/tcp* files. More specifically, there are three
+ * fields that are extracted:
+ * - address
+ * - port
+ * - PID
+ */
+ private static final String TCP_6_PATTERN = "\\d+:\\s([0-9A-F]{32}):([0-9A-F]{4})\\s[0-9A-F]{32}:[0-9A-F]{4}\\s[0-9A-F]{2}\\s[0-9]{8}:[0-9]{8}\\s[0-9]{2}:[0-9]{8}\\s[0-9]{8}\\s+([0-9]+)";
+//sargo:/ $ cat /proc/net/tcp6
+// sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
+// 0: 00000000000000000000000000000000:C36A 00000000000000000000000000000000:0000 8A 00000000:00000000 00:00000000 00000000 1001 0 35059 1 0000000000000000 99 0 0 10 0
+// 1: 00000000000000000000000000000000:A64B 00000000000000000000000000000000:0000 8A 00000000:00000000 00:00000000 00000000 1001 0 910009 1 0000000000000000 99 0 0 10 0
+
+
+ private static final String TCP_4_PATTERN = "\\d+:\\s([0-9A-F]{8}):([0-9A-F]{4})\\s[0-9A-F]{8}:[0-9A-F]{4}\\s[0-9A-F]{2}\\s[0-9A-F]{8}:[0-9A-F]{8}\\s[0-9]{2}:[0-9]{8}\\s[0-9A-F]{8}\\s+([0-9]+)";
+// sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
+// 0: 00000000:C368 00000000:0000 8A 00000000:00000000 00:00000000 00000000 1001 0 34999 1 0000000000000000 99 0 0 10 0
+
+ /*
+ * Optimises the socket lookup by checking if the connected network
+ * interface has a 'valid' IPv6 address (a global address, not a link-local
+ * one).
+ */
+ private static boolean checkConnectedIfaces = true;
+
+
+ /**
+ * The main method of the TCPSourceApp library. This method receives an
+ * Android Context instance, which is used to access the PackageManager.
+ * It parses the /proc/net/tcp* files, looking for a socket entry that
+ * matches the given port. If it finds an entry, this method extracts the
+ * PID value and it uses the PackageManager.getPackagesFromPid() method to
+ * find the originating application.
+ *
+ * @param context a valid Android Context instance
+ * @param daddr the (logical) address of the destination
+ * @param dport the (logical) port of the destination
+ * @return an AppDescriptor object, representing the found application; null
+ * if no application could be found
+ */
+ @SuppressWarnings("unused")
+ public static AppDescriptor getApplicationInfo(Context context, String saddr, int sport, String daddr, int dport) {
+
+ File tcp;
+ BufferedReader reader;
+ String line;
+ StringBuilder builder;
+ String content;
+
+ try {
+ boolean hasIPv6 = true;
+
+ // if true, checks for a connected network interface with a valid
+ // IPv4 / IPv6 address
+ if (checkConnectedIfaces) {
+ String ipv4Address = getIPAddress(true);
+ String ipv6Address = getIPAddress(false);
+
+ hasIPv6 = (ipv6Address.length() > 0);
+ }
+
+ tcp = new File(TCP_6_FILE_PATH);
+ reader = new BufferedReader(new FileReader(tcp));
+ line = "";
+ builder = new StringBuilder();
+
+ while ((line = reader.readLine()) != null) {
+ builder.append(line);
+ }
+
+ content = builder.toString();
+
+ Matcher m6 = Pattern.compile(TCP_6_PATTERN, Pattern.CASE_INSENSITIVE | Pattern.UNIX_LINES | Pattern.DOTALL).matcher(content);
+
+ if (hasIPv6)
+ while (m6.find()) {
+ String addressEntry = m6.group(1);
+ String portEntry = m6.group(2);
+ int pidEntry = Integer.valueOf(m6.group(3));
+
+ if (Integer.parseInt(portEntry, 16) == dport) {
+ PackageManager manager = context.getPackageManager();
+ String[] packagesForUid = manager.getPackagesForUid(pidEntry);
+
+ if (packagesForUid != null) {
+ String packageName = packagesForUid[0];
+ PackageInfo pInfo = manager.getPackageInfo(packageName, 0);
+ String version = pInfo.versionName;
+
+ return new AppDescriptor(pidEntry, packageName, version);
+ }
+ }
+ }
+
+ } catch (SocketException e) {
+ e.printStackTrace();
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (NameNotFoundException e) {
+ e.printStackTrace();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ // From here, no connection with the given port could be found in the tcp6 file
+ // So let's try the tcp (IPv4) one
+
+ try {
+ tcp = new File(TCP_4_FILE_PATH);
+ reader = new BufferedReader(new FileReader(tcp));
+ line = "";
+ builder = new StringBuilder();
+
+ while ((line = reader.readLine()) != null) {
+ builder.append(line);
+ }
+
+ content = builder.toString();
+
+ Matcher m4 = Pattern.compile(TCP_4_PATTERN, Pattern.CASE_INSENSITIVE | Pattern.UNIX_LINES | Pattern.DOTALL).matcher(content);
+
+ while (m4.find()) {
+ String addressEntry = m4.group(1);
+ String portEntry = m4.group(2);
+ int pidEntry = Integer.valueOf(m4.group(3));
+
+ if (Integer.parseInt(portEntry, 16) == dport) {
+ PackageManager manager = context.getPackageManager();
+ String[] packagesForUid = manager.getPackagesForUid(pidEntry);
+
+ if (packagesForUid != null) {
+ String packageName = packagesForUid[0];
+ PackageInfo pInfo = manager.getPackageInfo(packageName, 0);
+ String version = pInfo.versionName;
+
+ return new AppDescriptor(pidEntry, packageName, version);
+ }
+ }
+ }
+
+ } catch (SocketException e) {
+ e.printStackTrace();
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (NameNotFoundException e) {
+ e.printStackTrace();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ @SuppressLint("DefaultLocale")
+ public static String getIPAddress(boolean useIPv4) throws SocketException {
+
+ List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+
+ for (NetworkInterface intf : interfaces) {
+ List<InetAddress> addrs = Collections.list(intf.getInetAddresses());
+
+ for (InetAddress addr : addrs) {
+ if (!addr.isLoopbackAddress()) {
+ String sAddr = addr.getHostAddress().toUpperCase();
+
+ boolean isIPv4 = addr instanceof Inet4Address;
+
+ if (useIPv4) {
+ if (isIPv4)
+ return sAddr;
+ } else {
+ if (!isIPv4) {
+ if (sAddr.startsWith("fe80") || sAddr.startsWith("FE80")) // skipping link-local addresses
+ continue;
+
+ int delim = sAddr.indexOf('%'); // drop ip6 port suffix
+ return delim < 0 ? sAddr : sAddr.substring(0, delim);
+ }
+ }
+ }
+ }
+ }
+
+ return "";
+ }
+
+ /*
+ * Sets the connected interfaces optimisation.
+ */
+ public static void setCheckConnectedIfaces(boolean value) {
+ checkConnectedIfaces = value;
+ }
+
+}
\ No newline at end of file
diff --git a/orbotservice/src/main/java/org/torproject/android/service/vpn/OrbotVpnManager.java b/orbotservice/src/main/java/org/torproject/android/service/vpn/OrbotVpnManager.java
index 9b099a24..03ad79c2 100644
--- a/orbotservice/src/main/java/org/torproject/android/service/vpn/OrbotVpnManager.java
+++ b/orbotservice/src/main/java/org/torproject/android/service/vpn/OrbotVpnManager.java
@@ -38,6 +38,7 @@ import android.widget.Toast;
import com.runjva.sourceforge.jsocks.protocol.ProxyServer;
import com.runjva.sourceforge.jsocks.server.ServerAuthenticatorNone;
+import org.torproject.android.service.OrbotConstants;
import org.torproject.android.service.R;
import org.torproject.android.service.TorService;
import org.torproject.android.service.TorServiceConstants;
@@ -315,7 +316,6 @@ public class OrbotVpnManager implements Handler.Callback {
builder.addDnsServer(dummyDNS);
builder.addRoute(dummyDNS,32);
-
//handle ipv6
//builder.addAddress("fdfe:dcba:9876::1", 126);
//builder.addRoute("::", 0);
@@ -344,7 +344,7 @@ public class OrbotVpnManager implements Handler.Callback {
startDNS(filePdnsd.getCanonicalPath(), localhost,mTorDns, virtualGateway, pdnsdPort);
final boolean localDnsTransparentProxy = true;
- Tun2Socks.Start(mInterface, VPN_MTU, virtualIP, virtualNetMask, localSocks , virtualGateway + ":" + pdnsdPort , localDnsTransparentProxy);
+ Tun2Socks.Start(mService, mInterface, VPN_MTU, virtualIP, virtualNetMask, localSocks , virtualGateway + ":" + pdnsdPort , localDnsTransparentProxy);
}
@@ -366,17 +366,24 @@ public class OrbotVpnManager implements Handler.Callback {
{
ArrayList<TorifiedApp> apps = TorifiedApp.getApps(mService, getSharedPrefs(mService.getApplicationContext()));
-
- boolean perAppEnabled = false;
+
+ SharedPreferences prefs = getSharedPrefs(mService.getApplicationContext());
+
+ boolean perAppEnabled = false;
for (TorifiedApp app : apps)
{
if (app.isTorified() && (!app.getPackageName().equals(mService.getPackageName())))
{
- builder.addAllowedApplication(app.getPackageName());
- perAppEnabled = true;
- }
-
+ if (prefs.getBoolean(app.getPackageName() + OrbotConstants.APP_TOR_KEY,true)) {
+
+ builder.addAllowedApplication(app.getPackageName());
+
+ }
+
+ perAppEnabled = true;
+
+ }
}
if (!perAppEnabled)
diff --git a/orbotservice/src/main/java/org/torproject/android/service/vpn/Tun2Socks.java b/orbotservice/src/main/java/org/torproject/android/service/vpn/Tun2Socks.java
index 958fb9ad..e7f0591b 100644
--- a/orbotservice/src/main/java/org/torproject/android/service/vpn/Tun2Socks.java
+++ b/orbotservice/src/main/java/org/torproject/android/service/vpn/Tun2Socks.java
@@ -19,11 +19,22 @@ package org.torproject.android.service.vpn;
*
*/
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.net.ConnectivityManager;
+import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;
+import org.torproject.android.service.util.TCPSourceApp;
+
import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
import java.net.Socket;
+import java.util.HashMap;
+
+import static android.content.Context.CONNECTIVITY_SERVICE;
public class Tun2Socks
{
@@ -49,7 +60,9 @@ public class Tun2Socks
private static String mSocksServerAddress;
private static String mUdpgwServerAddress;
private static boolean mUdpgwTransparentDNS;
-
+
+ private static HashMap<Integer,String> mAppUidBlacklist = new HashMap<>();
+ private static Context mContext;
// Note: this class isn't a singleton, but you can't run more
// than one instance due to the use of global state (the lwip
// module, etc.) in the native code.
@@ -57,6 +70,7 @@ public class Tun2Socks
public static void init () {}
public static void Start(
+ Context context,
ParcelFileDescriptor vpnInterfaceFileDescriptor,
int vpnInterfaceMTU,
String vpnIpAddress,
@@ -65,6 +79,7 @@ public class Tun2Socks
String udpgwServerAddress,
boolean udpgwTransparentDNS)
{
+ mContext = context;
mVpnInterfaceFileDescriptor = vpnInterfaceFileDescriptor;
mVpnInterfaceMTU = vpnInterfaceMTU;
@@ -118,5 +133,50 @@ public class Tun2Socks
int udpgwTransparentDNS);
private native static void terminateTun2Socks();
-
+
+ public static boolean isAllowed (int protocol, String sourceAddr, int sourcePort, String destAddr, int destPort) {
+
+ TCPSourceApp.AppDescriptor aInfo = TCPSourceApp.getApplicationInfo(mContext, sourceAddr, sourcePort, destAddr, destPort);
+
+ if (aInfo != null) {
+ int uid = aInfo.getUid();
+ return mAppUidBlacklist.containsKey(uid);
+ }
+ else
+ return true;
+ }
+
+ @TargetApi(Build.VERSION_CODES.Q)
+ public static boolean isAllowedQ (int protocol, String sourceAddr, int sourcePort, String destAddr, int destPort) {
+ ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(CONNECTIVITY_SERVICE);
+ if (cm == null)
+ return false;
+
+ InetSocketAddress local = new InetSocketAddress(sourceAddr, sourcePort);
+ InetSocketAddress remote = new InetSocketAddress(destAddr, destPort);
+
+ int uid = cm.getConnectionOwnerUid(protocol, local, remote);
+ return mAppUidBlacklist.containsKey(uid);
+ }
+
+ public static void setBlacklist(HashMap<Integer,String> appUidBlacklist)
+ {
+ mAppUidBlacklist = appUidBlacklist;
+ }
+
+ public static void clearBlacklist()
+ {
+ mAppUidBlacklist.clear();
+ }
+
+ public static void addToBlacklist (int uid, String pkgId)
+ {
+ mAppUidBlacklist.put(uid,pkgId);
+ }
+
+ public static void removeFromBlacklist (int uid)
+ {
+ mAppUidBlacklist.remove(uid);
+ }
+
}
\ No newline at end of file