关于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_FORWARDNF_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,为了避免遇到问题时一脸懵逼,平时还是要多花时间熟悉内部实现!

说两句: