iptables 是一个在Linux内核集成的IP信息包过滤系统,用于控制IP信息包过滤和防火墙配置,通过存储在数据包过滤表中的规则,在不同的链中做出数据包过滤决定。
Android 是基于 Linux 的操作系统,支持 Iptables。执行 Iptables 命令需要 root 权限。

Android netd 的自定义链

以 INPUT 链为例子,Andorid 会自定义 bw_INPUT、fw_INPUT 等链,链到 INPUT 链上

# iptables -L 

Chain INPUT (policy ACCEPT)
target     prot opt source               destination                  
bw_INPUT   all  --  anywhere             anywhere            
fw_INPUT   all  --  anywhere             anywhere    

Controllers 执行 init() 方法时会先执行 initChildChains 方法, 在 INPUT 链下创造子链

void Controllers::initChildChains() {
    /*
     * This is the only time we touch top-level chains in iptables; controllers
     * should only mutate rules inside of their children chains, as created by
     * the constants above.
     *
     * Modules should never ACCEPT packets (except in well-justified cases);
     * they should instead defer to any remaining modules using RETURN, or
     * otherwise DROP/REJECT.
     */

    // Create chains for child modules.
    createChildChains(V4V6, "filter", "INPUT", FILTER_INPUT, true);
    createChildChains(V4V6, "filter", "FORWARD", FILTER_FORWARD, true);
    createChildChains(V4V6, "raw", "PREROUTING", RAW_PREROUTING, true);
    createChildChains(V4V6, "mangle", "FORWARD", MANGLE_FORWARD, true);
    createChildChains(V4V6, "mangle", "INPUT", MANGLE_INPUT, true);
    createChildChains(V4, "nat", "PREROUTING", NAT_PREROUTING, true);
    createChildChains(V4, "nat", "POSTROUTING", NAT_POSTROUTING, true);

    createChildChains(V4, "filter", "OUTPUT", FILTER_OUTPUT, false);
    createChildChains(V6, "filter", "OUTPUT", FILTER_OUTPUT, false);
    createChildChains(V4, "mangle", "POSTROUTING", MANGLE_POSTROUTING, false);
    createChildChains(V6, "mangle", "POSTROUTING", MANGLE_POSTROUTING, false);
}

FILTER_INPUT 是子链的集合,定义顺序,会从上往下添加,先添加的子链具有较高的优先级,以 FILTER_INPUT 为例,FILTER_INPUT 定义为:

/**
 * List of module chains to be created, along with explicit ordering. ORDERING
 * IS CRITICAL, AND SHOULD BE TRIPLE-CHECKED WITH EACH CHANGE.
 */
static const std::vector<const char*> FILTER_INPUT = {
        // Bandwidth should always be early in input chain, to make sure we
        // correctly count incoming traffic against data plan.
        BandwidthController::LOCAL_INPUT,
        FirewallController::LOCAL_INPUT,
};

createChildChains 方法定义如下

void Controllers::createChildChains(IptablesTarget target, const char* table,
                                    const char* parentChain,
                                    const std::vector<const char*>& childChains,
                                    bool exclusive) {
    std::string command = StringPrintf("*%s\n", table);

    // We cannot just clear all the chains we create because vendor code modifies filter OUTPUT and
    // mangle POSTROUTING directly. So:
    //
    // - If we're the exclusive owner of this chain, simply clear it entirely.
    // - If not, then list the chain's current contents to ensure that if we restart after a crash,
    //   we leave the existing rules alone in the positions they currently occupy. This is faster
    //   than blindly deleting our rules and recreating them, because deleting a rule that doesn't
    //   exists causes iptables-restore to quit, which takes ~30ms per delete. It's also more
    //   correct, because if we delete rules and re-add them, they'll be in the wrong position with
    //   regards to the vendor rules.
    //
    // TODO: Make all chains exclusive once vendor code uses the oem_* rules.
    std::set<std::string> existingChildChains;
    if (exclusive) {
        // Just running ":chain -" flushes user-defined chains, but not built-in chains like INPUT.
        // Since at this point we don't know if parentChain is a built-in chain, do both.
        StringAppendF(&command, ":%s -\n", parentChain);
        StringAppendF(&command, "-F %s\n", parentChain);
    } else {
        existingChildChains = findExistingChildChains(target, table, parentChain);
    }

    for (const auto& childChain : childChains) {
        // Always clear the child chain.
        StringAppendF(&command, ":%s -\n", childChain);
        // But only add it to the parent chain if it's not already there.
        if (existingChildChains.find(childChain) == existingChildChains.end()) {
            StringAppendF(&command, CHILD_CHAIN_TEMPLATE, parentChain, childChain);
        }
    }
    command += "COMMIT\n";
    execIptablesRestore(target, command);
}

