droidwall源码实现分析

Droid Wall是什么

Droid Wall是Android平台上一款强大的防火墙前端软件,与iptables配套使用,让你从此开着gprs不烧钱,如果你没有不限流量包月,那么它必然会被明智的你安装到手机里,这个不到40kb软件的伟大之处在于让你来决定哪些软件可以联接上网,在你允许的软件前面打上勾,而且支持按3g/wifi区分网络。
软件需要Root权限(就是手机需要破解)。支持黑名单与白名单两种模式,可设定允许(白名单)访问的程序或禁止(单名单)访问的程序。
——百度百科

Iptables是什么

iptables 是与最新的 3.5 版本 Linux 内核集成的 IP 信息包过滤系统。如果 Linux 系统连接到因特网或 LAN、服务器或连接 LAN 和因特网的代理服务器, 则该系统有利于在 Linux 系统上更好地控制 IP 信息包过滤和防火墙配置。
——百度百科

root@android:/ # iptables –help
iptables v1.4.11.1

Usage: iptables -[ACD] chain rule-specification [options]
iptables -I chain [rulenum] rule-specification [options]
iptables -R chain rulenum rule-specification [options]
iptables -D chain rulenum [options]
iptables -[LS] [chain [rulenum]] [options]
iptables -[FZ] [chain] [options]
iptables -[NX] chain
iptables -E old-chain-name new-chain-name
iptables -P chain target [options]
iptables -h (print this help information)

由此可见,测试的android平板系统中,是支持iptables的。

源码分析

Droid Wall是一个开源的android项目,其核心是系统中的iptables指令功能。拿到Droid Wall源码之后,下面我们来一步一步的分析它的实现方式。

代码结构

源码目录十分简单:

除了Api.java之外,其他类代码量都很少。所有的代码功能单一,非常好区分:

可以看出,整个DroidWall的核心功能是在Api.java中实现,其他类都是界面、说明、规则配置等实现。

核心代码

Iptables是DroidWall功能实现的核心,这里主要简单分析一下Api类中是如何调用iptables来执行命令的即可大致掌握整个应用的方向。

大体观感:

Api.java代码量在一千行以右,主要是围绕Android系统内部的iptables工具,执行相关的脚本、来达到控制整机的网络的目的,由于有针对某个应用的网络控制功能,所以这里还要有获取机器应用列表等接口操作。从Api类相关的类图和函数列表可以看出,该类有以下几个特点:

⦁ 内部类,该类有三个内部类,功能分别是日志结构、Android应用信息结构和脚本执行器;

⦁ 该类实际上是个静态工具类,它对外提供的都是静态方法;

⦁ 所有的静态方法中,以合成规则和执行脚本类型的方法为主,其余的方法则多事纯工具类型:使能操作、判断root、日志操作等;

实现分析

⦁ 脚本执行

我们跟踪runScriptXXX 相关的函数,到最后总是会跟踪到内部类ScriptRunner中,该类是具体执行脚本的工具实现类。
该类是Thread的拓展子类,那么我们注意一下它的构造和run函数就会有一定的收获:
构造如下:

