这几天 iqiyi 同学做了大更新,dpvs 增加 nat64 功能,这样机房对外暴露 ipv6, 对内还不用修改 ipv4 业务代码。算是重量级武器吧,为 ipv6 升级过渡提供了双栈的保障。
提前想好的问题
- 如何使用 nat64 配置呢?会不会很繁琐
- 后端 real server 看到的也是 ipv6 地址?怎么正确的获取源 ip,涉及 toa 的实现
-
为了兼容 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);
}
省去部分无用代码,先看主逻辑
-
mbuf_nat6to4_len
重新计算三层 pkt 长度,这里可以看源码除了要减去 ipv6 header,还要减去 next header 长度,最后再加上 ipv4 header length -
mbuf_6to4
函数把 ipv6 header 真正的变成 ipv4 header,看了内容就是正确的填充头部字段 - 然后调用
tcp_fnat_in_handler
填充 toa, 调整 seq - 最后再调用
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;
}
-
get_toa_data
生成 toa 数据,如果有 nat64 逻辑,sk_user_data 会赋值成 ptr_toa_entry - 如果没有 nat64 逻辑,那么正常返回 toa_ip4_data
- 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 暂时不会再碰了。以后用到了再分析。
网友评论