该方法的核心思路就是拼 command 字符串,语法与 iptables-save 生成文件的语法类似,最后调用 execIptablesRestore 执行 command,向 iptables 发送命令
此处生成的 command 为

04-03 15:31:55.872  1106  1106 E Netd    :  *filter
04-03 15:31:55.872  1106  1106 E Netd    : :INPUT -
04-03 15:31:55.872  1106  1106 E Netd    : -F INPUT
04-03 15:31:55.872  1106  1106 E Netd    : :bw_INPUT -
04-03 15:31:55.872  1106  1106 E Netd    : -A INPUT -j bw_INPUT
04-03 15:31:55.872  1106  1106 E Netd    : :fw_INPUT -
04-03 15:31:55.872  1106  1106 E Netd    : -A INPUT -j fw_INPUT
04-03 15:31:55.872  1106  1106 E Netd    : COMMIT

NetdConstants.cpp 的 execIptablesRestore 方法

execIptablesRestore 方法的实现定义在 NetdConstants.cpp

int execIptablesRestoreWithOutput(IptablesTarget target, const std::string& commands,
                                  std::string *output) {
    return android::net::gCtls->iptablesRestoreCtrl.execute(target, commands, output);
}

int execIptablesRestore(IptablesTarget target, const std::string& commands) {
    return execIptablesRestoreWithOutput(target, commands, nullptr);
}

本质上是调用 IptablesRestoreController 的 execute 方法

IptablesRestoreController 的 execute 方法

int IptablesRestoreController::execute(const IptablesTarget target, const std::string& command,
                                       std::string *output) {
    std::lock_guard lock(mLock);

    std::string buffer;
    if (output == nullptr) {
        output = &buffer;
    } else {
        output->clear();
    }

    int res = 0;
    if (target == V4 || target == V4V6) {
        res |= sendCommand(IPTABLES_PROCESS, command, output);
    }
    if (target == V6 || target == V4V6) {
        res |= sendCommand(IP6TABLES_PROCESS, command, output);
    }
    return res;
}

sendCommand 方法定义如下:

int IptablesRestoreController::sendCommand(const IptablesProcessType type,
                                           const std::string& command,
                                           std::string *output) {
   std::unique_ptr<IptablesProcess> *process =
           (type == IPTABLES_PROCESS) ? &mIpRestore : &mIp6Restore;


    // We might need to fork a new process if we haven't forked one yet, or
    // if the forked process terminated.
    //
    // NOTE: For a given command, this is the last point at which we try to
    // recover from a child death. If the child dies at some later point during
    // the execution of this method, we will receive an EPIPE and return an
    // error. The command will then need to be retried at a higher level.
    IptablesProcess *existingProcess = process->get();
    if (existingProcess != nullptr && !existingProcess->outputReady()) {
        existingProcess->stop();
        existingProcess = nullptr;
    }

    if (existingProcess == nullptr) {
        // Fork a new iptables[6]-restore process.
        IptablesProcess *newProcess = IptablesRestoreController::forkAndExec(type);
        if (newProcess == nullptr) {
            LOG(ERROR) << "Unable to fork ip[6]tables-restore, type: " << type;
            return -1;
        }

        process->reset(newProcess);
    }

    if (!android::base::WriteFully((*process)->stdIn, command.data(), command.length())) {
        ALOGE("Unable to send command: %s", strerror(errno));
        return -1;
    }

    if (!android::base::WriteFully((*process)->stdIn, PING, PING_SIZE)) {
        ALOGE("Unable to send ping command: %s", strerror(errno));
        return -1;
    }

    if (!drainAndWaitForAck(*process, command, output)) {
        // drainAndWaitForAck has already logged an error.
        return -1;
    }

    return 0;
}

