linux透明防火墙--br_netfilter
linux使用netfilter实现透明防火墙(桥接模式)下图展示了透明防火墙下,netfilter的报文传送流程:
1. 简介
透明防火墙(Transparent Firewall)又称桥接模式防火墙(Bridge Firewall)。简单来说,就是在网桥设备上加入防火墙功能。透明防火墙具有部署能力强、隐蔽性好、安全性高的优点。
通俗来说,桥接模式防火墙 是 Linux 内核的一种巧妙的“欺骗术”和“中介服务”,它让本应只做简单MAC转发的网桥流量,能在关键的转折点上停下来,把里面的IP包裹拿给更聪明(能识别IP/端口)的Netfilter防火墙去检查一下,检查通过再放行继续转发。这样,你就拥有了一个既能识别设备物理位置(MAC/二层)又能识别网络内容(IP/端口/四层)的强大防火墙网关!
典型应用: Docker/Kubernetes 网络(默认依赖此机制实现容器间隔离、端口映射和NAT网关)、透明防火墙网关、虚拟机网络隔离。
核心目标:让网桥也能看懂(并过滤)IP层的东西!
想象一下你有一个网桥(比如一个 Docker 主机上的虚拟交换机 br0),连接着物理网卡 eth0 和几个容器(veth1, veth2)。在普通的网桥模式下:
二层操作(MAC地址层): 网桥就像个“快递仓库管理员”,只关心包裹(数据帧)上的“收件人地址”(目的MAC地址)。它会查看 MAC 地址,决定把这个包裹转发给哪个连接在网桥上的设备(端口),或者广播给所有设备。它不会拆开包裹看里面的“具体内容”(IP 报头、TCP/UDP 端口等)。
三层防火墙(iptables/nftables): 传统的 iptables/nftables 规则,是基于 IP 地址、TCP/UDP 端口等三层及以上信息工作的。它们更像是“海关检查员”,要拆开包裹,检查里面的文件内容(IP/TCP 头)。但普通防火墙规则主要作用在设备的路由路径上,也就是说,数据包要经过设备的三层网络栈时才会被检查。
问题来了:
如果容器 veth1 想访问外网互联网,它的流量路径是:veth1 -> br0 -> eth0 -> 物理网络。
这条路径中,数据包只是在 br0 这个网桥(二层)被转发,并没有真正进入 Linux 主机自己的 IP 网络栈(三层)。因此,传统的 iptables/nftables FORWARD 链(用于过滤转发的数据包)对这些经过网桥的流量完全不起作用!你无法根据 IP 或端口来允许或阻止容器之间的通信或者容器访问外网。
解决方案:br_netfilter
br_netfilter 就像一个“神通广大的中介”,它能让那些只在网桥(二层)上流动的数据包,临时拐个弯,去给 Linux 内核中的 Netfilter 防火墙框架(iptables/nftables 的底层)瞅一眼,让防火墙有机会根据 IP 地址和端口号(三层/四层)来决定是放行还是丢弃它们。本质上,它让网桥流量也变得对三层防火墙“可见”。
2. 工作原理
识别目标流量: br_netfilter 被加载后,会告诉内核的 Netfilter 框架:“嘿,别只盯着走路由路径的流量(进主机、出主机、主机自身转发),那些在网桥设备上传输,目的不是本机(要转发的)的IPv4/IPv6流量(二层帧里的内容是 IP 包),我也可以想办法让它们经过你!”
数据包路径“拐弯”: 当数据帧到达网桥设备准备转发时:
br_netfilter 相关的钩子(hook)会在网桥转发决策之前和网桥转发决策之后插入。
触发 Netfilter: 在这些关键钩子点上(主要是 NF_BR_PRE_ROUTING 和 NF_BR_POST_ROUTING),br_netfilter 将当前正在处理的二层数据帧临时“升级”。它取出帧内部封装的 IP 数据包。
伪装成路由器: br_netfilter 把这个 IP 数据包伪装成它是刚从一个网络接口(伪装的“源接口”)进来,即将要从另一个网络接口(伪装的“目的接口”)出去的样子。实际上,这两个“接口”对应的就是网桥连接的那些端口(比如 veth1 和 eth0 对于 br0)。
送入防火墙框架: 现在,这个伪装的 IP 数据包被送入 Netfilter 的 FORWARD 链。这个链本来是用来过滤路由转发的数据包的,现在也可以用来过滤网桥转发的数据包了!br_netfilter 还确保了相关的 conntrack 连接跟踪也能正常工作。
应用规则 & 返回: iptables/nftables 规则像往常一样检查这个“伪装”的数据包(检查 IP、端口、连接状态等),并做出判决(ACCEPT/DROP/REJECT 等)。判决结果返回给 br_netfilter。
按判决执行:
ACCEPT: br_netfilter 告诉网桥:“继续你的转发操作吧,就像我从没拦过它一样。”数据包继续按 MAC 地址转发。
DROP/REJECT: br_netfilter 告诉网桥:“别转发这个包了,把它扔掉!”数据包在此终结。
恢复状态: 无论判决如何,br_netfilter 都需要清理它为了调用 Netfilter 而创建的临时伪装状态。
3. 实现架构
核心模块 (br_netfilter.ko):
这是实现透明桥接防火墙功能的核心内核模块。加载它才能启用相关功能。
注册 Netfilter 钩子(NF_BR_PRE_ROUTING, NF_BR_LOCAL_IN, NF_BR_FORWARD, NF_BR_LOCAL_OUT, NF_BR_POST_ROUTING),主要关注 PRE_ROUTING (入桥后,转发决策前) 和 POST_ROUTING (转发决策后,出桥前)。
在钩子函数中包含处理逻辑:提取 IP 数据包,伪装接口信息,送入 Netfilter 框架(iptables/nftables 的底层),处理判决结果。
Netfilter 框架 (netfilter):
Linux 内核内置的、通用的防火墙框架。
提供规则表(filter, nat, mangle, raw)和钩子点的抽象。br_netfilter 将网桥流量引导到 Netfilter 的 FORWARD 链(主要)以及其他相关链(如 PREROUTING, POSTROUTING 用于 DNAT/SNAT)。
执行用户空间配置的规则(通过 iptables/nftables 命令)。
连接跟踪 (nf_conntrack, nf_conntrack_bridge):
跟踪网络连接状态(如 TCP 的握手、ESTABLISHED 状态,UDP/ICMP 的关联)。
br_netfilter 的透明特性依赖于连接跟踪才能正确工作。它需要 nf_conntrack 来识别属于同一连接的数据包,尤其在 FORWARD 链做有状态过滤或 NAT 时。
透明桥接的 DNAT/SNAT 也必须依赖连接跟踪修改后续包的反向路径。
Procfs/Sysctl 控制开关 (/proc/sys/net/bridge/bridge-nf-*):
/proc/sys/net/bridge/bridge-nf-call-iptables (默认通常为1):最关键开关!设置为 1 表示让网桥流量进入 Netfilter FORWARD 等链(iptables)。设置为 0 则禁用透明防火墙过滤(流量纯二层转发)。
其他开关:
bridge-nf-call-arptables: 是否处理 ARP 帧 (较少用)。
bridge-nf-call-ip6tables: 对 IPv6 流量启用。
bridge-nf-filter-vlan-tagged: 是否处理 VLAN 帧里的 IP 包。
bridge-nf-pass-vlan-input-dev: 复杂 VLAN 场景相关。
协议适配层 (内核内部):
负责将网桥收到的原始二层以太网帧“还原”为 IP 数据包,并构建必要的上下文信息(伪装的输入/输出接口),以便 Netfilter 的通用接口处理它。
处理判决后数据包如何回到二层转发路径(或丢弃)。
4. 网络协议细节
数据包路径对比:
普通路由转发路径(涉及防火墙):
物理接口接收 ->
[NF_INET_PRE_ROUTING] -> Conntrack 建立/查找 -> DNAT? ->
[路由决策] -> 目标接口是另一个接口? ->
[NF_INET_FORWARD] -> 过滤规则!->
[NF_INET_POST_ROUTING] -> SNAT? -> Conntrack 确认 ->
物理接口发送
桥接转发路径(无 br_netfilter):
物理接口或 veth 接收 ->
网桥端口接收 -> 学习源 MAC -> 查找目的端口(MAC 表) ->
网桥端口发送(纯 MAC 层操作)
桥接转发路径(启用 br_netfilter & bridge-nf-call-iptables=1):
物理接口或 veth 接收 -> (进入桥端口)
** [NF_BR_PRE_ROUTING] ** (br_netfilter hook) {
如果是 IPv4/IPv6 且目的 MAC 非本机?
提取内部 IP 包 (skb->network_header)
伪装输入接口 (skb->dev = 接收该帧的物理/veth 接口)
调用 NF_INET_PRE_ROUTING 链 (DNAT! Conntrack!)
}
-> 网桥学习源 MAC -> 查找目的端口(MAC 表)->
(决定转发了) ->
** [NF_BR_FORWARD] ** (br_netfilter hook) {
提取 IP 包
伪装输入接口(接收帧的端口)
伪装输出接口(即将转发帧的端口)
调用 NF_INET_FORWARD 链(核心过滤发生在这里!)
根据 NF_FORWARD 链判决(ACCEPT/DROP)决定是否继续
} -> [NF_BR_POST_ROUTING] (br_netfilter hook) {
提取 IP 包
伪装输出接口(即将转发帧的端口)
调用 NF_INET_POST_ROUTING 链(SNAT! Conntrack 确认!)
}
-> 网桥端口发送(按 MAC 地址从指定端口发出去)
关键点:
二层 vs 三层: br_netfilter 的魔法在于在二层(网桥)路径中插入了对三层(IP)信息的检查点。它确保防火墙规则能看到 IP 地址和端口。
伪装的接口: skb->dev 被 br_netfilter 临时修改为真实的物理或 veth 接口,这对 Netfilter 规则正确识别流量来源和目标至关重要。规则可以针对具体的接口(-i eth0, -o veth1)。
FORWARD 链为主战场: 大部分桥接流量的过滤规则(ACCEPT/DROP/REJECT)都写在 Netfilter 的 FORWARD 链上,就像过滤路由器流量一样。
NAT 的可行性: 通过在 NF_INET_PRE_ROUTING (DNAT) 和 NF_INET_POST_ROUTING (SNAT) 链应用规则,结合 conntrack,可以对网桥流量进行目标地址转换(DNAT - 如端口映射)和源地址转换(SNAT - 如容器共享主机IP上网)。
ARP/IPv6 处理: br_netfilter 主要处理 IP 包。需要额外设置开关(bridge-nf-call-arptables, bridge-nf-call-ip6tables)来让 ARP 或 IPv6 流量也经过相应的防火墙(arptables, ip6tables)。这些通常默认关闭。
性能影响: 这种“拐弯”操作增加了处理开销(内核协议栈操作、规则匹配)。对性能要求极高的场景需注意。
5. br_netfilter代码流程
br_netfilter_init注册了一些HOOK
ret = nf_register_hooks(br_nf_ops, ARRAY_SIZE(br_nf_ops));
static struct nf_hook_ops br_nf_ops[] __read_mostly = {
{
.hook = br_nf_pre_routing,
.owner = THIS_MODULE,
.pf = PF_BRIDGE,
.hooknum = NF_BR_PRE_ROUTING,
.priority = NF_BR_PRI_BRNF,
},
{
.hook = br_nf_local_in,
.owner = THIS_MODULE,
.pf = PF_BRIDGE,
.hooknum = NF_BR_LOCAL_IN,
.priority = NF_BR_PRI_BRNF,
},
{
.hook = br_nf_forward_ip,
.owner = THIS_MODULE,
.pf = PF_BRIDGE,
.hooknum = NF_BR_FORWARD,
.priority = NF_BR_PRI_BRNF - 1,
},
{
.hook = br_nf_forward_arp,
.owner = THIS_MODULE,
.pf = PF_BRIDGE,
.hooknum = NF_BR_FORWARD,
.priority = NF_BR_PRI_BRNF,
},
{
.hook = br_nf_post_routing,
.owner = THIS_MODULE,
.pf = PF_BRIDGE,
.hooknum = NF_BR_POST_ROUTING,
.priority = NF_BR_PRI_LAST,
},
{
.hook = ip_sabotage_in,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_FIRST,
},
{
.hook = ip_sabotage_in,
.owner = THIS_MODULE,
.pf = PF_INET6,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP6_PRI_FIRST,
},
};
.hook = br_nf_forward_ip, 对应br_nf_forward_ip函数
/* This is the 'purely bridged' case. For IP, we pass the packet to
* netfilter with indev and outdev set to the bridge device,
* but we are still able to filter on the 'real' indev/outdev
* because of the physdev module. For ARP, indev and outdev are the
* bridge ports. */
static unsigned int br_nf_forward_ip(unsigned int hook, struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct nf_bridge_info *nf_bridge;
struct net_device *parent;
u_int8_t pf;
if (LDSEC_DBG_BRIDGE_ON)
LDSEC_PRINT_FUNC("br_nf_forward_ip");
if (!skb->nf_bridge)
return NF_ACCEPT;
/* Need exclusive nf_bridge_info since we might have multiple
* different physoutdevs. */
if (!nf_bridge_unshare(skb))
return NF_DROP;
parent = bridge_parent(out);
if (!parent)
return NF_DROP;
if (skb->protocol == htons(ETH_P_IP) || IS_VLAN_IP(skb) ||
IS_PPPOE_IP(skb))
pf = PF_INET;
else if (skb->protocol == htons(ETH_P_IPV6) || IS_VLAN_IPV6(skb) ||
IS_PPPOE_IPV6(skb))
pf = PF_INET6;
else
return NF_ACCEPT;
nf_bridge_pull_encap_header(skb);
nf_bridge = skb->nf_bridge;
if (skb->pkt_type == PACKET_OTHERHOST) {
skb->pkt_type = PACKET_HOST;
nf_bridge->mask |= BRNF_PKT_TYPE;
}
/* The physdev module checks on this */
nf_bridge->mask |= BRNF_BRIDGED;
nf_bridge->physoutdev = skb->dev;
if (pf == PF_INET)
skb->protocol = htons(ETH_P_IP);
else
skb->protocol = htons(ETH_P_IPV6);
NF_HOOK(pf, NF_INET_FORWARD, skb, bridge_parent(in), parent,
br_nf_forward_finish);
return NF_STOLEN;
}
br_nf_forward_ip最终调用ip层的NF_INET_FORWARD钩子
NF_HOOK(pf, NF_INET_FORWARD, skb, bridge_parent(in), parent,
br_nf_forward_finish);
参考:
http://blog.csdn.net/dog250/article/details/7314927
http://ebtables.netfilter.org/documentation/bridge-nf.html
http://ebtables.netfilter.org/misc/brnf-faq.html
http://ebtables.netfilter.org/br_fw_ia/br_fw_ia.html
https://www.linuxjournal.com/article/8172
更多推荐
所有评论(0)