dpvs学习笔记: 18 nat64 的实现

2019-03-13  本文已影响0人  董泽润

这几天 iqiyi 同学做了大更新,dpvs 增加 nat64 功能,这样机房对外暴露 ipv6, 对内还不用修改 ipv4 业务代码。算是重量级武器吧,为 ipv6 升级过渡提供了双栈的保障。

提前想好的问题

  1. 如何使用 nat64 配置呢?会不会很繁琐
  2. 后端 real server 看到的也是 ipv6 地址?怎么正确的获取源 ip,涉及 toa 的实现
  3. 为了兼容 4 to 4 和 6 to 6,现有主逻辑代码做了哪些修改呢?只是将三层的 ip 头来回替换就可以了吗?


    ipv4 header
    ipv6 header

如何使用 nat64

#!/bin/sh -
# add VIP to WAN interface
./dpip addr add 2001::1/128 dev dpdk1

# route for WAN/LAN access
# add routes for other network or default route if needed.
./dpip route -6 add 2001::/64 dev dpdk1
./dpip route add 10.0.0.0/8 dev dpdk0

# add service <VIP:vport> to forwarding, scheduling mode is RR.
# use ipvsadm --help for more info.
./ipvsadm -A -t [2001::1]:80 -s rr

# add two RS for service, forwarding mode is FNAT (-b)
./ipvsadm -a -t [2001::1]:80 -r 10.0.0.1 -b
./ipvsadm -a -t [2001::1]:80 -r 10.0.0.2 -b

# add at least one Local-IP (LIP) for FNAT on LAN interface
./ipvsadm --add-laddr -z 10.0.0.3 -t [2001::1]:80 -F dpdk0

上面是官网 ipv6 的 nat64 例子,看得出来,和普通搭建 full-nat 没有任何区别,只是 vip 变成了 ipv6,并且 real server 添加时是 ipv4 而己,运维上保持了一致性,点赞

对于新建连接的请求

完整 tcp4 流程可以参考之前的文章,对于 fullnat 大致流程是一样的。但是由于要做 64 转换,所以 ip 头需要重新填充。

函数 tcp_conn_sched 调度后端 service 并建产 session 结构体 conn, dp_vs_schedule 调用 dp_vs_conn_new 建立连接,将 seq -1,选择 local addr port ,并添加到 hash 表中。这块和 tcp4 逻辑基本一致,区别就在于后续发送到 rs 流程。

int dp_vs_xmit_fnat(struct dp_vs_proto *proto,
                    struct dp_vs_conn *conn,
                    struct rte_mbuf *mbuf)
{
    int af = conn->af;
    assert(af == AF_INET || af == AF_INET6);

    if (tuplehash_in(conn).af == AF_INET &&
        tuplehash_out(conn).af == AF_INET)
        return __dp_vs_xmit_fnat4(proto, conn, mbuf);
    if (tuplehash_in(conn).af == AF_INET6 &&
        tuplehash_out(conn).af == AF_INET6)
        return __dp_vs_xmit_fnat6(proto, conn, mbuf);
    if (tuplehash_in(conn).af == AF_INET6 &&
        tuplehash_out(conn).af == AF_INET)
        return __dp_vs_xmit_fnat64(proto, conn, mbuf);

    rte_pktmbuf_free(mbuf);
    return EDPVS_NOTSUPP;
}

xmit_inbound 开始发送数据包,由于进来的是 af_inet6,出去的是 af_inet,所以做调用 __dp_vs_xmit_fnat64 做 64 转换发送。

static int __dp_vs_xmit_fnat64(struct dp_vs_proto *proto,
                               struct dp_vs_conn *conn,
                               struct rte_mbuf *mbuf)
{
      ......
    /*
     * mbuf is from IPv6, icmp should send by icmp6
     * ext_hdr and
     */
    mtu = rt->mtu;
    pkt_len = mbuf_nat6to4_len(mbuf);
    if (pkt_len > mtu) {
        RTE_LOG(DEBUG, IPVS, "%s: frag needed.\n", __func__);
        icmp6_send(mbuf, ICMP6_PACKET_TOO_BIG, 0, mtu);

        err = EDPVS_FRAG;
        goto errout;
    }

    /* L3 translation before l4 re-csum */
    err = mbuf_6to4(mbuf, &conn->laddr.in, &conn->daddr.in);
    if (err)
        goto errout;
    ip4h = ip4_hdr(mbuf);
    ip4h->hdr_checksum = 0;

    /* L4 FNAT translation */
    if (proto->fnat_in_handler) {
        err = proto->fnat_in_handler(proto, conn, mbuf);
        if (err != EDPVS_OK)
            goto errout;
    }

    if (likely(mbuf->ol_flags & PKT_TX_IP_CKSUM)) {
        ip4h->hdr_checksum = 0;
    } else {
        ip4_send_csum(ip4h);
    }

    return INET_HOOK(AF_INET, INET_HOOK_LOCAL_OUT, mbuf,
                     NULL, rt->port, ipv4_output);
}

