背景
前不久有一个白名单的需求,之前的实现方式由APP来调用framework
中添加的接口,再调用到system/netd/
,之后在执行iptables
的命令来设置防火墙规则,详情可见这篇: Android11 iptables实现网络防火墙
但是用这种方式后发现,当防火墙的需求发生变化,每次在firewallController
中修改iptables
命令,都需要重新增量编译,浪费调试时间,于是有了该篇
本篇主要内容不是为实现白名单需求,而是介绍如何在APP种通过系统属性调用Shell脚本
方案
本次方案的具体方式为
- 当
APP
添加白名单时,将该规则按指定格式添加到一个文件中,假设文件保存在/`data/netWhiteList.txt
# 格式
IP:192.168.1.1 Netmask:24 Port:8000
- 编写
Shell
脚本,内容是大概为: 根据白名单文件,遍历文件内规则,执行iptables
命令,设置防火墙规则 - 将
Shell
脚本添加到rc
文件中,设置为service
- 设置
Shell
脚本的se
权限 - 通过系统属性来调用
ctl.start
拉起service
,执行特定的脚本,还可以指定参数
属性 ctrl.start
和 ctrl.stop
是用来启动和停止服务。这里的服务是指定义在 rc
后缀文件中的服务。当我们向 ctrl.start
属性写入一个值时,属性服务将使用该属性值作为服务名找到该服务,启动该服务。这项服务的启动结果将会放入 init.svc.<服务名>
属性中,可以通过查询这个属性值,以确定服务是否已经启动。
平台
展锐 T618 Android11
实现
-
APP
中增加规则,向/data/netWhiteList.txt
文件中按指定格式写入规则,记得换行// all white list file public static final String WHITE_LIST_FILE_PATH = "/data/networkWhiteList.txt"; // temp add rule public static final String WHITE_LIST_TEMP_ADD_FILE_PATH = "/data/temp_add.txt"; public void writeRuleToFile(String rule) { if (TextUtils.isEmpty(rule)) { return; } FileWriter writer; FileWriter tempWriter; File file = new File(Constants.WHITE_LIST_FILE_PATH); File tempFile = new File(Constants.WHITE_LIST_TEMP_ADD_FILE_PATH); try { if (!file.exists()) { file.createNewFile(); } if (!tempFile.exists()) { tempFile.createNewFile(); } } catch (IOException e) { throw new RuntimeException(e); } if (!file.exists()) { LogUtil.e(TAG, "can't create new File: " + Constants.WHITE_LIST_FILE_PATH); } try { writer = new FileWriter(Constants.WHITE_LIST_FILE_PATH); tempWriter = new FileWriter(Constants.WHITE_LIST_TEMP_ADD_FILE_PATH); writer.append(rule).append("\n"); writer.flush(); tempWriter.write(rule); tempWriter.write("\n"); LogUtil.d(TAG, "write rule: " + rule + " over"); doFirewallCtl(ACTION_ADD); writer.close(); tempWriter.close(); tempFile.delete(); } catch (IOException e) { throw new RuntimeException(e); } }
-
编写
Shell
脚本,我对Shell
脚本实际写的不多,找ChatGPT
写完改改还是很是能用,位置的话根据项目情况放置,我这边放在源码/device/sprd/mpool/xxx/module
目录下,只要能拷贝到设备中就行,shell
脚本内容如下:#!/system/bin/sh PARSE_FILE=/data/networkWhiteList.txt TEMP_ADD_FILE=/data/temp_add.txt TEMP_REMOVE_FILE=/data/temp_delete.txt LOG_FILE=/sdcard/firewall_log.txt function load_rules() { while read -r line do ip=$(echo "$line" | awk -F ' ' '{print }' | awk -F ':' '{print }') netmask=$(echo "$line" | awk -F ' ' '{print }' | awk -F ':' '{print }') port=$(echo "$line" | awk -F ' ' '{print }' | awk -F ':' '{print }') add_drop_rule $ip $netmask echo "load rule IP: $ip Netmask: $netmask Port: $port $(date "+%Y-%m-%d %H:%M:%S")" >> $LOG_FILE # fi done < $PARSE_FILE } function add_rule() { do ip=$(echo "$line" | awk -F ' ' '{print }' | awk -F ':' '{print }') netmask=$(echo "$line" | awk -F ' ' '{print }' | awk -F ':' '{print }') port=$(echo "$line" | awk -F ' ' '{print }' | awk -F ':' '{print }') add_drop_rule $ip $netmask echo "add rule IP: $ip Netmask: $netmask Port: $port $(date "+%Y-%m-%d %H:%M:%S")" >> $LOG_FILE # fi done < $TEMP_ADD_FILE } function remove_rule() { do ip=$(echo "$line" | awk -F ' ' '{print }' | awk -F ':' '{print }') netmask=$(echo "$line" | awk -F ' ' '{print }' | awk -F ':' '{print }') port=$(echo "$line" | awk -F ' ' '{print }' | awk -F ':' '{print }') remove_drop_rule $ip $netmask echo "remove rule IP: $ip Netmask: $netmask Port: $port $(date "+%Y-%m-%d %H:%M:%S")" >> $LOG_FILE # fi done < $TEMP_REMOVE_FILE } function add_drop_rule() { iptables -I OUTPUT \! -d \/ -j DROP iptables -I FORWARD \! -d \/ -j DROP } function remove_drop_rule() { iptables -D OUTPUT \! -d \/ -j DROP iptables -D FORWARD \! -d \/ -j DROP } function clear_rules() { iptables -F } echo "firewall_ctl count: $# param: " >> $LOG_FILE if [ $# -eq 0 ]; then load_rules $PARSE_FILE elif [ == "add" ]; then add_rule elif [ == "remove" ]; then remove_rule elif [ == "clear" ]; then clear_rules fi
脚本后可跟添加,删除,清空等参数,执行不同的操作,需要注意脚本头部的
#!/system/bin/sh
,之前使用时遇到过写成#!/system/bin/bash
和#!/bin/bash
,在设备中手动执行可以,但是使用属性调用时会失败的问题写完脚本后记得找个
mk
文件做文件拷贝PRODUCT_COPY_FILES += \ $(LOCAL_PATH)/firewall_ctl.sh:system/bin/firewall_ctl.sh \
-
将
Shell
脚本添加到rc
文件中,设置为service
,相关的rc
文件内容#net white list control # load rules from file service firewall /system/bin/firewall_ctl.sh class main group root system shell user root disabled oneshot # add rule service firewall_add /system/bin/firewall_ctl.sh add class main user root group root system shell disabled oneshot # remove rule service firewall_remove /system/bin/firewall_ctl.sh remove class main user root group root system shell disabled oneshot # clear rules service firewall_clear /system/bin/firewall_ctl.sh clear class main user root group root system shell disabled oneshot
写了4个
service
,后面通过系统属性执行不同的服务就会执行带不同参数的sh
脚本了 -
添加脚本的
SE
权限,两个文件分别是:source_code/system/sepolicy/vendor/file_contexts source_code/system/sepolicy/vendor/firewall_ctl.te
添加内容,具体根据实际情况修改:
# /system/sepolicy/vendor/file_contexts
/system/bin/firewall_ctl.sh u:object_r:firewall_ctl_exec:s0
# system/sepolicy/vendor/firewall_ctl.te
type firewall_ctl, domain;
type firewall_ctl_exec, system_file_type, exec_type, file_type;
typeattribute firewall_ctl coredomain;
init_daemon_domain(firewall_ctl)
-
在
APP
中使用系统属性来启动服务,执行特定的脚本操作private void doFirewallCtl(int action) { switch (action) { case ACTION_LOAD: SystemProperties.set("ctl.start", "firewall"); break; case ACTION_ADD: SystemProperties.set("ctl.start", "firewall_add"); break; case ACTION_REMOVE: SystemProperties.set("ctl.start", "firewall_remove"); break; case ACTION_CLEAR: SystemProperties.set("ctl.start", "firewall_clear"); break; } }
之后如果只是修改iptables
的设置命令,就可以直接修改shell
脚本了,测试时pull
出来修改完后再push
进去会方便很多
总结一下:
- 编写
shell
脚本,push
到设备/system/bin
目录下,手动执行看是否可生效,脚本文件是否正确 - 将服务添加到
rc
文件中 - 将脚本放到特定目录下,添加
SE
权限,执行make selinux_policy
看权限是否有错误,避免整编失败浪费时间 - 整编验证
问题
-
服务调用了但是
Shell
脚本没执行看脚本有没有执行可以在里面重定向到文件来
debug
,除了看android log
外可以看下kernel log
,比如出现:02E70 <14> [ 114.843612][11-24 17:02:18.843] init: starting service 'firewall_clear'... 02E7C <11> [ 114.848335][11-24 17:02:18.848] init: cannot execv('/system/bin/firewall_ctl.sh'). See the 'Debugging init' section of init's README.md for tips: No such file or directory 02E7D <14> [ 114.849522][11-24 17:02:18.849] init: Control message: Processed ctl.start for 'firewall_clear' from pid: 2315 (com.jathine.netwhitelist) 02E7E <38> [ 114.850086][11-24 17:02:18.850] type=1400 audit(1700816539.075:839): avc: denied { write } for comm="ne.netwhitelist" name="networkWhiteList.txt" dev="dm-5" ino=4709 scontext=u:r:system_app:s0 tcontext=u:object_r:system_data_root_file:s0 tclass=file permissive=1 02E7F <38> [ 114.850344][11-24 17:02:18.850] type=1400 audit(1700816539.075:840): avc: denied { open } for comm="ne.netwhitelist" path="/data/networkWhiteList.txt" dev="dm-5" ino=4709 scontext=u:r:system_app:s0 tcontext=u:object_r:system_data_root_file:s0 tclass=file permissive=1 02E80 <38> [ 114.850540][11-24 17:02:18.850] type=1400 audit(1700816539.075:841): avc: denied { getattr } for comm="ne.netwhitelist" path="/data/networkWhiteList.txt" dev="dm-5" ino=4709 scontext=u:r:system_app:s0 tcontext=u:object_r:system_data_root_file:s0 tclass=file permissive=1 02E81 <14> [ 114.852383][11-24 17:02:18.852] init: Service 'firewall_clear' (pid 3366) exited with status 127 oneshot service took 0.005000 seconds in background 02E82 <14> [ 114.852426][11-24 17:02:18.852] init: Sending signal 9 to service 'firewall_clear' (pid 3366) process group...
如果在
adb
中使用sh
执行脚本可以运行,但是属性调用不生效,要看下Shell
脚本的顶部格式了