一、技术背景
IPSec提供了端到端的IP通信的安全性,但在NAT环境下对IPSec的支持有限,AH协议是肯定不能进行NAT的了,这和AH设计的理念是相违背的;ESP协议在NAT环境下最多只能有一个VPN主机能建立VPN通道,无法实现多台机器同时在NAT环境下进行ESP通信。关于IPSec在NAT环境下的需求问题在RFC3715中进行了描述。
NAT穿越(NAT Traversal,NAT-T)就是为解决这个问题而提出的,RFC3947,3948中定义,在RFC4306中也加入了NAT-T的说明,但并没废除RFC3947,3948,只是不区分阶段1和阶段2。该方法将ESP协议包封装到UDP包中(在原ESP协议的IP包头外添加新的IP头和UDP头),使之可以在NAT环境下使用的一种方法,这样在NAT的内部网中可以有多个IPSec主机建立VPN通道进行通信。
二、使用限制
AH封装的校验从IP头开始,如果NAT将IP的头部改动,AH的校验就会失败,因此我们得出结论,AH是无法与NAT共存的。ESP封装的传输模式:对于NAT来说,ESP封装比AH的优势在于,无论是加密还是完整性的校验,IP头部都没有被包括进去。但是还是有新的问题,对于ESP的传输模式,NAT 无法更新上层校验和。TCP 和 UDP 报头包含一个校验和,它整合了源和目标 IP 地址和端口号的值。
当 NAT 改变了某个包的 IP 地址和(或)端口号时,它通常要更新 TCP 或 UDP 校验和。当 TCP 或 UDP 校验和使用了 ESP 来加密时,它就无法更新这个校验和。由于地址或端口已经被 NAT 更改,目的地的校验和检验就会失败。虽然 UDP 校验和是可选的,但是 TCP 校验和却是必需的。
ESP封装的隧道模式:从ESP隧道模式的封装中,我们可以发现,ESP隧道模式将整个原始的IP包整个进行了加密,且在ESP的头部外面新加了一层IP头部,所以NAT如果只改变最前面的IP地址对后面受到保护的部分是不会有影响的。因此,IPsec只有采用ESP的隧道模式来封装数据时才能与NAT共存。
由于完整性校验牵涉到IP头部,所以NAT无法对其修改,不兼容。
ESP的传输模式,因为TCP部分被加密,NAT无法对TCP校验和进行修改,不兼容。
ESP的隧道模式,由于NAT改动外部的IP而不能改动被加密的原始IP,使得只有这种情况下才能与NAT共存。
三、IPSEC NAT-T数据转发实现
NAT-T的基本思路是IPSec封装好的数据包外再进行一次UDP数据封装,即在IP和ESP报文之间插入一个8个字节UDP头部(端口号默认为4500)。这样,当此数据包穿过NAT网关时,被修改的只是最外层的IP和udp数据报。
图1 ESP隧道模式加密
图2 ESP隧道模式NAT-T封装
VPP中数据转发面实现NAT-T功能分成两个部分:一是注册一个新的IPSEC node用于处理UDP收到的4500端口报文,并注册UDP 4500端口与之关联;二是实现IPSEC加解封装流程对NAT-T的支持。
3.1 NAT-T node处理实现
3.1.1 注册PSEC node
IPSEC协商检查到发生NAT转换,会把原有封装协商报文的UDP 500端口转换成4500端口,为了保证IPSEC后续协商正常进行,IPSEC控制面需要能够正常的处理UDP 4500端口的报文,因此需注册一个新的IPSEC node用于处理从UDP收到的4500端口报文。
注册一个ikev2_nat的node,主要定义ikev2_nat_node_fn函数和next_nodes。ikev2_nat_node_fn函数用于处理UDP 4500端口报文,包括协商和数据报文。next_nodes定义了该node后续的处理node,主要是UDP 4500数据报文送往IPSEC解密流程进行解密处理。
static vlib_node_registration_t ikev2_nat_node;
/* *INDENT-OFF* */
VLIB_REGISTER_NODE (ikev2_nat_node,static) = {
.function = ikev2_nat_node_fn,
.name = "ikev2_nat",
.vector_size = sizeof (u32),
.format_trace = format_ikev2_nat_trace,
.type = VLIB_NODE_TYPE_INTERNAL,
.n_errors = ARRAY_LEN(ikev2_nat_error_strings),
.error_strings = ikev2_nat_error_strings,
.n_next_nodes = IKEV2_NAT_N_NEXT,
.next_nodes = {
[IKEV2_NAT_NEXT_IPSEC4_INPUT] = "ipsec4-input-feature",
[IKEV2_NEXT_IP4_LOOKUP] = "ip4-lookup",
[IKEV2_NEXT_ERROR_DROP] = "error-drop",
},
};
/* *INDENT-ON* */
typedef enum
{
IKEV2_NAT_NEXT_IPSEC4_INPUT,
IKEV2_NAT_NEXT_IP4_LOOKUP,
IKEV2_NAT_NEXT_ERROR_DROP,
IKEV2_NAT_N_NEXT,
} ikev2_nat_next_t;
a、ikev2_nat_node_fn函数实现
在IPSEC控制面检查到链路中存在NAT设备后,后续的所有IPSEC报文(控制协商报文、NAT保活报文和IPSEC数据报文)都需要使用UDP 4500端口进行封装,因此需要实现一个node处理函数,用于处理从UDP接收的4500端口报文。该函数主要功能是根据配置的NAT-T封装类型,判断收到的报文是那种类型的报文,控制协商报文直接传递IPSEC控制面进行处理,NAT-T保活报文在函数内部直接丢弃,IPSEC数据报文则偏移UDP头并传递给解密模块进行后续解密处理。
/* UDP encapsulation types */
#define UDP_ENCAP_ESPINUDP_NON_IKE 1 /* draft-ietf-ipsec-nat-t-ike-00/01 */
#define UDP_ENCAP_ESPINUDP 2 /* draft-ietf-ipsec-udp-encaps-06 */
static uword ikev2_nat_node_fn (vlib_main_t * vm,
vlib_node_runtime_t * node, vlib_frame_t * frame)
{
.........
switch (encap_type) {
default:
case UDP_ENCAP_ESPINUDP:
/* Check if this is a keepalive packet. If so, eat it. */
if (len == 1 && udpdata[0] == 0xff) {
goto drop;
} else if (len > sizeof(struct esp_header_t) && udpdata32[0] != 0) {
/* ESP Packet without Non-ESP header */
len = sizeof(struct udp_header_t);
} else
/* Must be an IKE packet.. pass it through */
goto ikev2_deal;
break;
case UDP_ENCAP_ESPINUDP_NON_IKE:
/* Check if this is a keepalive packet. If so, eat it. */
if (len == 1 && udpdata[0] == 0xff) {
goto drop;
} else if (len > 2 * sizeof(u32) + sizeof(struct esp_header_t) &&
udpdata32[0] == 0 && udpdata32[1] == 0) {
/* ESP Packet with Non-IKE marker */
len = sizeof(struct udp_header_t) + 2 * sizeof(u32);
} else
/* Must be an IKE packet.. pass it through */
goto ikev2_deal;
break;
}
/*ESP Packet, goto ipsec input node*/
goto ipsec_input_deal;
.........
return frame->n_vectors;
}
3.1.2 注册UDP监听4500端口
在IPSEC ike初始化函数中添加UDP 4500端口监听,并把其和ikev2_nat_node进行关联,这样UDP模块收到4500端口报文,则会交给ikev2_nat_node的处理函数进行后续处理。
clib_error_t *
ikev2_init (vlib_main_t * vm)
{
………
udp_register_dst_port (vm, 500, ikev2_node.index, 1);
udp_register_dst_port (vm, UDP_DST_PORT_ipsec, ikev2_nat_node.index, 1);
// 把ikev2_nat_node的index和UDP 4500进行关联
return 0;
}
注:UDP_DST_PORT_ipsec宏为VPP中定义的IPSEC NAT-T 4500端口。
3.2、IPSEC NAT-T加解封装实现
因IPSEC NAT-T只支持ESP的隧道模式,因此只需在ESP的隧道模式处理流程中加入UDP头部的处理即可。其实现思路为:加封装方向,当进行ESP封装时,检查到SA中的udp_encap标记被设置,则在封装外部IP头前先根据设置的NAT-t封装类型,封装一个UDP头,并调整buffer的各个字段;解封装方向,收到UDP 4500端口报文,则直接跳过UDP头部进行后续处理。
3.2.1 SA中添加封装结构
NAT-T的封装源端口和type是变化的,因此需要从SA中去获取每一次的值。
struct ipsec_encap_tmpl {
__u16 encap_type;
__be16 encap_sport;
__be16 encap_dport;
};
typedef struct
{
……….
u8 udp_encap;
ipsec_encap_tmpl encap;//for NAT-T
………
} ipsec_sa_t;
3.2.2 ESP 中加封装实现
如果SA中udp_encap标记被设置,则直接根据ipsec_encap_tmpl结构进行封装处理。如果封装类型为UDP_ENCAP_ESPINUDP_NON_IKE,则需要添加两个U32的数据。
always_inline uword
esp_encrypt_inline (vlib_main_t * vm,
vlib_node_runtime_t * node, vlib_frame_t * from_frame,
int is_ip6)
{
if (sa0->udp_encap)
{
u32 ike_len = 0;
struct udp_header_t *uh;
ouh0->udp.src_port = sa0->encap.encap_sport;
ouh0->udp.dst_port = sa0->encap.encap_dport;
ouh0->udp.checksum = 0;
uh = &ouh0->udp;
switch (sa0->encap.encap_type) {
default:
case UDP_ENCAP_ESPINUDP:
break;
case UDP_ENCAP_ESPINUDP_NON_IKE:
udpdata32 = (__be32 *)(uh + 1);
udpdata32[0] = udpdata32[1] = 0;
ike_len = 2*sizeof(udpdata32);
break;
}
ouh0->ip4.protocol = IP_PROTOCOL_UDP;
ip_udp_hdr_size =
sizeof (udp_header_t) + sizeof (ip4_header_t) + ike_len;
ouh0->udp.length = data_len;
}
}
3.2.3 ESP解封装中处理
解封装流程,在获取ESP头部时需要判断IP头部中的协议字段,协议字段是IP_PROTOCOL_UDP时需要偏移UDP头部才能正确的获取ESP头。
VLIB_NODE_FN (ipsec4_input_node) (vlib_main_t * vm,
vlib_node_runtime_t * node,
vlib_frame_t * from_frame)
{
........
esp_header_t *esp0;
esp0 = (esp_header_t *) ((u8 *) ip0 + ip4_header_bytes (ip0));
if (PREDICT_FALSE (ip0->protocol == IP_PROTOCOL_UDP))
{
esp0 = (esp_header_t *) ((u8 *) esp0 + sizeof (udp_header_t));//偏移UDP头部
}
.......
}
3.2.4 QAT硬件加解密处理(选做)
在QAT处理流程中同样加入对UDP头部的处理,加封装流程判断SA中udp_encap标记确定是否需要封装UDP 头部。解封装流程添加对UDP 报文的处理,这里同样需要对UDP报文进行区分,如果是IPSEC NAT-T非数据报文,则需要上送控制面进行处理,QAT中只处理解封装数据报文。