使用 eBPF 技術(shù)跟蹤 Netfilter 數(shù)據(jù)流
1. 網(wǎng)絡(luò)層數(shù)據(jù)流向與 Netfilter 體系
圖 1-1 為網(wǎng)絡(luò)層內(nèi)核收發(fā)核心流程圖,在函數(shù)流程圖中我們可以看到 Netfliter 在其中的位置(圖中深色底紋圓角矩形)。圖中對應(yīng)的 hook 點有 5 個,每個hook 點中保存一組按照優(yōu)先級排序的函數(shù)列表:
NF_IP_PREROUTING:接收到的包進入?yún)f(xié)議棧后立即觸發(fā)此hook中注冊的對應(yīng)函數(shù)列表,在進行任何路由判斷 (將包發(fā)往哪里)之前;NF_IP_LOCAL_IN:接收到的包經(jīng)過路由判斷,如果目的是本機,將觸發(fā)此hook中注冊的對應(yīng)函數(shù)列表;NF_IP_FORWARD:接收到的包經(jīng)過路由判斷,如果目的是其他機器,將觸發(fā)此hook中注冊的對應(yīng)函數(shù)列表;NF_IP_LOCAL_OUT:本機產(chǎn)生的準(zhǔn)備發(fā)送的包,在進入?yún)f(xié)議棧后立即觸發(fā)此hook中注冊的對應(yīng)函數(shù)列表;NF_IP_POST_ROUTING:本機產(chǎn)生的準(zhǔn)備發(fā)送的包或者轉(zhuǎn)發(fā)的包,在經(jīng)過路由判斷之后, 將觸發(fā)此hook中注冊的對應(yīng)函數(shù)列表;

圖 1-1 網(wǎng)絡(luò)層內(nèi)核收發(fā)核心流程圖
從圖 1-1 的數(shù)據(jù)流分為三類,分別用不同的顏色標(biāo)注,因此我們可以得知:
本地處理的數(shù)據(jù)包,在 Netfliter體系中會依次流經(jīng)NF_IP_PREROUTING和NF_IP_LOCAL_IN;轉(zhuǎn)發(fā)的數(shù)據(jù)包,在 Netfliter體系中會依次流經(jīng)NF_IP_FORWARD和NF_IP_POST_ROUTING;本地發(fā)送的數(shù)據(jù)包, 在 Netfliter體系中會依次流經(jīng)NF_IP_LOCAL_OUT和NF_IP_POST_ROUTING;
2. Netfilter 與 IPtables
2.1 Netfilter 數(shù)據(jù)結(jié)構(gòu)
Netfilter 架構(gòu)中對于 hook 點中注冊的函數(shù)管理,采用二維數(shù)組的方式進行組織,縱軸為協(xié)議,橫軸為 hook 點,每個 Network Namespace 對應(yīng)一個此種格式的二維數(shù)組,詳見圖 2-1。數(shù)組中保存的為 nf_hook_entries 結(jié)構(gòu),對應(yīng)保存了該 hook 點中注冊的 hook 函數(shù),函數(shù)按照優(yōu)先級的方式進行管理,調(diào)用時也是按照優(yōu)先級進行過濾。

