美文网首页
dpvs学习笔记: 18 nat64 的实现

dpvs学习笔记: 18 nat64 的实现

作者: 董泽润 | 来源:发表于2019-03-13 13:46 被阅读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 暂时不会再碰了。以后用到了再分析。

相关文章

网友评论

      本文标题:dpvs学习笔记: 18 nat64 的实现

      本文链接:https://www.haomeiwen.com/subject/xgahuqtx.html