[tor-commits] r24625: {} adding in iptables API wrapper class from Droidwall (projects/android/trunk/Orbot/src/org/torproject/android/service)

Nathan Freitas nathan at freitas.net
Fri Apr 15 05:57:53 UTC 2011


Author: n8fr8
Date: 2011-04-15 05:57:52 +0000 (Fri, 15 Apr 2011)
New Revision: 24625

Added:
   projects/android/trunk/Orbot/src/org/torproject/android/service/Api.java
Log:
adding in iptables API wrapper class from Droidwall


Added: projects/android/trunk/Orbot/src/org/torproject/android/service/Api.java
===================================================================
--- projects/android/trunk/Orbot/src/org/torproject/android/service/Api.java	                        (rev 0)
+++ projects/android/trunk/Orbot/src/org/torproject/android/service/Api.java	2011-04-15 05:57:52 UTC (rev 24625)
@@ -0,0 +1,1052 @@
+/**
+ * Contains shared programming interfaces.
+ * All iptables "communication" is handled by this class.
+ * 
+ * Copyright (C) 2009-2010  Rodrigo Zechin Rosauro
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author Rodrigo Zechin Rosauro
+ * @version 1.0
+ */
+
+package org.torproject.android.service;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.torproject.android.R;
+
+import android.Manifest;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.util.Log;
+import android.widget.Toast;
+
+/**
+ * Contains shared programming interfaces.
+ * All iptables "communication" is handled by this class.
+ */
+public final class Api {
+	/** application version string */
+	public static final String VERSION = "1.5.1-dev";
+	/** special application UID used to indicate "any application" */
+	public static final int SPECIAL_UID_ANY	= -10;
+	/** special application UID used to indicate the Linux Kernel */
+	public static final int SPECIAL_UID_KERNEL	= -11;
+	/** root script filename */
+	private static final String SCRIPT_FILE = "droidwall.sh";
+	
+	// Preferences
+	public static final String PREFS_NAME 		= "DroidWallPrefs";
+	public static final String PREF_3G_UIDS		= "AllowedUids3G";
+	public static final String PREF_WIFI_UIDS	= "AllowedUidsWifi";
+	public static final String PREF_PASSWORD 	= "Password";
+	public static final String PREF_MODE 		= "BlockMode";
+	public static final String PREF_ENABLED		= "Enabled";
+	public static final String PREF_LOGENABLED	= "LogEnabled";
+	// Modes
+	public static final String MODE_WHITELIST = "whitelist";
+	public static final String MODE_BLACKLIST = "blacklist";
+	// Messages
+	public static final String STATUS_CHANGED_MSG 	= "com.googlecode.droidwall.intent.action.STATUS_CHANGED";
+	public static final String TOGGLE_REQUEST_MSG	= "com.googlecode.droidwall.intent.action.TOGGLE_REQUEST";
+	public static final String STATUS_EXTRA			= "com.googlecode.droidwall.intent.extra.STATUS";
+	
+	// Cached applications
+	public static DroidApp applications[] = null;
+	// Do we have root access?
+	private static boolean hasroot = false;
+	// Flag indicating if this is an ARMv6 device (-1: unknown, 0: no,  1: yes)
+	private static int isARMv6 = -1;
+
+    /**
+     * Display a simple alert box
+     * @param ctx context
+     * @param msg message
+     */
+	public static void alert(Context ctx, CharSequence msg) {
+    	if (ctx != null) {
+        	new AlertDialog.Builder(ctx)
+        	.setNeutralButton(android.R.string.ok, null)
+        	.setMessage(msg)
+        	.show();
+    	}
+    }
+	/**
+	 * Check if this is an ARMv6 device
+	 * @return true if this is ARMv6
+	 */
+	private static boolean isARMv6() {
+		if (isARMv6 == -1) {
+			BufferedReader r = null;
+			try {
+				isARMv6 = 0;
+				r = new BufferedReader(new FileReader("/proc/cpuinfo"));
+				for (String line = r.readLine(); line != null; line = r.readLine()) {
+					if (line.startsWith("Processor") && line.contains("ARMv6")) {
+						isARMv6 = 1;
+						break;
+					} else if (line.startsWith("CPU architecture") && (line.contains("6TE") || line.contains("5TE"))) {
+						isARMv6 = 1;
+						break;
+					}
+				}
+			} catch (Exception ex) {
+			} finally {
+				if (r != null) try {r.close();} catch (Exception ex) {}
+			}
+		}
+		return (isARMv6 == 1);
+	}
+	/**
+	 * Create the generic shell script header used to determine which iptables binary to use.
+	 * @param ctx context
+	 * @return script header
+	 */
+	private static String scriptHeader(Context ctx) {
+		final String dir = ctx.getDir("bin",0).getAbsolutePath();
+		final String myiptables = dir + (isARMv6() ? "/iptables_g1" : "/iptables_n1");
+		return "" +
+			"IPTABLES=iptables\n" +
+			"BUSYBOX=busybox\n" +
+			"GREP=grep\n" +
+			"ECHO=echo\n" +
+			"# Try to find busybox\n" +
+			"if " + dir + "/busybox_g1 --help >/dev/null 2>/dev/null ; then\n" +
+			"	BUSYBOX="+dir+"/busybox_g1\n" +
+			"	GREP=\"$BUSYBOX grep\"\n" +
+			"	ECHO=\"$BUSYBOX echo\"\n" +
+			"elif busybox --help >/dev/null 2>/dev/null ; then\n" +
+			"	BUSYBOX=busybox\n" +
+			"elif /system/xbin/busybox --help >/dev/null 2>/dev/null ; then\n" +
+			"	BUSYBOX=/system/xbin/busybox\n" +
+			"elif /system/bin/busybox --help >/dev/null 2>/dev/null ; then\n" +
+			"	BUSYBOX=/system/bin/busybox\n" +
+			"fi\n" +
+			"# Try to find grep\n" +
+			"if ! $ECHO 1 | $GREP -q 1 >/dev/null 2>/dev/null ; then\n" +
+			"	if $ECHO 1 | $BUSYBOX grep -q 1 >/dev/null 2>/dev/null ; then\n" +
+			"		GREP=\"$BUSYBOX grep\"\n" +
+			"	fi\n" +
+			"	# Grep is absolutely required\n" +
+			"	if ! $ECHO 1 | $GREP -q 1 >/dev/null 2>/dev/null ; then\n" +
+			"		$ECHO The grep command is required. DroidWall will not work.\n" +
+			"		exit 1\n" +
+			"	fi\n" +
+			"fi\n" +
+			"# Try to find iptables\n" +
+			"if " + myiptables + " --version >/dev/null 2>/dev/null ; then\n" +
+			"	IPTABLES="+myiptables+"\n" +
+			"fi\n" +
+			"";
+	}
+	/**
+	 * Copies a raw resource file, given its ID to the given location
+	 * @param ctx context
+	 * @param resid resource id
+	 * @param file destination file
+	 * @param mode file permissions (E.g.: "755")
+	 * @throws IOException on error
+	 * @throws InterruptedException when interrupted
+	 */
+	private static void copyRawFile(Context ctx, int resid, File file, String mode) throws IOException, InterruptedException
+	{
+		final String abspath = file.getAbsolutePath();
+		// Write the iptables binary
+		final FileOutputStream out = new FileOutputStream(file);
+		final InputStream is = ctx.getResources().openRawResource(resid);
+		byte buf[] = new byte[1024];
+		int len;
+		while ((len = is.read(buf)) > 0) {
+			out.write(buf, 0, len);
+		}
+		out.close();
+		is.close();
+		// Change the permissions
+		Runtime.getRuntime().exec("chmod "+mode+" "+abspath).waitFor();
+	}
+    /**
+     * Purge and re-add all rules (internal implementation).
+     * @param ctx application context (mandatory)
+     * @param uidsWifi list of selected UIDs for WIFI to allow or disallow (depending on the working mode)
+     * @param uids3g list of selected UIDs for 2G/3G to allow or disallow (depending on the working mode)
+     * @param showErrors indicates if errors should be alerted
+     */
+	private static boolean applyIptablesRulesImpl(Context ctx, List<Integer> uidsWifi, List<Integer> uids3g, boolean showErrors) {
+		if (ctx == null) {
+			return false;
+		}
+		assertBinaries(ctx, showErrors);
+		final String ITFS_WIFI[] = {"tiwlan+", "wlan+", "eth+"};
+		final String ITFS_3G[] = {"rmnet+","pdp+","ppp+","uwbr+","wimax+","vsnet+"};
+		final SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, 0);
+		final boolean whitelist = prefs.getString(PREF_MODE, MODE_WHITELIST).equals(MODE_WHITELIST);
+		final boolean blacklist = !whitelist;
+		final boolean logenabled = ctx.getSharedPreferences(PREFS_NAME, 0).getBoolean(PREF_LOGENABLED, false);
+
+    	final StringBuilder script = new StringBuilder();
+		try {
+			int code;
+			script.append(scriptHeader(ctx));
+			script.append("" +
+				"$IPTABLES --version || exit 1\n" +
+				"# Create the droidwall chains if necessary\n" +
+				"$IPTABLES -L droidwall >/dev/null 2>/dev/null || $IPTABLES --new droidwall || exit 2\n" +
+				"$IPTABLES -L droidwall-3g >/dev/null 2>/dev/null || $IPTABLES --new droidwall-3g || exit 3\n" +
+				"$IPTABLES -L droidwall-wifi >/dev/null 2>/dev/null || $IPTABLES --new droidwall-wifi || exit 4\n" +
+				"$IPTABLES -L droidwall-reject >/dev/null 2>/dev/null || $IPTABLES --new droidwall-reject || exit 5\n" +
+				"# Add droidwall chain to OUTPUT chain if necessary\n" +
+				"$IPTABLES -L OUTPUT | $GREP -q droidwall || $IPTABLES -A OUTPUT -j droidwall || exit 6\n" +
+				"# Flush existing rules\n" +
+				"$IPTABLES -F droidwall || exit 7\n" +
+				"$IPTABLES -F droidwall-3g || exit 8\n" +
+				"$IPTABLES -F droidwall-wifi || exit 9\n" +
+				"$IPTABLES -F droidwall-reject || exit 10\n" +
+			"");
+			// Check if logging is enabled
+			if (logenabled) {
+				script.append("" +
+					"# Create the log and reject rules (ignore errors on the LOG target just in case it is not available)\n" +
+					"$IPTABLES -A droidwall-reject -j LOG --log-prefix \"[DROIDWALL] \" --log-uid\n" +
+					"$IPTABLES -A droidwall-reject -j REJECT || exit 11\n" +
+				"");
+			} else {
+				script.append("" +
+					"# Create the reject rule (log disabled)\n" +
+					"$IPTABLES -A droidwall-reject -j REJECT || exit 11\n" +
+				"");
+			}
+			if (whitelist && logenabled) {
+				script.append("# Allow DNS lookups on white-list for a better logging (ignore errors)\n");
+				script.append("$IPTABLES -A droidwall -p udp --dport 53 -j RETURN\n");
+			}
+			script.append("# Main rules (per interface)\n");
+			for (final String itf : ITFS_3G) {
+				script.append("$IPTABLES -A droidwall -o ").append(itf).append(" -j droidwall-3g || exit\n");
+			}
+			for (final String itf : ITFS_WIFI) {
+				script.append("$IPTABLES -A droidwall -o ").append(itf).append(" -j droidwall-wifi || exit\n");
+			}
+			
+			script.append("# Filtering rules\n");
+			final String targetRule = (whitelist ? "RETURN" : "droidwall-reject");
+			final boolean any_3g = uids3g.indexOf(SPECIAL_UID_ANY) >= 0;
+			final boolean any_wifi = uidsWifi.indexOf(SPECIAL_UID_ANY) >= 0;
+			if (whitelist && !any_wifi) {
+				// When "white listing" wifi, we need to ensure that the dhcp and wifi users are allowed
+				int uid = android.os.Process.getUidForName("dhcp");
+				if (uid != -1) {
+					script.append("# dhcp user\n");
+					script.append("$IPTABLES -A droidwall-wifi -m owner --uid-owner ").append(uid).append(" -j RETURN || exit\n");
+				}
+				uid = android.os.Process.getUidForName("wifi");
+				if (uid != -1) {
+					script.append("# wifi user\n");
+					script.append("$IPTABLES -A droidwall-wifi -m owner --uid-owner ").append(uid).append(" -j RETURN || exit\n");
+				}
+			}
+			if (any_3g) {
+				if (blacklist) {
+					/* block any application on this interface */
+					script.append("$IPTABLES -A droidwall-3g -j ").append(targetRule).append(" || exit\n");
+				}
+			} else {
+				/* release/block individual applications on this interface */
+				for (final Integer uid : uids3g) {
+					if (uid >= 0) script.append("$IPTABLES -A droidwall-3g -m owner --uid-owner ").append(uid).append(" -j ").append(targetRule).append(" || exit\n");
+				}
+			}
+			if (any_wifi) {
+				if (blacklist) {
+					/* block any application on this interface */
+					script.append("$IPTABLES -A droidwall-wifi -j ").append(targetRule).append(" || exit\n");
+				}
+			} else {
+				/* release/block individual applications on this interface */
+				for (final Integer uid : uidsWifi) {
+					if (uid >= 0) script.append("$IPTABLES -A droidwall-wifi -m owner --uid-owner ").append(uid).append(" -j ").append(targetRule).append(" || exit\n");
+				}
+			}
+			if (whitelist) {
+				if (!any_3g) {
+					if (uids3g.indexOf(SPECIAL_UID_KERNEL) >= 0) {
+						script.append("# hack to allow kernel packets on white-list\n");
+						script.append("$IPTABLES -A droidwall-3g -m owner --uid-owner 0:999999999 -j droidwall-reject || exit\n");
+					} else {
+						script.append("$IPTABLES -A droidwall-3g -j droidwall-reject || exit\n");
+					}
+				}
+				if (!any_wifi) {
+					if (uidsWifi.indexOf(SPECIAL_UID_KERNEL) >= 0) {
+						script.append("# hack to allow kernel packets on white-list\n");
+						script.append("$IPTABLES -A droidwall-wifi -m owner --uid-owner 0:999999999 -j droidwall-reject || exit\n");
+					} else {
+						script.append("$IPTABLES -A droidwall-wifi -j droidwall-reject || exit\n");
+					}
+				}
+			} else {
+				if (uids3g.indexOf(SPECIAL_UID_KERNEL) >= 0) {
+					script.append("# hack to BLOCK kernel packets on black-list\n");
+					script.append("$IPTABLES -A droidwall-3g -m owner --uid-owner 0:999999999 -j RETURN || exit\n");
+					script.append("$IPTABLES -A droidwall-3g -j droidwall-reject || exit\n");
+				}
+				if (uidsWifi.indexOf(SPECIAL_UID_KERNEL) >= 0) {
+					script.append("# hack to BLOCK kernel packets on black-list\n");
+					script.append("$IPTABLES -A droidwall-wifi -m owner --uid-owner 0:999999999 -j RETURN || exit\n");
+					script.append("$IPTABLES -A droidwall-wifi -j droidwall-reject || exit\n");
+				}
+			}
+	    	final StringBuilder res = new StringBuilder();
+			code = runScriptAsRoot(ctx, script.toString(), res);
+			if (showErrors && code != 0) {
+				String msg = res.toString();
+				Log.e("DroidWall", msg);
+				// Remove unnecessary help message from output
+				if (msg.indexOf("\nTry `iptables -h' or 'iptables --help' for more information.") != -1) {
+					msg = msg.replace("\nTry `iptables -h' or 'iptables --help' for more information.", "");
+				}
+				alert(ctx, "Error applying iptables rules. Exit code: " + code + "\n\n" + msg.trim());
+			} else {
+				return true;
+			}
+		} catch (Exception e) {
+			if (showErrors) alert(ctx, "error refreshing iptables: " + e);
+		}
+		return false;
+    }
+    /**
+     * Purge and re-add all saved rules (not in-memory ones).
+     * This is much faster than just calling "applyIptablesRules", since it don't need to read installed applications.
+     * @param ctx application context (mandatory)
+     * @param showErrors indicates if errors should be alerted
+     */
+	public static boolean applySavedIptablesRules(Context ctx, boolean showErrors) {
+		if (ctx == null) {
+			return false;
+		}
+		final SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, 0);
+		final String savedUids_wifi = prefs.getString(PREF_WIFI_UIDS, "");
+		final String savedUids_3g = prefs.getString(PREF_3G_UIDS, "");
+		final List<Integer> uids_wifi = new LinkedList<Integer>();
+		if (savedUids_wifi.length() > 0) {
+			// Check which applications are allowed on wifi
+			final StringTokenizer tok = new StringTokenizer(savedUids_wifi, "|");
+			while (tok.hasMoreTokens()) {
+				final String uid = tok.nextToken();
+				if (!uid.equals("")) {
+					try {
+						uids_wifi.add(Integer.parseInt(uid));
+					} catch (Exception ex) {
+					}
+				}
+			}
+		}
+		final List<Integer> uids_3g = new LinkedList<Integer>();
+		if (savedUids_3g.length() > 0) {
+			// Check which applications are allowed on 2G/3G
+			final StringTokenizer tok = new StringTokenizer(savedUids_3g, "|");
+			while (tok.hasMoreTokens()) {
+				final String uid = tok.nextToken();
+				if (!uid.equals("")) {
+					try {
+						uids_3g.add(Integer.parseInt(uid));
+					} catch (Exception ex) {
+					}
+				}
+			}
+		}
+		return applyIptablesRulesImpl(ctx, uids_wifi, uids_3g, showErrors);
+	}
+	
+    /**
+     * Purge and re-add all rules.
+     * @param ctx application context (mandatory)
+     * @param showErrors indicates if errors should be alerted
+     */
+	public static boolean applyIptablesRules(Context ctx, boolean showErrors) {
+		if (ctx == null) {
+			return false;
+		}
+		saveRules(ctx);
+		return applySavedIptablesRules(ctx, showErrors);
+    }
+	
+	/**
+	 * Save current rules using the preferences storage.
+	 * @param ctx application context (mandatory)
+	 */
+	public static void saveRules(Context ctx) {
+		final SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, 0);
+		final DroidApp[] apps = getApps(ctx);
+		// Builds a pipe-separated list of names
+		final StringBuilder newuids_wifi = new StringBuilder();
+		final StringBuilder newuids_3g = new StringBuilder();
+		for (int i=0; i<apps.length; i++) {
+			if (apps[i].selected_wifi) {
+				if (newuids_wifi.length() != 0) newuids_wifi.append('|');
+				newuids_wifi.append(apps[i].uid);
+			}
+			if (apps[i].selected_3g) {
+				if (newuids_3g.length() != 0) newuids_3g.append('|');
+				newuids_3g.append(apps[i].uid);
+			}
+		}
+		// save the new list of UIDs
+		final Editor edit = prefs.edit();
+		edit.putString(PREF_WIFI_UIDS, newuids_wifi.toString());
+		edit.putString(PREF_3G_UIDS, newuids_3g.toString());
+		edit.commit();
+    }
+    
+    /**
+     * Purge all iptables rules.
+     * @param ctx mandatory context
+     * @param showErrors indicates if errors should be alerted
+     * @return true if the rules were purged
+     */
+	public static boolean purgeIptables(Context ctx, boolean showErrors) {
+    	StringBuilder res = new StringBuilder();
+		try {
+			assertBinaries(ctx, showErrors);
+			int code = runScriptAsRoot(ctx, scriptHeader(ctx) +
+					"$IPTABLES -F droidwall\n" +
+					"$IPTABLES -F droidwall-reject\n" +
+					"$IPTABLES -F droidwall-3g\n" +
+					"$IPTABLES -F droidwall-wifi\n", res);
+			if (code == -1) {
+				if (showErrors) alert(ctx, "error purging iptables. exit code: " + code + "\n" + res);
+				return false;
+			}
+			return true;
+		} catch (Exception e) {
+			if (showErrors) alert(ctx, "error purging iptables: " + e);
+			return false;
+		}
+    }
+	
+	/**
+	 * Display iptables rules output
+	 * @param ctx application context
+	 */
+	public static void showIptablesRules(Context ctx) {
+		try {
+    		final StringBuilder res = new StringBuilder();
+			runScriptAsRoot(ctx, scriptHeader(ctx) +
+								 "$ECHO $IPTABLES\n" +
+								 "$IPTABLES -L -v\n", res);
+			alert(ctx, res);
+		} catch (Exception e) {
+			alert(ctx, "error: " + e);
+		}
+	}
+
+	/**
+	 * Display logs
+	 * @param ctx application context
+     * @return true if the clogs were cleared
+	 */
+	public static boolean clearLog(Context ctx) {
+		try {
+			final StringBuilder res = new StringBuilder();
+			int code = runScriptAsRoot(ctx, "dmesg -c >/dev/null || exit\n", res);
+			if (code != 0) {
+				alert(ctx, res);
+				return false;
+			}
+			return true;
+		} catch (Exception e) {
+			alert(ctx, "error: " + e);
+		}
+		return false;
+	}
+	/**
+	 * Display logs
+	 * @param ctx application context
+	 */
+	public static void showLog(Context ctx) {
+		try {
+    		StringBuilder res = new StringBuilder();
+			int code = runScriptAsRoot(ctx, scriptHeader(ctx) +
+					"dmesg | $GREP DROIDWALL\n", res);
+			if (code != 0) {
+				if (res.length() == 0) {
+					res.append("Log is empty");
+				}
+				alert(ctx, res);
+				return;
+			}
+			final BufferedReader r = new BufferedReader(new StringReader(res.toString()));
+			final Integer unknownUID = -99;
+			res = new StringBuilder();
+			String line;
+			int start, end;
+			Integer appid;
+			final HashMap<Integer, LogInfo> map = new HashMap<Integer, LogInfo>();
+			LogInfo loginfo = null;
+			while ((line = r.readLine()) != null) {
+				if (line.indexOf("[DROIDWALL]") == -1) continue;
+				appid = unknownUID;
+				if (((start=line.indexOf("UID=")) != -1) && ((end=line.indexOf(" ", start)) != -1)) {
+					appid = Integer.parseInt(line.substring(start+4, end));
+				}
+				loginfo = map.get(appid);
+				if (loginfo == null) {
+					loginfo = new LogInfo();
+					map.put(appid, loginfo);
+				}
+				loginfo.totalBlocked += 1;
+				if (((start=line.indexOf("DST=")) != -1) && ((end=line.indexOf(" ", start)) != -1)) {
+					String dst = line.substring(start+4, end);
+					if (loginfo.dstBlocked.containsKey(dst)) {
+						loginfo.dstBlocked.put(dst, loginfo.dstBlocked.get(dst) + 1);
+					} else {
+						loginfo.dstBlocked.put(dst, 1);
+					}
+				}
+			}
+			final DroidApp[] apps = getApps(ctx);
+			for (Integer id : map.keySet()) {
+				res.append("App ID ");
+				if (id != unknownUID) {
+					res.append(id);
+					for (DroidApp app : apps) {
+						if (app.uid == id) {
+							res.append(" (").append(app.names[0]);
+							if (app.names.length > 1) {
+								res.append(", ...)");
+							} else {
+								res.append(")");
+							}
+							break;
+						}
+					}
+				} else {
+					res.append("(kernel)");
+				}
+				loginfo = map.get(id);
+				res.append(" - Blocked ").append(loginfo.totalBlocked).append(" packets");
+				if (loginfo.dstBlocked.size() > 0) {
+					res.append(" (");
+					boolean first = true;
+					for (String dst : loginfo.dstBlocked.keySet()) {
+						if (!first) {
+							res.append(", ");
+						}
+						res.append(loginfo.dstBlocked.get(dst)).append(" packets for ").append(dst);
+						first = false;
+					}
+					res.append(")");
+				}
+				res.append("\n\n");
+			}
+			if (res.length() == 0) {
+				res.append("Log is empty");
+			}
+			alert(ctx, res);
+		} catch (Exception e) {
+			alert(ctx, "error: " + e);
+		}
+	}
+
+    /**
+     * @param ctx application context (mandatory)
+     * @return a list of applications
+     */
+	public static DroidApp[] getApps(Context ctx) {
+		if (applications != null) {
+			// return cached instance
+			return applications;
+		}
+		final SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, 0);
+		// allowed application names separated by pipe '|' (persisted)
+		final String savedUids_wifi = prefs.getString(PREF_WIFI_UIDS, "");
+		final String savedUids_3g = prefs.getString(PREF_3G_UIDS, "");
+		int selected_wifi[] = new int[0];
+		int selected_3g[] = new int[0];
+		if (savedUids_wifi.length() > 0) {
+			// Check which applications are allowed
+			final StringTokenizer tok = new StringTokenizer(savedUids_wifi, "|");
+			selected_wifi = new int[tok.countTokens()];
+			for (int i=0; i<selected_wifi.length; i++) {
+				final String uid = tok.nextToken();
+				if (!uid.equals("")) {
+					try {
+						selected_wifi[i] = Integer.parseInt(uid);
+					} catch (Exception ex) {
+						selected_wifi[i] = -1;
+					}
+				}
+			}
+			// Sort the array to allow using "Arrays.binarySearch" later
+			Arrays.sort(selected_wifi);
+		}
+		if (savedUids_3g.length() > 0) {
+			// Check which applications are allowed
+			final StringTokenizer tok = new StringTokenizer(savedUids_3g, "|");
+			selected_3g = new int[tok.countTokens()];
+			for (int i=0; i<selected_3g.length; i++) {
+				final String uid = tok.nextToken();
+				if (!uid.equals("")) {
+					try {
+						selected_3g[i] = Integer.parseInt(uid);
+					} catch (Exception ex) {
+						selected_3g[i] = -1;
+					}
+				}
+			}
+			// Sort the array to allow using "Arrays.binarySearch" later
+			Arrays.sort(selected_3g);
+		}
+		try {
+			final PackageManager pkgmanager = ctx.getPackageManager();
+			final List<ApplicationInfo> installed = pkgmanager.getInstalledApplications(0);
+			final HashMap<Integer, DroidApp> map = new HashMap<Integer, DroidApp>();
+			final Editor edit = prefs.edit();
+			boolean changed = false;
+			String name = null;
+			String cachekey = null;
+			DroidApp app = null;
+			for (final ApplicationInfo apinfo : installed) {
+				app = map.get(apinfo.uid);
+				// filter applications which are not allowed to access the Internet
+				if (app == null && PackageManager.PERMISSION_GRANTED != pkgmanager.checkPermission(Manifest.permission.INTERNET, apinfo.packageName)) {
+					continue;
+				}
+				// try to get the application label from our cache - getApplicationLabel() is horribly slow!!!!
+				cachekey = "cache.label."+apinfo.packageName;
+				name = prefs.getString(cachekey, "");
+				if (name.length() == 0) {
+					// get label and put on cache
+					name = pkgmanager.getApplicationLabel(apinfo).toString();
+					edit.putString(cachekey, name);
+					changed = true;
+				}
+				if (app == null) {
+					app = new DroidApp();
+					app.uid = apinfo.uid;
+					app.names = new String[] { name };
+					map.put(apinfo.uid, app);
+				} else {
+					final String newnames[] = new String[app.names.length + 1];
+					System.arraycopy(app.names, 0, newnames, 0, app.names.length);
+					newnames[app.names.length] = name;
+					app.names = newnames;
+				}
+				// check if this application is selected
+				if (!app.selected_wifi && Arrays.binarySearch(selected_wifi, app.uid) >= 0) {
+					app.selected_wifi = true;
+				}
+				if (!app.selected_3g && Arrays.binarySearch(selected_3g, app.uid) >= 0) {
+					app.selected_3g = true;
+				}
+			}
+			if (changed) {
+				edit.commit();
+			}
+			/* add special applications to the list */
+			final DroidApp special[] = {
+				new DroidApp(SPECIAL_UID_ANY,"(Any application) - Same as selecting all applications", false, false),
+				new DroidApp(SPECIAL_UID_KERNEL,"(Kernel) - Linux kernel", false, false),
+				new DroidApp(android.os.Process.getUidForName("root"), "(root) - Applications running as root", false, false),
+				new DroidApp(android.os.Process.getUidForName("media"), "Media server", false, false),
+				new DroidApp(android.os.Process.getUidForName("vpn"), "VPN networking", false, false),
+				new DroidApp(android.os.Process.getUidForName("shell"), "Linux shell", false, false),
+			};
+			for (int i=0; i<special.length; i++) {
+				app = special[i];
+				if (app.uid != -1 && !map.containsKey(app.uid)) {
+					// check if this application is allowed
+					if (Arrays.binarySearch(selected_wifi, app.uid) >= 0) {
+						app.selected_wifi = true;
+					}
+					if (Arrays.binarySearch(selected_3g, app.uid) >= 0) {
+						app.selected_3g = true;
+					}
+					map.put(app.uid, app);
+				}
+			}
+			applications = new DroidApp[map.size()];
+			int index = 0;
+			for (DroidApp application : map.values()) applications[index++] = application;
+			return applications;
+		} catch (Exception e) {
+			alert(ctx, "error: " + e);
+		}
+		return null;
+	}
+	/**
+	 * Check if we have root access
+	 * @param ctx mandatory context
+     * @param showErrors indicates if errors should be alerted
+	 * @return boolean true if we have root
+	 */
+	public static boolean hasRootAccess(Context ctx, boolean showErrors) {
+		if (hasroot) return true;
+		final StringBuilder res = new StringBuilder();
+		try {
+			// Run an empty script just to check root access
+			if (runScriptAsRoot(ctx, "exit 0", res) == 0) {
+				hasroot = true;
+				return true;
+			}
+		} catch (Exception e) {
+		}
+		if (showErrors) {
+			alert(ctx, "Could not acquire root access.\n" +
+				"You need a rooted phone to run DroidWall.\n\n" +
+				"If this phone is already rooted, please make sure DroidWall has enough permissions to execute the \"su\" command.\n" +
+				"Error message: " + res.toString());
+		}
+		return false;
+	}
+    /**
+     * Runs a script, wither as root or as a regular user (multiple commands separated by "\n").
+	 * @param ctx mandatory context
+     * @param script the script to be executed
+     * @param res the script output response (stdout + stderr)
+     * @param timeout timeout in milliseconds (-1 for none)
+     * @return the script exit code
+     */
+	public static int runScript(Context ctx, String script, StringBuilder res, long timeout, boolean asroot) {
+		final File file = new File(ctx.getDir("bin",0), SCRIPT_FILE);
+		final ScriptRunner runner = new ScriptRunner(file, script, res, asroot);
+		runner.start();
+		try {
+			if (timeout > 0) {
+				runner.join(timeout);
+			} else {
+				runner.join();
+			}
+			if (runner.isAlive()) {
+				// Timed-out
+				runner.interrupt();
+				runner.join(150);
+				runner.destroy();
+				runner.join(50);
+			}
+		} catch (InterruptedException ex) {}
+		return runner.exitcode;
+	}
+    /**
+     * Runs a script as root (multiple commands separated by "\n").
+	 * @param ctx mandatory context
+     * @param script the script to be executed
+     * @param res the script output response (stdout + stderr)
+     * @param timeout timeout in milliseconds (-1 for none)
+     * @return the script exit code
+     */
+	public static int runScriptAsRoot(Context ctx, String script, StringBuilder res, long timeout) {
+		return runScript(ctx, script, res, timeout, true);
+    }
+    /**
+     * Runs a script as root (multiple commands separated by "\n") with a default timeout of 20 seconds.
+	 * @param ctx mandatory context
+     * @param script the script to be executed
+     * @param res the script output response (stdout + stderr)
+     * @param timeout timeout in milliseconds (-1 for none)
+     * @return the script exit code
+     * @throws IOException on any error executing the script, or writing it to disk
+     */
+	public static int runScriptAsRoot(Context ctx, String script, StringBuilder res) throws IOException {
+		return runScriptAsRoot(ctx, script, res, 40000);
+	}
+    /**
+     * Runs a script as a regular user (multiple commands separated by "\n") with a default timeout of 20 seconds.
+	 * @param ctx mandatory context
+     * @param script the script to be executed
+     * @param res the script output response (stdout + stderr)
+     * @param timeout timeout in milliseconds (-1 for none)
+     * @return the script exit code
+     * @throws IOException on any error executing the script, or writing it to disk
+     */
+	public static int runScript(Context ctx, String script, StringBuilder res) throws IOException {
+		return runScript(ctx, script, res, 40000, false);
+	}
+	/**
+	 * Asserts that the binary files are installed in the cache directory.
+	 * @param ctx context
+     * @param showErrors indicates if errors should be alerted
+	 * @return false if the binary files could not be installed
+	 */
+	public static boolean assertBinaries(Context ctx, boolean showErrors) {
+		boolean changed = false;
+		try {
+			// Check iptables_g1
+			File file = new File(ctx.getDir("bin",0), "iptables_g1");
+			if ((!file.exists()) && isARMv6()) {
+				copyRawFile(ctx, R.raw.iptables_g1, file, "755");
+				changed = true;
+			}
+			// Check iptables_n1
+			file = new File(ctx.getDir("bin",0), "iptables_n1");
+			if ((!file.exists()) && (!isARMv6())) {
+				copyRawFile(ctx, R.raw.iptables_n1, file, "755");
+				changed = true;
+			}
+			// Check busybox
+			file = new File(ctx.getDir("bin",0), "busybox_g1");
+			if (!file.exists()) {
+				copyRawFile(ctx, R.raw.busybox_g1, file, "755");
+				changed = true;
+			}
+			if (changed) {
+				Toast.makeText(ctx, R.string.status_install_success, Toast.LENGTH_LONG).show();
+			}
+		} catch (Exception e) {
+			if (showErrors) alert(ctx, "Error installing binary files: " + e);
+			return false;
+		}
+		return true;
+	}
+	/**
+	 * Check if the firewall is enabled
+	 * @param ctx mandatory context
+	 * @return boolean
+	 */
+	public static boolean isEnabled(Context ctx) {
+		if (ctx == null) return false;
+		return ctx.getSharedPreferences(PREFS_NAME, 0).getBoolean(PREF_ENABLED, false);
+	}
+	
+	/**
+	 * Defines if the firewall is enabled and broadcasts the new status
+	 * @param ctx mandatory context
+	 * @param enabled enabled flag
+	 */
+	public static void setEnabled(Context ctx, boolean enabled) {
+		if (ctx == null) return;
+		final SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, 0);
+		if (prefs.getBoolean(PREF_ENABLED, false) == enabled) {
+			return;
+		}
+		final Editor edit = prefs.edit();
+		edit.putBoolean(PREF_ENABLED, enabled);
+		if (!edit.commit()) {
+			alert(ctx, "Error writing to preferences");
+			return;
+		}
+		/* notify */
+		final Intent message = new Intent(Api.STATUS_CHANGED_MSG);
+        message.putExtra(Api.STATUS_EXTRA, enabled);
+        ctx.sendBroadcast(message);
+	}
+	/**
+	 * Called when an application in removed (un-installed) from the system.
+	 * This will look for that application in the selected list and update the persisted values if necessary
+	 * @param ctx mandatory app context
+	 * @param uid UID of the application that has been removed
+	 */
+	public static void applicationRemoved(Context ctx, int uid) {
+		final SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, 0);
+		final Editor editor = prefs.edit();
+		// allowed application names separated by pipe '|' (persisted)
+		final String savedUids_wifi = prefs.getString(PREF_WIFI_UIDS, "");
+		final String savedUids_3g = prefs.getString(PREF_3G_UIDS, "");
+		final String uid_str = uid + "";
+		boolean changed = false;
+		// look for the removed application in the "wi-fi" list
+		if (savedUids_wifi.length() > 0) {
+			final StringBuilder newuids = new StringBuilder();
+			final StringTokenizer tok = new StringTokenizer(savedUids_wifi, "|");
+			while (tok.hasMoreTokens()) {
+				final String token = tok.nextToken();
+				if (uid_str.equals(token)) {
+					Log.d("DroidWall", "Removing UID " + token + " from the wi-fi list (package removed)!");
+					changed = true;
+				} else {
+					if (newuids.length() > 0) newuids.append('|');
+					newuids.append(token);
+				}
+			}
+			if (changed) {
+				editor.putString(PREF_WIFI_UIDS, newuids.toString());
+			}
+		}
+		// look for the removed application in the "3g" list
+		if (savedUids_3g.length() > 0) {
+			final StringBuilder newuids = new StringBuilder();
+			final StringTokenizer tok = new StringTokenizer(savedUids_3g, "|");
+			while (tok.hasMoreTokens()) {
+				final String token = tok.nextToken();
+				if (uid_str.equals(token)) {
+					Log.d("DroidWall", "Removing UID " + token + " from the 3G list (package removed)!");
+					changed = true;
+				} else {
+					if (newuids.length() > 0) newuids.append('|');
+					newuids.append(token);
+				}
+			}
+			if (changed) {
+				editor.putString(PREF_3G_UIDS, newuids.toString());
+			}
+		}
+		// if anything has changed, save the new prefs...
+		if (changed) {
+			editor.commit();
+			if (isEnabled(ctx)) {
+				// .. and also re-apply the rules if the firewall is enabled
+				applySavedIptablesRules(ctx, false);
+			}
+		}
+	}
+
+    /**
+     * Small structure to hold an application info
+     */
+	public static final class DroidApp {
+		/** linux user id */
+    	int uid;
+    	/** application names belonging to this user id */
+    	String names[];
+    	/** indicates if this application is selected for wifi */
+    	boolean selected_wifi;
+    	/** indicates if this application is selected for 3g */
+    	boolean selected_3g;
+    	/** toString cache */
+    	String tostr;
+    	
+    	public DroidApp() {
+    	}
+    	public DroidApp(int uid, String name, boolean selected_wifi, boolean selected_3g) {
+    		this.uid = uid;
+    		this.names = new String[] {name};
+    		this.selected_wifi = selected_wifi;
+    		this.selected_3g = selected_3g;
+    	}
+    	/**
+    	 * Screen representation of this application
+    	 */
+    	@Override
+    	public String toString() {
+    		if (tostr == null) {
+        		final StringBuilder s = new StringBuilder();
+        		if (uid > 0) s.append(uid + ": ");
+        		for (int i=0; i<names.length; i++) {
+        			if (i != 0) s.append(", ");
+        			s.append(names[i]);
+        		}
+        		s.append("\n");
+        		tostr = s.toString();
+    		}
+    		return tostr;
+    	}
+    }
+    /**
+     * Small internal structure used to hold log information
+     */
+	private static final class LogInfo {
+		private int totalBlocked; // Total number of packets blocked
+		private HashMap<String, Integer> dstBlocked; // Number of packets blocked per destination IP address
+		private LogInfo() {
+			this.dstBlocked = new HashMap<String, Integer>();
+		}
+	}
+	/**
+	 * Internal thread used to execute scripts (as root or not).
+	 */
+	private static final class ScriptRunner extends Thread {
+		private final File file;
+		private final String script;
+		private final StringBuilder res;
+		private final boolean asroot;
+		public int exitcode = -1;
+		private Process exec;
+		
+		/**
+		 * Creates a new script runner.
+		 * @param file temporary script file
+		 * @param script script to run
+		 * @param res response output
+		 * @param asroot if true, executes the script as root
+		 */
+		public ScriptRunner(File file, String script, StringBuilder res, boolean asroot) {
+			this.file = file;
+			this.script = script;
+			this.res = res;
+			this.asroot = asroot;
+		}
+		@Override
+		public void run() {
+			try {
+				file.createNewFile();
+				final String abspath = file.getAbsolutePath();
+				// make sure we have execution permission on the script file
+				Runtime.getRuntime().exec("chmod 777 "+abspath).waitFor();
+				// Write the script to be executed
+				final OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(file));
+				if (new File("/system/bin/sh").exists()) {
+					out.write("#!/system/bin/sh\n");
+				}
+				out.write(script);
+				if (!script.endsWith("\n")) out.write("\n");
+				out.write("exit\n");
+				out.flush();
+				out.close();
+				if (this.asroot) {
+					// Create the "su" request to run the script
+					exec = Runtime.getRuntime().exec("su -c "+abspath);
+				} else {
+					// Create the "sh" request to run the script
+					exec = Runtime.getRuntime().exec("sh "+abspath);
+				}
+				InputStreamReader r = new InputStreamReader(exec.getInputStream());
+				final char buf[] = new char[1024];
+				int read = 0;
+				// Consume the "stdout"
+				while ((read=r.read(buf)) != -1) {
+					if (res != null) res.append(buf, 0, read);
+				}
+				// Consume the "stderr"
+				r = new InputStreamReader(exec.getErrorStream());
+				read=0;
+				while ((read=r.read(buf)) != -1) {
+					if (res != null) res.append(buf, 0, read);
+				}
+				// get the process exit code
+				if (exec != null) this.exitcode = exec.waitFor();
+			} catch (InterruptedException ex) {
+				if (res != null) res.append("\nOperation timed-out");
+			} catch (Exception ex) {
+				if (res != null) res.append("\n" + ex);
+			} finally {
+				destroy();
+			}
+		}
+		/**
+		 * Destroy this script runner
+		 */
+		public synchronized void destroy() {
+			if (exec != null) exec.destroy();
+			exec = null;
+		}
+	}
+}



More information about the tor-commits mailing list