圖 2-1 Netfilter hook 點函數(shù)數(shù)據(jù)結(jié)構(gòu)
其中 hooks_ipv4[NF_INET_NUMHOOKS] 位于 net->nf 變量中。hook 函數(shù)的原型定義如下:
typedef unsigned int nf_hookfn(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state);
以 table nat 定義的 hook 函數(shù)為例, struct nf_hook_ops nf_nat_ipv4_ops 如下:
static const struct nf_hook_ops nf_nat_ipv4_ops[] = {
{
.hook = iptable_nat_do_chain, // 函數(shù)名
.pf = NFPROTO_IPV4, // 協(xié)議名
.hooknum = NF_INET_PRE_ROUTING, // hook 點
.priority = NF_IP_PRI_NAT_DST, // 優(yōu)先級
},
{
.hook = iptable_nat_do_chain,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_POST_ROUTING,
.priority = NF_IP_PRI_NAT_SRC,
},
{
.hook = iptable_nat_do_chain,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_OUT,
.priority = NF_IP_PRI_NAT_DST,
},
{
.hook = iptable_nat_do_chain,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_IN,
.priority = NF_IP_PRI_NAT_SRC,
},
};
nf_nat_ipv4_ops 結(jié)構(gòu)在函數(shù) iptable_nat_table_init 中初始化,最終通過 nf_register_net_hook 函數(shù)注冊到對應(yīng) hook 點的函數(shù)列表中。
2.2 iptabes
iptables 是運行在用戶空間的應(yīng)用軟件,通過控制 Linux 內(nèi)核 中 Netfilter 模塊,來管理網(wǎng)絡(luò)數(shù)據(jù)包的處理和轉(zhuǎn)發(fā)。iptables 使用 table 來組織規(guī)則,根據(jù)用來做什么類型的判斷標(biāo)準(zhǔn),將規(guī)則分為不同 table,當(dāng)前支持的 table 有 raw/mangle/nat/filter/security 等。在 table 內(nèi)部采用鏈 (chain)進行組織,其中系統(tǒng)內(nèi)置的 chain 與 Netfilter 中的 hook 點一一對應(yīng),例如 chain PREROUTING 對應(yīng)于 NF_IP_PRE_ROUTING hook,用戶自定義 chain 沒有對應(yīng)的 Netfilter hook 對應(yīng),因此必須通過 jump 跳轉(zhuǎn)的方式進行關(guān)聯(lián)。
iptables 的整體組織如下表,縱軸代表的是 table 名,橫軸是 chain 的名字,與 Netfilter hook 點一一對應(yīng)??v軸的方向代表了在某個 chain 上調(diào)用的順序,優(yōu)先級自上而下。
| Tables↓ /Chains→ | PREROUTING | INPUT | FORWARD | OUTPUT | POSTROUTING |
|---|---|---|---|---|---|
| (routing decision) | ? | ||||
| raw | ? | ? | |||
| (connection tracking enabled) | ? | ? | |||
| mangle | ? | ? | ? | ? | ? |
| nat (DNAT) | ? | ? | |||
| (routing decision) | ? | ? | |||
| filter | ? | ? | ? | ||
| security | ? | ? | ? | ||
| nat (SNAT) | ? | ? |
2.3 內(nèi)核代碼實現(xiàn)
此處以 ip_rcv 函數(shù)為例,簡單討論在代碼層面的實現(xiàn):
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
struct net_device *orig_dev)
{
struct net *net = dev_net(dev);
skb = ip_rcv_core(skb, net); // 對于 ip 數(shù)據(jù)進行校驗
if (skb == NULL)
return NET_RX_DROP;
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
}
NF_HOOK 宏在啟用 Netfilter 的條件編譯下,會首先調(diào)用 nf_hook 函數(shù),在該函數(shù)中會根據(jù)傳入的協(xié)議和 hook 點,獲取到對應(yīng)的 hook 函數(shù)列表頭(例如 IPv4 協(xié)議中的 net->nf.hooks_ipv4[hook] ),然后在 nf_hook_slow 中循環(huán)調(diào)用列表中的 hook 函數(shù)(hook 函數(shù)按照優(yōu)先級組織),并基于 hook 函數(shù)返回的結(jié)果決定繼續(xù)調(diào)用列表中后續(xù)的 hook 函數(shù),還是直接返回。
Netfilter 中 hook 函數(shù)的格式基本如下,直接調(diào)用 ipt_do_table 函數(shù),最后的參數(shù)傳入對應(yīng)的 table 字段。
static unsigned int iptable_nat_do_chain(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
return ipt_do_table(skb, state, state->net->ipv4.nat_table);
}
所以,如果我們想要獲取到 Netfilter hook 點中對應(yīng)函數(shù)的過濾的結(jié)果,則需要跟蹤 ipt_do_table 函數(shù)的入?yún)⒑头祷亟Y(jié)果即可。
unsigned int ipt_do_table(struct sk_buff *skb, // skb
const struct nf_hook_state *state, // 相關(guān)狀態(tài)
struct xt_table *table) // table 表
3. 使用 eBPF 技術(shù)跟蹤
經(jīng)過上述分析,我們了解到對于 Netfilter 的底層函數(shù)為 ipt_do_table,那么我們只需要使用 kprobe 和 kretprobe 獲取到入?yún)⒑头祷亟Y(jié)果,即可以獲取到對應(yīng)的過濾結(jié)果,這對于我們分析采用 iptables 管理流量的場景下定位問題非常方便。

