/**
* 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 .
*
* @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 uidsWifi, List 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 uids_wifi = new LinkedList();
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 uids_3g = new LinkedList();
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/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 map = new HashMap();
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 0) {
// Check which applications are allowed
final StringTokenizer tok = new StringTokenizer(savedUids_3g, "|");
selected_3g = new int[tok.countTokens()];
for (int i=0; i installed = pkgmanager.getInstalledApplications(0);
final HashMap map = new HashMap();
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= 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");
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");
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 dstBlocked; // Number of packets blocked per destination IP address
private LogInfo() {
this.dstBlocked = new HashMap();
}
}
/**
* 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;
}
}
}