本周在协助验证一套虚拟网络的方案,该方案包含一个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,为了避免遇到问题时一脸懵逼,平时还是要多花时间熟悉内部实现!