圖 3-1 程序架構(gòu)
運行效果圖:
./iptables_trace_ex.py
pid skb table hook verdict
3956565 ffff8a7571a5eae0 b'filter' OUTPUT ACCEPT
完整代碼如下:
#!/usr/bin/python
from bcc import BPF
prog = """
#include <bcc/proto.h>
#include <uapi/linux/ip.h>
#include <uapi/linux/icmp.h>
#include <uapi/linux/tcp.h>
#include <net/inet_sock.h>
#include <linux/netfilter/x_tables.h>
#define MAC_HEADER_SIZE 14;
#define member_address(source_struct, source_member) \
({ \
void* __ret; \
__ret = (void*) (((char*)source_struct) + offsetof(typeof(*source_struct), source_member)); \
__ret; \
})
#define member_read(destination, source_struct, source_member) \
do{ \
bpf_probe_read( \
destination, \
sizeof(source_struct->source_member), \
member_address(source_struct, source_member) \
); \
} while(0)
struct ipt_do_table_args
{
struct sk_buff *skb;
const struct nf_hook_state *state;
struct xt_table *table;
u64 start_ns;
};
BPF_HASH(cur_ipt_do_table_args, u32, struct ipt_do_table_args);
int kprobe__ipt_do_table(struct pt_regs *ctx, struct sk_buff *skb, const struct nf_hook_state *state, struct xt_table *table)
{
u32 pid = bpf_get_current_pid_tgid();
struct ipt_do_table_args args = {
.skb = skb,
.state = state,
.table = table,
};
args.start_ns = bpf_ktime_get_ns();
cur_ipt_do_table_args.update(&pid, &args);
return 0;
};
struct event_data_t {
void *skb;
u32 pid;
u32 hook;
u32 verdict;
u8 pf;
u8 reserv[3];
char table[XT_TABLE_MAXNAMELEN];
};
BPF_PERF_OUTPUT(open_events);
int kretprobe__ipt_do_table(struct pt_regs *ctx)
{
struct ipt_do_table_args *args;
u32 pid = bpf_get_current_pid_tgid();
struct event_data_t evt = {};
args = cur_ipt_do_table_args.lookup(&pid);
if (args == 0)
return 0;
cur_ipt_do_table_args.delete(&pid);
evt.pid = pid;
evt.skb = args->skb;
member_read(&evt.hook, args->state, hook);
member_read(&evt.pf, args->state, pf);
member_read(&evt.table, args->table, name);
evt.verdict = PT_REGS_RC(ctx);
open_events.perf_submit(ctx, &evt, sizeof(evt));
return 0;
}
"""
# uapi/linux/netfilter.h
NF_VERDICT_NAME = [
'DROP',
'ACCEPT',
'STOLEN',
'QUEUE',
'REPEAT',
'STOP',
]
# uapi/linux/netfilter.h
# net/ipv4/netfilter/ip_tables.c
HOOKNAMES = [
"PREROUTING",
"INPUT",
"FORWARD",
"OUTPUT",
"POSTROUTING",
]
def _get(l, index, default):
'''
Get element at index in l or return the default
'''
if index < len(l):
return l[index]
return default
def print_event(cpu, data, size):
event = b["open_events"].event(data)
hook = _get(HOOKNAMES, event.hook, "~UNK~")
verdict = _get(NF_VERDICT_NAME, event.verdict, "~UNK~")
print("%-10d %-16x %-12s %-12s %-10s"%(event.pid, event.skb, event.table, hook, verdict))
b = BPF(text=prog)
b["open_events"].open_perf_buffer(print_event)
print("pid skb_addr table hook verdict")
while True:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
可以在樣例程序的基礎(chǔ)上通過 skb 讀取對應(yīng)的 IP 和端口信息(包括源和目的),這可以實現(xiàn)對于 Netfilter 中的 hook 點跟蹤。完整的可使用代碼參見 skbtracer.py[1],使用幫助如下:
./skbtracer.py -h
usage: skbtracer.py [-h] [-H IPADDR] [--proto PROTO] [--icmpid ICMPID] [-c CATCH_COUNT] [-P PORT] [-p PID] [-N NETNS] [--dropstack] [--callstack] [--iptable] [--route]
[--keep] [-T] [-t]
Trace any packet through TCP/IP stack
optional arguments:
-h, --help show this help message and exit
-H IPADDR, --ipaddr IPADDR
ip address
--proto PROTO tcp|udp|icmp|any
--icmpid ICMPID trace icmp id
-c CATCH_COUNT, --catch-count CATCH_COUNT
catch and print count
-P PORT, --port PORT udp or tcp port
-p PID, --pid PID trace this PID only
-N NETNS, --netns NETNS
trace this Network Namespace only
--dropstack output kernel stack trace when drop packet
--callstack output kernel stack trace
--iptable output iptable path
--route output route path
--keep keep trace packet all lifetime
-T, --time show HH:MM:SS timestamp
-t, --timestamp show timestamp in seconds at us resolution
examples:
skbtracer.py # trace all packets
skbtracer.py --proto=icmp -H 1.2.3.4 --icmpid 22 # trace icmp packet with addr=1.2.3.4 and icmpid=22
skbtracer.py --proto=tcp -H 1.2.3.4 -P 22 # trace tcp packet with addr=1.2.3.4:22
skbtracer.py --proto=udp -H 1.2.3.4 -P 22 # trace udp packet wich addr=1.2.3.4:22
skbtracer.py -t -T -p 1 --debug -P 80 -H 127.0.0.1 --proto=tcp --kernel-stack --icmpid=100 -N 10000
查看 iptables 數(shù)據(jù)流程,需要添加 --iptable 標(biāo)記。
4. 相關(guān)資料
【BPF 入門系列-8】文件打開記錄跟蹤之 perf_event 篇[2] [譯] 深入理解 iptables 和 netfilter 架構(gòu)[3] 英文[4] Linux 協(xié)議棧--Netfilter 源碼分析[5]
參考資料
skbtracer.py: https://github.com/DavadDi/skbtracer/blob/main/skbtracer.py
[2]【BPF入門系列-8】文件打開記錄跟蹤之 perf_event 篇: https://www.ebpf.top/post/ebpf_trace_file_open_perf_output/
[3][譯] 深入理解 iptables 和 netfilter 架構(gòu): https://arthurchiao.art/blog/deep-dive-into-iptables-and-netfilter-arch-zh/
[4]英文: https://www.digitalocean.com/community/tutorials/a-deep-dive-into-iptables-and-netfilter-architecture
[5]Linux協(xié)議棧--Netfilter源碼分析: http://cxd2014.github.io/2017/08/23/netfilter/