这里 process 其实就是 /system/bin/iptables-restore 的进程,WriteFully 方法所在位置实际就是把 command 数据重定向输入到 /system/bin/iptables-restore ,与下面命令等价

iptables-restore < command

使用 oem-iptables-init.sh 添加自定义的防火墙规则

/system/netd/server/oem_iptables_hook.cpp
Android Iptables 客制化方法及基本使用-LMLPHP

/system/netd/server/NetdConstants.cpp
Android Iptables 客制化方法及基本使用-LMLPHP
在 Controllers::initIptablesRules 先通过 initChildChains() 初始化 INPUT OUTPUT 等链,再通过 setupOemIptablesHook 加载 ODM 规则,随后在加载系统防火墙规则

void Controllers::initIptablesRules() {
    Stopwatch s;
    initChildChains(); // 初始化 INPUT OUTPUT 等链
    ALOGI("Creating child chains: %.1fms", s.getTimeAndReset());

    // Let each module setup their child chains
    setupOemIptablesHook(); // 加载 OEM 规则
    ALOGI("Setting up OEM hooks: %.1fms", s.getTimeAndReset());

    /* When enabled, DROPs all packets except those matching rules. */
    firewallCtrl.setupIptablesHooks(); // 加载防火墙规则
    ALOGI("Setting up FirewallController hooks: %.1fms", s.getTimeAndReset());

    /* Does DROPs in FORWARD by default */
    tetherCtrl.setupIptablesHooks();
    ALOGI("Setting up TetherController hooks: %.1fms", s.getTimeAndReset());

    /*
     * Does REJECT in INPUT, OUTPUT. Does counting also.
     * No DROP/REJECT allowed later in netfilter-flow hook order.
     */
    bandwidthCtrl.setupIptablesHooks();
    ALOGI("Setting up BandwidthController hooks: %.1fms", s.getTimeAndReset());

    /*
     * Counts in nat: PREROUTING, POSTROUTING.
     * No DROP/REJECT allowed later in netfilter-flow hook order.
     */
    idletimerCtrl.setupIptablesHooks();
    ALOGI("Setting up IdletimerController hooks: %.1fms", s.getTimeAndReset());

    /*
     * Add rules for detecting IPv6/IPv4 TCP/UDP connections with TLS/DTLS header
     */
    strictCtrl.setupIptablesHooks();
    ALOGI("Setting up StrictController hooks: %.1fms", s.getTimeAndReset());
}

oem-iptables-init.sh 示例文件

#!/bin/bash
/system/bin/iptables -A INPUT -p tcp --destination-port 5555 -s 127.0.0.1 -j ACCEPT
/system/bin/iptables -A INPUT -p tcp --destination-port 5555 -j DROP

基本概念

Iptables 链

  • INPUT:处理入站数据包
  • OUTPUT:处理出站数据包
  • FORWARD:处理转发数据包
  • POSTROUTING:在进行路由选择后处理数据包(对数据链进行源地址修改转换)
  • PREROUTING:在进行路由选择前处理数据包(做目标地址转换)

Iptables 表

  1. raw表:确定是否对该数据包进行状态跟踪以及处理异常,表内包含两个链:OUTPUT、PREROUTING
  2. mangle表:为数据包的 TOS(服务类型)、TTL(生命周期)值,或者为数据包设置 Mark 标记,以实现流量整形、策略路由等高级应用。其对应 iptable_mangle,表内包含五个链:PREROUTING、POSTROUTING、INPUT、OUTPUT、FORWARD
  3. nat表:修改数据包中的源、目标IP地址或端口;其对应的模块为 iptable_nat,表内包括三个链:PREROUTING、POSTROUTING、OUTPUT(centos7中还有 INPUT,centos6 中没有)
  4. filter表:确定是否放行该数据包(过滤);其对应的内核模块为 iptable_filter,表内包含三个链:INPUT、FORWARD、OUTPUT。
    Android Iptables 客制化方法及基本使用-LMLPHP