/**
* 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)

构造上可以看出,输入参数有文件属性参数、脚本内容字符串、脚本响应的记录载体StringBuilder和root执行标志。后两个参数很好理解,前面两个参数还要看具体的实现分析和调用者调用方式的佐证才确定。

核心run函数如下:

@Override
public void run() {
    try {
        //文件参数调用了创建新文件,这意味着调用者传进来的不会是一个已存在的文件对象
        file.createNewFile();
        final String abspath = file.getAbsolutePath();
        //执行chmod命令 添加权限
        // 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()) {
            //检查sh是否存在后 很熟悉的脚本头
            out.write("#!/system/bin/sh\n");
        }
        //写入脚本内容字符串
        out.write(script);
        if (!script.endsWith("\n")){
            out.write("\n");
        }
        out.write("exit\n");
        out.flush();
        out.close();
        //根据root标志 执行脚本文件
        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);
        }
        //下面应该是脚本执行响应的接收 对res参数写入响应信息 这应该算是个输出参数了
        final InputStream stdout = exec.getInputStream();
        final InputStream stderr = exec.getErrorStream();
        final byte buf[] = new byte[8192];
        int read = 0;
        while (true) {
            final Process localexec = exec;
            if (localexec == null) break;
            try {
                // get the process exit code - will raise IllegalThreadStateException if still running
                this.exitcode = localexec.exitValue();
            } catch (IllegalThreadStateException ex) {
                // The process is still running
            }
            // Read stdout
            if (stdout.available() > 0) {
                read = stdout.read(buf);
                if (res != null) {
                    res.append(new String(buf, 0, read));
                }
            }
            // Read stderr
            if (stderr.available() > 0) {
                read = stderr.read(buf);
                if (res != null) {
                    res.append(new String(buf, 0, read));
                }
            }
            if (this.exitcode != -1) {
                // finished
                break;
            }
            // Sleep for the next round
            Thread.sleep(50);
        }
    } catch (InterruptedException ex) {
        if (res != null) {
            res.append("\nOperation timed-out");
        }
    } catch (Exception ex) {
        if (res != null) {
            res.append("\n" + ex);
        }
    } finally {
        destroy();
    }
}

经过上述的观察和注释,基本可以确认,调用者需要new一个脚本文件出来,然后传递脚本内容、响应参数和root标志来执行脚本命令。ScriptRunner作为一个单独开启的线程,创建脚本文件-> 添加执行权限-> 写入脚本命令-> 根据root标志选择执行方式执行->将执行响应写入响应的StringBuilder中,流程基本如此。该处涉及到的小知识点:

⦁ 执行脚本文件采用 Runtime.getRuntime().exec(“su -c “+abspath) 方法;

⦁ 读取脚本执行响应的方法,采用 Process对象的以下方法组合使用:

final InputStream stdout = exec.getInputStream();

final InputStream stderr = exec.getErrorStream();

this.exitcode = localexec.exitValue();

那么我们可以查找一下ScriptRunner的调用者,看看它的调用方式来佐证一下上面的分析:

private static final String SCRIPT_FILE = "droidwall.sh";
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();
    …
}

基本可以诠释上面的分析是正确的。

⦁ 规则合成

在一批规则相关的函数中,最终找到了合成规则的函数:

/**
 * 合成规则
 * 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) {
    final StringBuilder res = new StringBuilder();
    try {
        assertBinaries(ctx, showErrors);
        // Custom "shutdown" script
        final String customScript = ctx.getSharedPreferences(Api.PREFS_NAME, 0).getString(Api.PREF_CUSTOMSCRIPT2, "");
        final StringBuilder script = new StringBuilder();
        script.append(scriptHeader(ctx));
        script.append("" +
                "$IPTABLES -F\n" +
                "$IPTABLES -X\n" +
                "$IPTABLES -Z\n" +
                "$IPTABLES -P INPUT ACCEPT\n" +
                "$IPTABLES -P OUTPUT ACCEPT\n" +
                "$IPTABLES -P FORWARD ACCEPT\n"
                "");
        if (customScript.length() > 0) {
            script.append("\n# BEGIN OF CUSTOM SCRIPT (user-defined)\n");
            script.append(customScript);
            script.append("\n# END OF CUSTOM SCRIPT (user-defined)\n\n");
        }
        int code = runScriptAsRoot(ctx, script.toString(), 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;
    }
}

参数上看会有些不解,因为只有个Context和一个显示错误信息的标志,看实现才清楚在DroidWall中,规则应该是保存在SharedPreferences中的,界面上更新了规则(比如添加应用链接网络的黑名单)之后,会将规则内容保存在SharedPreferences中,而后再调用Api中的相关接口去合成规则、执行iptables实现新规则。

以上是DroidWall在应用层利用iptables实现防火墙的方式,更加具体和深入的分析,则是对iptables命令的使用了。

感谢您赏个荷包蛋~