commit efb1c0686d0e7a80b7470020d748be4d0814d7e4 Author: n8fr8 nathan@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