省去部分无用代码,先看主逻辑

  1. mbuf_nat6to4_len 重新计算三层 pkt 长度,这里可以看源码除了要减去 ipv6 header,还要减去 next header 长度,最后再加上 ipv4 header length
  2. mbuf_6to4 函数把 ipv6 header 真正的变成 ipv4 header,看了内容就是正确的填充头部字段
  3. 然后调用 tcp_fnat_in_handler 填充 toa, 调整 seq
  4. 最后再调用 ipv4_output_fin2 走正常发送数据包流程

到这里,重点就是 mbuf_6to4,对于己建立连接的数据包,也是同样的流程

toa dpvs 做了哪些修改

struct tcpopt_ip4_addr {
    uint8_t opcode;
    uint8_t opsize;
    __be16 port;
    struct in_addr  addr;
} __attribute__((__packed__));

struct tcpopt_ip6_addr {
    uint8_t opcode;
    uint8_t opsize;
    __be16 port;
    struct in6_addr addr;
} __attribute__((__packed__));

struct tcpopt_addr {
    uint8_t opcode;
    uint8_t opsize;
    __be16 port;
    uint8_t addr[16];
} __attribute__((__packed__));

首先 toa 结构体变了,以前只有一个 tcpopt_addr,并且 addr 字段是 4 字节大小,现在为了兼容变成了 16 字节。

    /* insert toa right after TCP basic header */
    toa = (struct tcpopt_addr *)(tcph + 1);
    toa->opcode = TCP_OPT_ADDR;
    toa->opsize = tcp_opt_len;
    toa->port = conn->cport;

    if (conn->af == AF_INET) {
        struct tcpopt_ip4_addr *toa_ip4 = (struct tcpopt_ip4_addr *)(tcph + 1);
        toa_ip4->addr = conn->caddr.in;
    }
    else {
        struct tcpopt_ip6_addr *toa_ip6 = (struct tcpopt_ip6_addr *)(tcph + 1);
        toa_ip6->addr = conn->caddr.in6;
    }

利用结构体进行强转,然后给 tcp opt 赋值,这是填充 toa 操作。

toa kmod 内核做了哪些

首先 toa 是运行在 real server 上的,所以肯定进来的是 ipv4 数据,那么 nat64 的逻辑一定在 tcp_v4_syn_recv_sock_toa 里兼容。

static struct sock *
tcp_v4_syn_recv_sock_toa(struct sock *sk, struct sk_buff *skb,
            struct request_sock *req, struct dst_entry *dst)
{
    struct sock *newsock = NULL;
    int nat64 = 0;

    TOA_DBG("tcp_v4_syn_recv_sock_toa called\n");

    /* call orginal one */
    newsock = tcp_v4_syn_recv_sock(sk, skb, req, dst);

    /* set our value if need */
    if (NULL != newsock && NULL == newsock->sk_user_data) {
        newsock->sk_user_data = get_toa_data(AF_INET, skb, &nat64);
        sock_reset_flag(newsock, SOCK_NAT64);
        if (NULL != newsock->sk_user_data) {
            TOA_INC_STATS(ext_stats, SYN_RECV_SOCK_TOA_CNT);
#ifdef TOA_NAT64_ENABLE
            if (nat64) {
                struct toa_ip6_entry *ptr_ip6_entry = newsock->sk_user_data;
                ptr_ip6_entry->sk = newsock;
                toa_ip6_hash(ptr_ip6_entry);

                newsock->sk_destruct = tcp_v6_sk_destruct_toa;

                sock_set_flag(newsock, SOCK_NAT64);
            }
#endif
        }
        else
            TOA_INC_STATS(ext_stats, SYN_RECV_SOCK_NO_TOA_CNT);

        TOA_DBG("tcp_v4_syn_recv_sock_toa: set "
            "sk->sk_user_data to %p\n",
            newsock->sk_user_data);
    }
    return newsock;
}
  1. get_toa_data 生成 toa 数据,如果有 nat64 逻辑,sk_user_data 会赋值成 ptr_toa_entry
  2. 如果没有 nat64 逻辑,那么正常返回 toa_ip4_data
  3. sock_reset_flag(newsock, SOCK_NAT64) 将 ipv4 socket 设置 nat64 标记

real server 如何获取 src ip

        if (getsockopt(connfd, IPPROTO_IP, TOA_SO_GET_LOOKUP, &uaddr, &len) == 0) {
               inet_ntop(AF_INET6, &uaddr.saddr, from, sizeof(from));
            printf("  real client [%s]:%d\n", from, ntohs(uaddr.sport));
        } else {
            printf("client is %s\n", inet_ntoa(caddr.sin_addr));
        }

上面是 real server 获取 src ip 的例子,这里看出来线上如果想用 nat64 还是要修改源码的,除非你不关心,但是话说回来,如果 nginx 入口做了 patch 后端也不需要改的。

toa kmod 调用 inet64_getname_toa 填充真正的 src ip,这里也没啥好说的。

小结

由于工作原因,dpvs 暂时不会再碰了。以后用到了再分析。

上一篇 下一篇

猜你喜欢

热点阅读