關於SNAT在bridge中不生效的問題
本週在協助驗證一套虛擬網路的方案,該方案包含一個bridge,向上對接容器的veth,並接管真實NIC作為tx口,方案中需要在bridge中做SNAT,具體hook點位於POST_ROUTING
,命令如下:
iptables -t nat -A POSTROUTING -d 192.168.0.0/24 -j SNAT --to-source 192.168.0.5
為了驗證該方案,我建立了一對veth,其中一端劃分到獨立的netns中,命令如下:
ip link add br-veth type veth peer name veth ip link set br-veth up ip link set veth up brctl addif br0 br-veth ip netns add test-zone ip link set veth netns test-zone ip netns exec test-zone ip addr add 192.168.0.100/24 dev veth
為了避免方案過於複雜,規避gw帶來的影響,這裡假設對端和本端在同一子網,在test-zone中ping對端,我發現對端抓包看到的源IP並未變為192.168.0.5,換句話說SNAT未生效。
在veth的tx方向,這裡僅需做二層轉發即可,閱讀bridge的原始碼可以發現如下路徑(kernel 3.10/4.9無顯著區別):
br_forward -> BR_FORWARD -> br_nf_forward -> BR_POST_FORWARD -> br_nf_post_routing -> INET_POST_ROUTING
然而掛載POST_ROUTING
的SNAT竟然未生效,真的令人匪夷所思。一番google之後,發現需要enable一個標誌:
echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables
縱觀bridge實現,該sysctl
的值最終設定到了變數nf_call_iptables
,而該變數僅在br_nf_pre_routing
中使用。顯然,這是不符合預期的。
慣性思維使然,總認為既然有代表特性是否開啟的變數,那麼在程式碼相關的分支處一定會用到該變數,而bridge的實現則正好不是這樣。
可以看到,只有開啟了nf_call_iptables
,才會進入網路層的netfilter hook點:
static unsigned int br_nf_pre_routing(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { struct nf_bridge_info *nf_bridge; struct net_bridge_port *p; struct net_bridge *br; __u32 len = nf_bridge_encap_header_len(skb); if (unlikely(!pskb_may_pull(skb, len))) return NF_DROP; p = br_port_get_rcu(state->in); if (p == NULL) return NF_DROP; br = p->br; ... if (!brnf_call_iptables && !br->nf_call_iptables) return NF_ACCEPT; ... nf_bridge_put(skb->nf_bridge); if (!nf_bridge_alloc(skb)) // 注意這裡! return NF_DROP; ... NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, state->net, state->sk, skb, skb->dev, NULL, br_nf_pre_routing_finish); return NF_STOLEN; }
很容易忽略標著註釋的那一行,而恰是那一行,直接決定了是否進入NF_INET_FORWARD
和NF_INET_POST_ROUTING
的HOOK點。
static unsigned int br_nf_forward_ip(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { struct nf_bridge_info *nf_bridge; struct net_device *parent; u_int8_t pf; if (!skb->nf_bridge) // 看這裡! return NF_ACCEPT; ... NF_HOOK(pf, NF_INET_FORWARD, state->net, NULL, skb, brnf_get_logical_dev(skb, state->in), parent,br_nf_forward_finish); return NF_STOLEN; }
static unsigned int br_nf_post_routing(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { struct nf_bridge_info *nf_bridge = nf_bridge_info_get(skb); struct net_device *realoutdev = bridge_parent(skb->dev); u_int8_t pf; if (!nf_bridge || !nf_bridge->physoutdev) // 看這裡! return NF_ACCEPT; ... NF_HOOK(pf, NF_INET_POST_ROUTING, state->net, state->sk, skb, NULL, realoutdev, br_nf_dev_queue_xmit); return NF_STOLEN; }
參加工作後,見識了很多相當“野”的解決方案,一言不合就得改kernel,為了避免遇到問題時一臉懵逼,平時還是要多花時間熟悉內部實現!