表、链规则检查顺序

Android Iptables 客制化方法及基本使用-LMLPHP
按顺序依次检查,匹配即停止(LOG策略例外),若找不到相匹配的规则,则按该链的默认策略处理

规则

匹配条件

  1. 源地址 Source IP,目标地址 Destination IP
  2. 源端口 Source Port, 目标端口 Destination Port
  3. …等等

处理动作

处理动作在 iptables 中被称为 target,此处列出一些常用的动作:
ACCEPT:允许数据包通过
DROP:直接丢弃数据包,不给任何回应信息,客户端不会收到任何回应
REJECT:拒绝数据包通过,必要时会给数据发送端一个响应的信息,客户端刚请求就会收到拒绝的信息
SNAT:源地址转换,解决内网用户用同一个公网地址上网的问题
MASQUERADE:是 SNAT 的一种特殊形式,适用于动态的、临时会变的 ip 上
DNAT:目标地址转换
REDIRECT:在本机做端口映射

iptables 规则管理

信息查询

# filter负责过滤功能,比如允许哪些IP地址访问,拒绝哪些IP地址访问,允许访问哪些端口,禁止访问哪些端口
# filter表会根据我们定义的规则进行过滤,filter表应该是我们最常用到的表了
# 下面两种都可以
# 默认不加-t就是指的filter表
iptables -t filter --list
iptables -t raw -L
iptables -t mangle -L
iptables -t nat -L

# -v是显示详细的信息,列出INPUT链的详细信息
iptables -vL INPUT
# 不让IP进行反解
iptables -nvL INPUT
# 显示规则的序号,--line-numbers选项表示显示规则的序号,注意,此选项为长选项,不能与其他短选项合并,不过此选项可以简写为--line
iptables --line-numbers -t 表名 -L
# 表示查看表中的所有规则,并且显示更详细的信息(-v选项),不过,计数器中的信息显示为精确的计数值,而不是显示为经过可读优化的计数值,-x选项表示显示计数器的精确值
iptables -t 表名 -v -x -L
# 可以合起来,不过-L在最后
iptables --line -t filter -nvxL INPUT

# -----------------------显示界面解释-----------------------
# Chain INPUT (policy ACCEPT 170M packets, 33G bytes)
# policy表示当前链的默认策略,policy ACCEPT表示INPUT的链的默认动作为ACCEPT,换句话说就是,默认接受通过INPUT关卡的所有请求,所以我们在配置INPUT链的具体规则时,应该将需要拒绝的请求配置到规则中
# 说白了就是”黑名单”机制,默认所有人都能通过,只有指定的人不能通过,当我们把INPUT链默认动作设置为接受(ACCEPT),就表示所有人都能通过这个关卡,此时就应该在具体的规则中指定需要拒绝的请求,就表示只有指定的人不能通过这个关卡,这就是黑名单机制
# packets表示当前链(上例为INPUT链)默认策略匹配到的包的数量,0 packets表示默认策略匹配到0个包。
# bytes表示当前链默认策略匹配到的所有包的大小总和。
# 其实,我们可以把packets与bytes称作”计数器”,上图中的计数器记录了默认策略匹配到的报文数量与总大小,”计数器”只会在使用-v选项时,才会显示出来

# pkts:对应规则匹配到的报文的个数。
# bytes:对应匹配到的报文包的大小总和。
# target:规则对应的target,往往表示规则对应的”动作”,即规则匹配成功后需要采取的措施。
# prot:表示规则对应的协议,是否只针对某些协议应用此规则。
# opt:表示规则对应的选项。
# in:表示数据包由哪个接口(网卡)流入,即从哪个网卡来。
# out:表示数据包将由哪个接口(网卡)流出,即到哪个网卡去。
# source:表示规则对应的源头地址,可以是一个IP,也可以是一个网段。
# destination:表示规则对应的目标地址。可以是一个IP,也可以是一个网段

规则添加

注意:添加规则时,规则的顺序非常重要,哪个先匹配就执行哪个,后面就算有一模一样的也不会执行

