在APP中如何使用系统属性执行shell脚本

775 阅读6分钟

背景

前不久有一个白名单的需求,之前的实现方式由APP来调用framework中添加的接口,再调用到system/netd/ ,之后在执行iptables的命令来设置防火墙规则,详情可见这篇: Android11 iptables实现网络防火墙

但是用这种方式后发现,当防火墙的需求发生变化,每次在firewallController中修改iptables命令,都需要重新增量编译,浪费调试时间,于是有了该篇

本篇主要内容不是为实现白名单需求,而是介绍如何在APP中通过系统属性调用Shell脚本

方案

本次方案的具体方式为

  1. APP添加白名单时,将该规则按指定格式添加到一个文件中,假设文件保存在/data/netWhiteList.txt
    # 格式
    IP:192.168.1.1 Netmask:24 Port:8000
    
  2. 编写Shell脚本,内容是大概为: 根据白名单文件,遍历文件内规则,执行iptables命令,设置防火墙规则
  3. Shell脚本添加到rc文件中,设置为service
  4. 设置Shell脚本的se权限
  5. 通过系统属性来调用ctl.start拉起service,执行特定的脚本,还可以指定参数

属性 ctrl.start 和 ctrl.stop 是用来启动和停止服务。这里的服务是指定义在 rc 后缀文件中的服务。当我们向 ctrl.start 属性写入一个值时,属性服务将使用该属性值作为服务名找到该服务,启动该服务。这项服务的启动结果将会放入 init.svc.<服务名> 属性中,可以通过查询这个属性值,以确定服务是否已经启动。

平台

展锐 T618 Android11

实现

  1. 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);
        }
    }
    
  2. 编写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 $1}' | awk -F ':' '{print $2}')
            netmask=$(echo "$line" | awk -F ' ' '{print $2}' | awk -F ':' '{print $2}')
            port=$(echo "$line" | awk -F ' ' '{print $3}' | awk -F ':' '{print $2}')
            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 $1}' | awk -F ':' '{print $2}')
            netmask=$(echo "$line" | awk -F ' ' '{print $2}' | awk -F ':' '{print $2}')
            port=$(echo "$line" | awk -F ' ' '{print $3}' | awk -F ':' '{print $2}')
            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 $1}' | awk -F ':' '{print $2}')
            netmask=$(echo "$line" | awk -F ' ' '{print $2}' | awk -F ':' '{print $2}')
            port=$(echo "$line" | awk -F ' ' '{print $3}' | awk -F ':' '{print $2}')
            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 $1\\/$2 -j DROP
        iptables -I FORWARD \\! -d $1\\/$2 -j DROP
    }
    
    function remove_drop_rule() {
        iptables -D OUTPUT \\! -d $1\\/$2 -j DROP
        iptables -D FORWARD \\! -d $1\\/$2 -j DROP
    }
    
    function clear_rules() {
        iptables -F
    }
    
    echo "firewall_ctl count: $# param: $1" >> $LOG_FILE
    if [ $# -eq 0 ]; then
        load_rules $PARSE_FILE
    elif [ $1 == "add" ]; then
        add_rule
    elif [ $1 == "remove" ]; then
        remove_rule
    elif [ $1 == "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 \\
    
  3. 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脚本了

  4. 添加脚本的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)
    
  5. 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进去会方便很多

总结一下:

  1. 编写shell脚本,push到设备/system/bin目录下,手动执行看是否可生效,脚本文件是否正确
  2. 将服务添加到rc文件中
  3. 将脚本放到特定目录下,添加SE权限,执行make selinux_policy 看权限是否有错误,避免整编失败浪费时间
  4. 整编验证

问题

  • 服务调用了但是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脚本的顶部格式了