1、前言
DPDK源码提供了不少的用户程序示例,在源码的 ./examples/ 目录下可以看到这些示例,有诸如2层转发、三层转发、ACL之类的常见网络应用,我注意到没有NAT的内容,于是打算基于已有的示例程序编写一个。
编写一个通用的内容需要花费不少时间,并且成品也更加复杂,这并不符合DPDK的使用场景。我认为,在特定的网络环境下,DPDK程序应该在满足网络需求的条件下尽量精简,毕竟,如果不是为了提高网络性能而是追求通用性,那为什么不选择Linux网络内核呢?
2、网络规划
因此,在给出程序代码之前,有必要了解到将要应用的场景。我预想中的简单网络可以由下图描述:
可以看到,上图详细表明了各个网络接口的MAC及IP,并表明这三台虚拟机的网口都是桥接模式。在网络访问过程中,client 将直接访问其网关 192.168.31.100 ,DPDK NAT 用户程序将数据包的源IP地址、目的IP地址修改,并修改源MAC、目的MAC之后发送给server,server发送给client的回复报文也是类似处理。
另外,可以看到这里给出的各个接口都位于 192.168.31.x 网段,因此,需要在client和server上分别添加他们的网关的ARP,否则访问将会异常。对于一个简单示例而言,再添加ARP功能似乎不是那么好的选择,因此,先这样。
3、代码及解析
针对此模型,我在 dpdk-19.05 源码示例 ./examples/l3fwd-vf 的基础上完成了NAT应用程序的编写,具体修改的是 l3fwd_simple_forward 函数,代码如下:
#define NAT
#define PORT2CLIENT 0
#define PORT2SERVER 1
#define CLIENT_MAC 0x38cf85239c30
#define SERVER_MAC 0xb29e29290c00
#define CLIENT_IP "192.168.31.214"
#define SERVER_IP "192.168.31.197"
#define CLIENT_GW "192.168.31.100"
#define SERVER_GW "192.168.31.200"
static inline void
l3fwd_simple_forward(struct rte_mbuf *m, uint16_t portid,
lookup_struct_t *l3fwd_lookup_struct)
{
struct ether_hdr *eth_hdr;
struct ipv4_hdr *ipv4_hdr;
void *tmp;
uint16_t dst_port;
eth_hdr = rte_pktmbuf_mtod(m, struct ether_hdr *);
ipv4_hdr = rte_pktmbuf_mtod_offset(m, struct ipv4_hdr *,
sizeof(struct ether_hdr));
#ifdef NAT //打印收到的报文的信息
char mac[30] = {0};
struct in_addr addr;
ether_format_addr(mac, 30, ð_hdr->s_addr);
printf("recieve packet, src_mac[%s],", mac);
ether_format_addr(mac, 30, ð_hdr->d_addr);
printf("dst_mac[%s];", mac);
addr.s_addr = ipv4_hdr->src_addr;
printf("src_ip:%s;", inet_ntoa(addr));
addr.s_addr = ipv4_hdr->dst_addr;
printf("dst_ip:%s; ", inet_ntoa(addr));
printf("portid:%d\n", portid);
#endif
#ifdef DO_RFC_1812_CHECKS
/* Check to make sure the packet is valid (RFC1812) */
if (is_valid_ipv4_pkt(ipv4_hdr, m->pkt_len) < 0) {
rte_pktmbuf_free(m);
return;
}
#endif
dst_port = get_dst_port(ipv4_hdr, portid, l3fwd_lookup_struct);
if (dst_port >= RTE_MAX_ETHPORTS || (enabled_port_mask & 1 << dst_port) == 0)
dst_port = portid;
#ifdef NAT
if(PORT2CLIENT == portid){
dst_port = PORT2SERVER;
}
else{
dst_port = PORT2CLIENT;
}
#endif
/* 02:00:00:00:00:xx */
tmp = ð_hdr->d_addr.addr_bytes[0];
#ifndef NAT
*((uint64_t *)tmp) = 0x000000000002 + ((uint64_t)dst_port << 40);
#endif
#ifdef DO_RFC_1812_CHECKS
/* Update time to live and header checksum */
--(ipv4_hdr->time_to_live);
++(ipv4_hdr->hdr_checksum);
#endif
/* src addr */
ether_addr_copy(&ports_eth_addr[dst_port], ð_hdr->s_addr);
#ifdef NAT //修改以太网头及ip头
if(PORT2SERVER == dst_port){
*((uint64_t *)tmp) = SERVER_MAC;
}
else{
*((uint64_t *)tmp) = CLIENT_MAC;
}
printf("Nat and foward packet, ");
ether_format_addr(mac, 30, ð_hdr->s_addr);
printf("src_mac[%s],", mac);
ether_format_addr(mac, 30, ð_hdr->d_addr);
printf("dst_mac[%s];", mac);
if(PORT2SERVER == dst_port){
inet_aton(SERVER_IP, &addr);
ipv4_hdr->dst_addr = addr.s_addr;
inet_aton(SERVER_GW, &addr);
ipv4_hdr->src_addr = addr.s_addr;
}
else{
inet_aton(CLIENT_IP, &addr);
ipv4_hdr->dst_addr = addr.s_addr;
inet_aton(CLIENT_GW, &addr);
ipv4_hdr->src_addr = addr.s_addr;
}
addr.s_addr = ipv4_hdr->src_addr;
printf("src:%s,",inet_ntoa(addr));
addr.s_addr = ipv4_hdr->dst_addr;
printf("dst:%s; ",inet_ntoa(addr));
ipv4_hdr->hdr_checksum = 0;
ipv4_hdr->hdr_checksum = rte_ipv4_cksum(ipv4_hdr);
printf("dst_port=%d\n", dst_port);
#endif
send_single_packet(m, dst_port);
}
正如一开始所说,这个程序做的主要工作就是修改源IP、目的IP以实现NAT,并修改源MAC、目的MAC以完成二层转发,除此之外,我并未如何修改及使用原示例程序中的内容,如本身三层路由功能我就并未使用,只是简单修改了目的端口,这对于示例程序来说足够了,并且,这可以更直观地体现出DPDK的灵活性。事实上,修改TTL也是没有必要的,因为本实例给出的所有相关网络接口都位于同一网段。
构建DPDK框架,编译并运行此用户程序,client发出的所有流向其网关192.168.31.100的报文都会NAT并转给server,这一切都可以由程序本身打印出来,以下是一个实例:
$ sudo ./run.sh
......
recieve packet, src_mac[30:9C:23:85:CF:38],dst_mac[00:0C:29:53:03:27];src_ip:192.168.31.214;dst_ip:192.168.31.100; portid:0
Nat and foward packet, src_mac[00:00:29:53:03:31],dst_mac[00:0C:29:29:9E:B2];src:192.168.31.200,dst:192.168.31.197; dst_port=1
recieve packet, src_mac[00:0C:29:29:9E:B2],dst_mac[00:0C:29:53:03:31];src_ip:192.168.31.197;dst_ip:192.168.31.200; portid:1
Nat and foward packet, src_mac[00:00:29:53:03:27],dst_mac[30:9C:23:85:CF:38];src:192.168.31.100,dst:192.168.31.214; dst_port=0
在这里,我编写了一个脚本./run.sh来构建DPDK框架及编译、运行NAT用户程序,并且我省略了用户程序运行时的打印(用省略号表示)。以上打印是当client ping其网关192.168.31.100时的实时打印,它清楚地给出了icmp报文的处理过程,以便大家更好地理解DPDK的工作方式。
3、总结及展望
我编写了一个十分简单的DPDK用户程序以实现NAT功能,它工作的环境略显苛刻,但正因此此程序足够简单明了,可以足够清晰地展示dpdk中是如何一层层剥开数据报文,并根据需求修改各个字段的值。
在程序中我仅解析、修改了二层及三层的报头,实际上,可以进一步分析传输层头部,因此把源端口、目的端口,再加上三层报头可以获取到的源IP地址、目的IP地址、传输层协议,凑齐五元组,并用哈希表存储NAT规则,以实现跟iptables提供的NAT功能一样的效果。