# 在指定表的指定链的尾部添加一条规则,-A选项表示在对应链的末尾添加规则,省略-t选项时,表示默认操作filter表中的规则
iptables -t 表名 -A 链名 匹配条件 -j 动作
# 举例,表示丢弃来自192.168.1.146的数据包
# 使用-s选项,指明”匹配条件”中的”源地址”,即如果报文的源地址属于-s对应的地址,那么报文则满足匹配条件,-s为source之意,表示源地址
iptables -t filter -A INPUT -s 192.168.1.146 -j DROP

# 在指定表的指定链的首部添加一条规则,-I选型表示在对应链的开头添加规则
iptables -t 表名 -I 链名 匹配条件 -j 动作
# 举例
iptables -t filter -I INPUT -s 192.168.1.146 -j ACCEPT

# 在指定表的指定链的指定位置添加一条规则
iptables -t 表名 -I 链名 规则序号 匹配条件 -j 动作
iptables -t filter -I INPUT 5 -s 192.168.1.146 -j REJECT

# 设置指定表的指定链的默认策略(默认动作),并非添加规则。
iptables -t 表名 -P 链名 动作
# 表示将filter表中FORWARD链的默认策略设置为ACCEPT
iptables -t filter -P FORWARD ACCEPT

规则删除

# 按照规则序号删除规则,删除指定表的指定链的指定规则,-D选项表示删除对应链中的规则
iptables -nvL --line-numbers
iptables -t 表名 -D 链名 规则序号
# 表示删除filter表中INPUT链中序号为3的规则
iptables -t filter -D INPUT 3

# 按照具体的匹配条件与动作删除规则,删除指定表的指定链的指定规则
iptables -t 表名 -D 链名 匹配条件 -j 动作
# 表示删除filter表中INPUT链中源地址为192.168.1.146并且动作为DROP的规则
iptables -t filter -D INPUT -s 192.168.1.146 -j DROP

# 删除指定表的指定链中的所有规则,-F选项表示清空对应链中的规则,执行时需三思
iptables -t 表名 -F 链名
iptables -t filter -F INPUT

# 删除指定表中的所有规则,执行时需三思
iptables -t 表名 -F
iptables -t filter -F

规则修改

# 修改指定表中指定链的指定规则,-R选项表示修改对应链中的规则,使用-R选项时要同时指定对应的链以及规则对应的序号,并且规则中原本的匹配条件不可省略
iptables -t 表名 -R 链名 规则序号 规则原本的匹配条件 -j 动作
# 修改filter表中INPUT链的第3条规则,将这条规则的动作修改为ACCEPT, -s 192.168.1.146为这条规则中原本的匹配条件,如果省略此匹配条件,修改后的规则中的源地址可能会变为0.0.0.0/0
iptables -t filter -R INPUT 3 -s 192.168.1.146 -j ACCEPT
# 其他修改规则的方法:先通过编号删除规则,再在原编号位置添加一条规则

# 修改指定表的指定链的默认策略(默认动作),并非修改规则,可以使用如下命令
iptables -t 表名 -P 链名 动作
# 将filter表中FORWARD链的默认策略修改为ACCEPT
iptables -t filter -P FORWARD ACCEPT

规则匹配

当规则中同时存在多个匹配条件时,多个条件之间默认存在"与"的关系,即报文必须同时满足所有条件,才能被规则匹配

# --------------------匹配条件:目标IP地址
# -s用于匹配报文的源地址,可以同时指定多个源地址,每个IP之间用逗号隔开,也可以指定为一个网段
# 逗号两侧均不能包含空格,多个IP之间必须与逗号相连
iptables -t filter -I INPUT -s 192.168.1.111,192.168.1.118 -j DROP
iptables -t filter -I INPUT -s 192.168.1.0/24 -j ACCEPT
# 只要发往本机的报文的源地址不是192.168.1.146,就接受报文
iptables -t filter -I INPUT ! -s 192.168.1.0/24 -j ACCEPT

# -d用于匹配报文的目标地址,可以同时指定多个目标地址,每个IP之间用逗号隔开,也可以指定为一个网段
# 所有IP发送往111,118的报文都将被丢弃
iptables -t filter -I OUTPUT -d 192.168.1.111,192.168.1.118 -j DROP
iptables -t filter -I INPUT -d 192.168.1.0/24 -j ACCEPT
# 不管是-s选项还是-d选项,取反操作与同时指定多个IP的操作不能同时使用
iptables -t filter -I INPUT ! -d 192.168.1.0/24 -j ACCEPT

# -------------------匹配条件:协议类型
# -p用于匹配报文的协议类型,可以匹配的协议类型tcp、udp、udplite、icmp、esp、ah、sctp等(centos7中还支持icmpv6、mh)
iptables -t filter -I INPUT -p tcp -s 192.168.1.146 -j ACCEPT
iptables -t filter -I INPUT ! -p udp -s 192.168.1.146 -j ACCEPT

# ---------------匹配条件:网卡接口
# -i用于匹配报文是从哪个网卡接口流入本机的,由于匹配条件只是用于匹配报文流入的网卡
# 所以在OUTPUT链与POSTROUTING链中不能使用此选项
# 拒绝由网卡eth4流入的ping请求报文
iptables -t filter -I INPUT -p icmp -i eth4 -j DROP
iptables -t filter -I INPUT -p icmp ! -i eth4 -j DROP

# -o用于匹配报文将要从哪个网卡接口流出本机,于匹配条件只是用于匹配报文流出的网卡,所以在INPUT链与PREROUTING链中不能使用此选项。
iptables -t filter -I OUTPUT -p icmp -o eth4 -j DROP
iptables -t filter -I OUTPUT -p icmp ! -o eth4 -j DROP

自定义链

# 创建自定义链
#示例:在filter表中创建IN_WEB自定义链,省略-t选项时,缺省操作的就是filter表
iptables -t filter -N IN_WEB
# 可以看到,这条自定义链的引用计数为0 (0 references),就是说,这条自定义链还没有被任何默认链所引用
iptables -nvL

# 自定义链中配置规则,和其他一样
iptables -t filter -I IN_WEB -s 192.168.1.139 -j REJECT
iptables -I IN_WEB -s 192.168.1.188 -j REJECT
iptables -t filter --line -nvL IN_WEB


# 引用自定义链
#示例:在INPUT链中引用刚才创建的自定义链
iptables -t filter -I INPUT -p tcp --dport 80 -j IN_WEB

# 重命名自定义链
#示例:将IN_WEB自定义链重命名为WEB
iptables -E IN_WEB WEB

# 删除自定义链
# 删除自定义链需要满足两个条件:自定义链没有被引用;自定义链中没有任何规则
#示例:删除引用计数为0并且不包含任何规则的WEB链
iptables -D INPUT 1
iptables -t filter -F WEB
# 使用”-X”选项可以删除一个引用计数为0的、空的自定义链
iptables -X WEB

Iptables 保存规则

命令

iptables-save [-M modprobe] [-c] [-t table] [-f filename]

-M, --modprobe <modprobe_program>
        指定 modprobe 程序的路径。默认情况下,iptables-save 将检查 /proc/sys/kernel/modprobe 以确定可执行文件的路径。
-f, --file <filename>
        指定要记录输出到的文件名。如果没有指定,将输出到 STDOUT。
-c, --counters
        在输出中包含所有数据包和字节计数器的当前值。
-t, --table <tablename>
        将输出限制为一个表。如果内核配置了自动模块加载,如果该表不存在,则会尝试为该表加载相应的模块。

        如果未指定,输出所有可用的表。

示例

iptables-save  -t filter > filter.bak

储存结果

iptables-save -c
# Generated by iptables-save v1.4.21 on Sat Dec 17 17:05:12 2022
*filter
:INPUT ACCEPT [8397115:1774409253]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [8446458:1291918085]
COMMIT
# Completed on Sat Dec 17 17:05:12 2022

其中:

“#”号开头的表示注释;
“*filter”表示所在的表;
“:链名默认策略”表示相应的链及默认策略,具体的规则部分省略了命令名“iptables”;
在末尾处“COMMIT”表示提交前面的规则设置。

导入规则

iptables-restore 命令

iptables-restore < 文件名称

参考

https://blog.csdn.net/lemon_TT/article/details/130018867

05-15 12:00