Android Iptables 客制化方法及基本使用
-
- [Android netd 的自定义链](#Android netd 的自定义链)
-
- [NetdConstants.cpp 的 execIptablesRestore 方法](#NetdConstants.cpp 的 execIptablesRestore 方法)
- [IptablesRestoreController 的 execute 方法](#IptablesRestoreController 的 execute 方法)
- [使用 oem-iptables-init.sh 添加自定义的防火墙规则](#使用 oem-iptables-init.sh 添加自定义的防火墙规则)
- [oem-iptables-init.sh 示例文件](#oem-iptables-init.sh 示例文件)
- 基本概念
-
- [Iptables 链](#Iptables 链)
- [Iptables 表](#Iptables 表)
- 表、链规则检查顺序
- 规则
- [iptables 规则管理](#iptables 规则管理)
- [Iptables 保存规则](#Iptables 保存规则)
- 参考
iptables 是一个在Linux内核集成的IP信息包过滤系统,用于控制IP信息包过滤和防火墙配置,通过存储在数据包过滤表中的规则,在不同的链中做出数据包过滤决定。
Android 是基于 Linux 的操作系统,支持 Iptables。执行 Iptables 命令需要 root 权限。
Android netd 的自定义链
以 INPUT 链为例子,Andorid 会自定义 bw_INPUT、fw_INPUT 等链,链到 INPUT 链上
shell
# 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 链下创造子链
c
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 定义为:
c
/**
* 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 方法定义如下
c
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
c
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 方法
c
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 方法定义如下:
c
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 ,与下面命令等价
shell
iptables-restore < command
使用 oem-iptables-init.sh 添加自定义的防火墙规则
/system/netd/server/oem_iptables_hook.cpp
/system/netd/server/NetdConstants.cpp
在 Controllers::initIptablesRules 先通过 initChildChains() 初始化 INPUT OUTPUT 等链,再通过 setupOemIptablesHook 加载 ODM 规则,随后在加载系统防火墙规则
c
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 示例文件
shell
#!/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 表
- raw表:确定是否对该数据包进行状态跟踪以及处理异常,表内包含两个链:OUTPUT、PREROUTING
- mangle表:为数据包的 TOS(服务类型)、TTL(生命周期)值,或者为数据包设置 Mark 标记,以实现流量整形、策略路由等高级应用。其对应 iptable_mangle,表内包含五个链:PREROUTING、POSTROUTING、INPUT、OUTPUT、FORWARD
- nat表:修改数据包中的源、目标IP地址或端口;其对应的模块为 iptable_nat,表内包括三个链:PREROUTING、POSTROUTING、OUTPUT(centos7中还有 INPUT,centos6 中没有)
- filter表:确定是否放行该数据包(过滤);其对应的内核模块为 iptable_filter,表内包含三个链:INPUT、FORWARD、OUTPUT。
表、链规则检查顺序
按顺序依次检查,匹配即停止(LOG策略例外),若找不到相匹配的规则,则按该链的默认策略处理
规则
匹配条件
- 源地址 Source IP,目标地址 Destination IP
- 源端口 Source Port, 目标端口 Destination Port
- ...等等
处理动作
处理动作在 iptables 中被称为 target,此处列出一些常用的动作:
ACCEPT :允许数据包通过
DROP :直接丢弃数据包,不给任何回应信息,客户端不会收到任何回应
REJECT :拒绝数据包通过,必要时会给数据发送端一个响应的信息,客户端刚请求就会收到拒绝的信息
SNAT:源地址转换,解决内网用户用同一个公网地址上网的问题
MASQUERADE:是 SNAT 的一种特殊形式,适用于动态的、临时会变的 ip 上
DNAT:目标地址转换
REDIRECT:在本机做端口映射
iptables 规则管理
信息查询
shell
# 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,也可以是一个网段
规则添加
注意:添加规则时,规则的顺序非常重要,哪个先匹配就执行哪个,后面就算有一模一样的也不会执行
shell
# 在指定表的指定链的尾部添加一条规则,-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
规则删除
shell
# 按照规则序号删除规则,删除指定表的指定链的指定规则,-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
规则修改
shell
# 修改指定表中指定链的指定规则,-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
规则匹配
当规则中同时存在多个匹配条件时,多个条件之间默认存在"与"的关系,即报文必须同时满足所有条件,才能被规则匹配
shell
# --------------------匹配条件:目标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
自定义链
shell
# 创建自定义链
#示例:在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 保存规则
命令
shell
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>
将输出限制为一个表。如果内核配置了自动模块加载,如果该表不存在,则会尝试为该表加载相应的模块。
如果未指定,输出所有可用的表。
示例
shell
iptables-save -t filter > filter.bak
储存结果
shell
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 命令
shell
iptables-restore < 文件名称