如何利用iptables/ebtables钩子函数获取出站报文的二层信息
解决IP层与链路层之间捕获出站报文并获取二层信息的方案
嘿,这个需求我之前做流量监控工具时刚好踩过同款坑,给你捋几个可行的实操思路:
1. 用Netfilter的链路层钩子(PF_PACKET)
这是最贴合你需求的内核态方案。Netfilter不止支持IP层(PF_INET)的钩子,还提供了链路层的PF_PACKET协议族钩子,对应的出站钩子点是NF_PACKET_OUTGOING——这个钩子会在IP层处理完成、ARP地址解析完毕、报文准备发往物理接口时触发,刚好能拿到完整的二层信息。
核心实现要点:
- 注册钩子时,把
nf_hook_ops结构的pf字段设为PF_PACKET,hooknum设为NF_PACKET_OUTGOING,优先级可以设高一些(比如NF_IP_PRI_FIRST)确保先处理。 - 在钩子函数中,通过
struct sk_buff直接提取关键信息:- 输出接口:
skb->dev,可以拿到接口名(dev->name)、接口MAC(dev->dev_addr)等。 - 二层头部:用
eth_hdr(skb)获取以太网头指针,直接读取源MAC(eth->h_source)和目的MAC(eth->h_dest)。
- 输出接口:
简化代码示例:
#include <linux/netfilter.h> #include <linux/netfilter_packet.h> #include <linux/if_ether.h> static unsigned int packet_out_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { struct ethhdr *eth; struct net_device *dev = skb->dev; if (!skb || !dev) return NF_ACCEPT; // 获取以太网头部 eth = eth_hdr(skb); if (!eth) return NF_ACCEPT; // 打印接口名、源MAC、目的MAC pr_info("Outgoing interface: %s\n", dev->name); pr_info("Source MAC: %pM\n", eth->h_source); pr_info("Dest MAC: %pM\n", eth->h_dest); // 此处可添加过滤、修改逻辑,最后返回NF_ACCEPT/NF_DROP等动作 return NF_ACCEPT; } static struct nf_hook_ops packet_out_ops = { .hook = packet_out_hook, .pf = PF_PACKET, .hooknum = NF_PACKET_OUTGOING, .priority = NF_IP_PRI_FIRST, }; // 模块加载时注册钩子 static int __init my_module_init(void) { return nf_register_net_hook(&init_net, &packet_out_ops); } // 模块卸载时注销钩子 static void __exit my_module_exit(void) { nf_unregister_net_hook(&init_net, &packet_out_ops); } module_init(my_module_init); module_exit(my_module_exit); MODULE_LICENSE("GPL");
2. 用TC(Traffic Control)+ BPF(用户态方案)
如果不想写内核模块,这个方案更灵活易维护。TC可以把BPF程序挂载到链路层的egress方向,在报文离开IP层进入链路层时捕获,同样能拿到完整的二层信息。
操作步骤:
- 用
bpftool编写或加载BPF程序,在程序中通过skb获取接口、MAC等信息(逻辑和内核模块类似,但在用户态BPF中实现)。 - 将BPF程序挂载到指定接口的egress队列:
tc qdisc add dev eth0 clsact tc filter add dev eth0 egress bpf da obj my_bpf.o sec egress - BPF程序里可以通过直接访问
skb->mac_header提取二层数据,还能把信息通过perf buffer输出到用户态做进一步分析。
3. Ebtables的扩展使用(适合网桥场景)
你之前觉得ebtables只和网桥有关,其实也能用它处理本机出站流量,但前提是把物理接口加入一个单接口网桥(即使只有一个接口)。此时可以用ebtables的OUTPUT钩子(对应NF_BRIDGE_LOCAL_OUT),这个钩子会拦截本机发往网桥接口的报文,能获取二层信息。
不过这个方案需要调整网络配置(比如把IP地址从物理接口移到网桥),不如前两种直接,更适合本身就是网桥设备的场景。
避坑提醒
- 别再用
NF_INET_POSTROUTING:正如你推测的,这个钩子在ip_output()中ARP解析前触发,此时还没有生成二层头部,自然拿不到MAC地址。 - 注意VLAN报文:如果你的网络带VLAN标签,需要处理
struct vlan_ethhdr而非普通ethhdr,可以通过skb->vlan_tci判断是否带VLAN标识。
内容的提问来源于stack exchange,提问作者gdb007
相关产品推荐
相关产品推荐

