容器网络转发一次故障笔记

今天处理问题时,发现docker 容器内访问外网不通,tcpdump 抓包发现,只抓到veth 接口上的包,但是报文需要再宿主机物理网卡上转发到外网,此时这部分报文没看到,

目前容器使用桥接模式(Bridge Network)

 

容器 eth0 (veth0)
   ↓
veth_xmit() [veth.c]
   ↓
peer veth1 in host namespace
   ↓
netif_receive_skb() [dev.c]
   ↓
ip_rcv() [ip_input.c]
   ↓
  if local: ip_local_deliver()
  else: ip_forward() [ip_forward.c]
             ↓
       netfilter (FORWARD hook, NAT/MASQUERADE)
             ↓
       dst_output()
             ↓
       dev_queue_xmit()
             ↓
       宿主机物理网卡

  也就是ip forward 被丢弃,查看ip_forward选项,发现net.ipv4.ip_forward = 0 ,设置为1后解决问题。

  使用iptable -nvL  -t nat可以看到

 0     0 MASQUERADE  all  --  *      *       192.168.122.0/24    !192.168.122.0/24   

内网 192.168.122.0/24 的主机访问外部网络时,其源 IP 会被伪装成网关的公网 IP,实现 NAT 出口。

SNAT模块MASQUERADE

内核空间整个封包逻辑兼容的核心,包含了了

  1. 如何根据使用输入的参数来选择源端口
  2. 如何动态选择网路卡介面的IP地址,如果有多个会怎么选择

内核中相关内容在这个文件xt_MASQUERADE.c

static struct xt_target masquerade_tg_reg[] __read_mostly = {
    {
        .name        = "MASQUERADE",
        .family        = NFPROTO_IPV4,
        .target        = masquerade_tg,
        .targetsize    = sizeof(struct nf_nat_ipv4_multi_range_compat),
        .table        = "nat",
        .hooks        = 1 << NF_INET_POST_ROUTING,
        .checkentry    = masquerade_tg_check,
        .destroy    = masquerade_tg_destroy,
        .me        = THIS_MODULE,
    }
};

static unsigned int
masquerade_tg(struct sk_buff *skb, const struct xt_action_param *par)
{
    struct nf_nat_range2 range;
    const struct nf_nat_ipv4_multi_range_compat *mr;

    mr = par->targinfo;
    range.flags = mr->range[0].flags;
    range.min_proto = mr->range[0].min;
    range.max_proto = mr->range[0].max;

    return nf_nat_masquerade_ipv4(skb, xt_hooknum(par), &range,
                      xt_out(par));
}

真正的核心逻辑处理函数式nf_nat_masquerade_ipv4

unsigned int
nf_nat_masquerade_ipv4(struct sk_buff *skb, unsigned int hooknum,
               const struct nf_nat_range2 *range,
               const struct net_device *out)
{
    struct nf_conn *ct;
    struct nf_conn_nat *nat;
    enum ip_conntrack_info ctinfo;
    struct nf_nat_range2 newrange;
    const struct rtable *rt;
    __be32 newsrc, nh;

    WARN_ON(hooknum != NF_INET_POST_ROUTING);

    ct = nf_ct_get(skb, &ctinfo);

    WARN_ON(!(ct && (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED ||
             ctinfo == IP_CT_RELATED_REPLY)));

    /* Source address is 0.0.0.0 - locally generated packet that is
     * probably not supposed to be masqueraded.
     */
    if (ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.src.u3.ip == 0)
        return NF_ACCEPT;

    rt = skb_rtable(skb);
    nh = rt_nexthop(rt, ip_hdr(skb)->daddr);
    newsrc = inet_select_addr(out, nh, RT_SCOPE_UNIVERSE);
    if (!newsrc) {
        pr_info("%s ate my IP address\n", out->name);
        return NF_DROP;
    }

    nat = nf_ct_nat_ext_add(ct);
    if (nat)
        nat->masq_index = out->ifindex;

    /* Transfer from original range. */
    memset(&newrange.min_addr, 0, sizeof(newrange.min_addr));
    memset(&newrange.max_addr, 0, sizeof(newrange.max_addr));
    newrange.flags       = range->flags | NF_NAT_RANGE_MAP_IPS;
    newrange.min_addr.ip = newsrc;
    newrange.max_addr.ip = newsrc;
    newrange.min_proto   = range->min_proto;
    newrange.max_proto   = range->max_proto;

    /* Hand modified range to generic setup. */
    return nf_nat_setup_info(ct, &newrange, NF_NAT_MANIP_SRC);
}

 

 

第一个重点,选出一个可用的地址作为封包之后的来源IP地址

   rt = skb_rtable(skb);
    nh = rt_nexthop(rt, ip_hdr(skb)->daddr);
    newsrc = inet_select_addr(out, nh, RT_SCOPE_UNIVERSE);

 

  1. 从skb取出该封包对应的routing entry
  2. 从该routing entry中根据封包的目的ip选出下一个节点next_hop
  3. 接下来根据参数下一个节点的IP地址,已经决定好的输出网路接口来决定最后使用的IP

  对于接口上配置了多个ip地址的情况,使用遍历方式去取出(in_dev)->ifa_list内的所有list,其实就是所有的IP地址,

然后根据额外的scope , netmask 进行判断是否选择这个ip作为源newsrc(遍历确认该接口上ip与next_hop 是否在同一个网段,是的话就可以用这个IP地址创建来源IP发送出去。

  1. 针对所有非SECONDARY的IP地址依序检查

 

in_dev_for_each_ifa_rcu(ifa, in_dev) {
        if (ifa->ifa_flags & IFA_F_SECONDARY)
            continue;
        if (min(ifa->ifa_scope, localnet_scope) > scope)
            continue;
        if (!dst || inet_ifa_match(dst, ifa)) {
            addr = ifa->ifa_local;
            break;
        }
        if (!addr)
            addr = ifa->ifa_local;
    }
static inline bool inet_ifa_match(__be32 addr, const struct in_ifaddr *ifa)
{
    return !((addr^ifa->ifa_address)&ifa->ifa_mask);
}

 

 

Source Port端口选择

/* Transfer from original range. */
    memset(&newrange.min_addr, 0, sizeof(newrange.min_addr));
    memset(&newrange.max_addr, 0, sizeof(newrange.max_addr));
    newrange.flags       = range->flags | NF_NAT_RANGE_MAP_IPS;
    newrange.min_addr.ip = newsrc;
    newrange.max_addr.ip = newsrc;
    newrange.min_proto   = range->min_proto;
    newrange.max_proto   = range->max_proto;

    /* Hand modified range to generic setup. */
    return nf_nat_setup_info(ct, &newrange, NF_NAT_MANIP_SRC);

 

  1. 新范围.标志NF_NAT_RANGE_MAP_IPS
  2. newrange.min_proto
  3. newrange.max_proto
  4. newrange.min_addr.ip          max_addr.ip

用户在操作MASQUERADE的时候设置了其他参数--random、--to-ports的话,上述的栏位会有不同的数值,接下来的逻辑处理根据这些数值来处理。

  • - 随机的--to-ports
    • 标志 = NF_NAT_RANGE_PROTO_RANDOM
    • min_proto:max_proto => 用户设置的端口号区间
    • 标志 = NF_NAT_RANGE_PROTO_SPECIFIED

 

对于 nf_nat_setup_info  参考nat分析

 

posted @ 2025-05-08 00:00  codestacklinuxer  阅读(17)  评论(0)    收藏  举报