Golang编程语言知识介绍


  • 首页

  • todo

  • 思考

  • life

  • food

  • OS

  • lua

  • redis

  • Golang

  • C

  • TCP/IP

  • ebpf

  • p4

  • OpenVPN

  • IPSec

  • L2TP

  • DNS

  • distributed

  • web

  • OpenWRT

  • 运维

  • Git

  • 鸟哥的私房菜

  • IT杂谈

  • 投资

  • About Me

  • 友情链接

  • FTP

  • 搜索
close

【BPF入门系列-11】使用 eBPF 技术跟踪 Netfilter 数据流过滤结果

时间: 2023-08-24   |   分类: ebpf     |   阅读: 2389 字 ~5分钟

网络层数据流向与 Netfilter 体系

图 1-1 为网络层内核收发核心流程图,在函数流程图中我们可以看到 Netfliter 在其中的位置(图中深色底纹圆角矩形)。图中对应的 hook 点有 5 个,每个hook 点中保存一组按照优先级排序的函数列表:

  • NF_IP_PREROUTING:接收到的包进入协议栈后立即触发此 hook 中注册的对应函数列表,在进行任何路由判断 (将包发往哪里)之前;
  • NF_IP_LOCAL_IN:接收到的包经过路由判断,如果目的是本机,将触发此 hook 中注册的对应函数列表;
  • NF_IP_FORWARD:接收到的包经过路由判断,如果目的是其他机器,将触发此 hook 中注册的对应函数列表;
  • NF_IP_LOCAL_OUT:本机产生的准备发送的包,在进入协议栈后立即触发此 hook 中注册的对应函数列表;
  • NF_IP_POST_ROUTING:本机产生的准备发送的包或者转发的包,在经过路由判断之后, 将触发此 hook 中注册的对应函数列表;

network_l3_flow

图 1-1 网络层内核收发核心流程图

从图 1-1 的数据流分为三类,分别用不同的颜色标注,因此我们可以得知:

  1. 本地处理的数据包,在 Netfliter 体系中会依次流经 NF_IP_PREROUTING 和 NF_IP_LOCAL_IN;
  2. 转发的数据包,在 Netfliter 体系中会依次流经 NF_IP_FORWARD 和 NF_IP_POST_ROUTING;
  3. 本地发送的数据包,在 Netfliter 体系中会依次流经 NF_IP_LOCAL_OUT 和 NF_IP_POST_ROUTING;

Netfilter 与 IPtables

Netfilter 数据结构

Netfilter 架构中对于 hook 点中注册的函数管理,采用二维数组的方式进行组织,纵轴为协议,横轴为 hook 点,每个 Network Namespace 对应一个此种格式的二维数组,详见图 2-1。数组中保存的为 nf_hook_entries 结构,对应保存了该 hook 点中注册的 hook 函数,函数按照优先级的方式进行管理,调用时也是按照优先级进行过滤。

netfilter_data_struct

图 2-1 Netfilter hook 点函数数据结构

其中 hooks_ipv4[NF_INET_NUMHOOKS] 位于 net->nf 变量中。 hook 函数的原型定义如下:

typedef unsigned int nf_hookfn(void *priv,
			       struct sk_buff *skb,
			       const struct nf_hook_state *state);

以 table nat 定义的 hook 函数为例, struct nf_hook_ops nf_nat_ipv4_ops 如下:

static const struct nf_hook_ops nf_nat_ipv4_ops[] = {
	{
		.hook		= iptable_nat_do_chain,  // 函数名
		.pf		= NFPROTO_IPV4,            // 协议名
		.hooknum	= NF_INET_PRE_ROUTING, // hook 点
		.priority	= NF_IP_PRI_NAT_DST,   // 优先级
	},
	{
		.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 结构在函数 iptable_nat_table_init 中初始化,最终通过 nf_register_net_hook 函数注册到对应 hook 点的函数列表中。

iptabes

iptables 是运行在用户空间的应用软件,通过控制 Linux 内核 中 Netfilter 模块,来管理网络数据包的处理和转发。iptables 使用 table 来组织规则,根据用来做什么类型的判断标准,将规则分为不同 table,当前支持的 table 有 raw/mangle/nat/filter/security 等。在 table 内部采用链 (chain)进行组织,其中系统内置的 chain 与 Netfilter 中的 hook 点一一对应,例如 chain PREROUTING 对应于 NF_IP_PRE_ROUTING hook,用户自定义 chain 没有对应的 Netfilter hook 对应,因此必须通过 jump 跳转的方式进行关联。

iptables 的整体组织如下表,纵轴代表的是 table 名,横轴是 chain 的名字,与 Netfilter hook 点一一对应。纵轴的方向代表了在某个 chain 上调用的顺序,优先级自上而下。

Tables↓ /Chains→ PREROUTING INPUT FORWARD OUTPUT POSTROUTING
(routing decision) ✓
raw ✓ ✓
(connection tracking enabled) ✓ ✓
mangle ✓ ✓ ✓ ✓ ✓
nat (DNAT) ✓ ✓
(routing decision) ✓ ✓
filter ✓ ✓ ✓
security ✓ ✓ ✓
nat (SNAT) ✓ ✓

表 2-1 iptables 规则组织

内核代码实现

此处以 ip_rcv 函数为例,简单讨论在代码层面的实现:

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 数据进行校验
	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 的条件编译下,会首先调用 nf_hook 函数,在该函数中会根据传入的协议和 hook 点,获取到对应的 hook 函数列表头(例如 IPv4 协议中的 net->nf.hooks_ipv4[hook] ),然后在 nf_hook_slow 中循环调用列表中的 hook 函数(hook 函数按照优先级组织),并基于 hook 函数返回的结果决定继续调用列表中后续的 hook 函数,还是直接返回。

Netfilter 中 hook 函数的格式基本如下,直接调用 ipt_do_table 函数,最后的参数传入对应的 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 点中对应函数的过滤的结果,则需要跟踪 ipt_do_table 函数的入参和返回结果即可。

unsigned int ipt_do_table(struct sk_buff *skb,          // skb 
	     const struct nf_hook_state *state,  							// 相关状态
	     struct xt_table *table) 

使用 eBPF 技术跟踪

经过上述分析,我们了解到对于 Netfilter 的底层函数为 ipt_do_table,那么我们只需要使用 kprobe 和 kretprobe 获取到入参和返回结果,即可以获取到对应的过滤结果,这对于我们分析采用 iptables 管理流量的场景下定位问题非常方便。

ipt_tracer

图 3-1 程序架构

运行效果图:

./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()

可以在样例程序的基础上通过 skb 读取对应的 IP 和端口信息(包括源和目的),这可以实现对于 Netfilter 中的 hook 点跟踪。完整的可使用代码参见 skbtracer.py,使用帮助如下:

./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 数据流程,需要添加 –iptable 标记。

4. 参考资料

  • 【BPF入门系列-8】文件打开记录跟踪之 perf_event 篇

  • [译] 深入理解 iptables 和 netfilter 架构 英文

  • Linux协议栈–Netfilter源码分析

  • 原文作者:DavidDi

  • **原文链接:**https://www.ebpf.top/post/iptalbes_ebpf/

  • **版权声明:**本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。

#ebpf#
基于 eBPF 的网络监测实践
【BPF入门系列-10】使用 tracepoint 跟踪文件 open 系统调用
shankusu2017@gmail.com

shankusu2017@gmail.com

日志
分类
标签
GitHub
© 2009 - 2025
粤ICP备2021068940号-1 粤公网安备44011302003059
0%