Switch-Router

VPP: NAT

Published at 2022-04-11 | Last Update 2022-04-11

本系列文章仅为学习使用 VPP 过程中的一些自用笔记

VPP 的 NAT 功能是针对网关设备设计的

VPP 的 NAT 功能是针对网关设备设计的, 它的其中一端连接 local 网络(这一侧的接口称为 inside 口),另一端连接 external (这一个侧的接口称为 outside 口)。

根据报文的方向,流量可分为左图中的 in2out 和右图中的 out2in ,in2out 做 SNAT, out2in 做 DNAT

VPP NAT 的位置

VPP 中 SNAT 和 DNAT 的位置在报文输入到达 IP 层查找路由前,类比 Netfilter 模型的话,就是在 PREROUTING 阶段,

VPP没有类似于 Netfilter 中 LOCAL_IN 、FORWARD、OUTPUT 用于做 NAT,它只能在 PREROUTING 阶段做 NAT。

POSTROUTING 处可能可以,但是我暂时没有看懂output feature的用法。以下只考虑在 PREROUTING 处的 NAT

VPP NAT 以 plugin 的形式插入到进 node graph 的 node 之间的。在 ip4-input 到 ip4-lookup 的路径上,可以 enable nat44 feature,这样单播IP报文就会绕道 ip4-unicast 这个 arc,在这个 arc 中,VPP 可以进行 NAT 变换。feature 的开关是以 interface 为粒度的。如果我们想 enable 某个 interface 的 NAT 功能,那么需要做的就是将这个 interface

vpp# set int nat44 in G1
vpp# set int nat44 out G0

VPP NAT 的 conntrack 机制

VPP NAT 也有类似于 Netfilter conntrack 机制,它的表项称为 snat session,报文在 NAT 前总是尝试先搜索是否满足条件的 snat session,若有,则直接做 NAT。

具体实现

数据结构

snat_main_t
snat_main_t snat_main;

typedef struct snat_main_s
{
  /* Per thread data */
  snat_main_per_thread_data_t *per_thread_data;

  /* Find a static mapping by local */
  clib_bihash_8_8_t static_mapping_by_local; 

  /* Find a static mapping by external */
  clib_bihash_8_8_t static_mapping_by_external; 
  /* Static mapping pool */
  snat_static_mapping_t *static_mappings;

  /* Interface pool */
  snat_interface_t *interfaces;
 
  /* vector of outside fibs */
  nat_outside_fib_t *outside_fibs; 
    
  // other filed..  
} snat_main_t;

  • per_thread_data: 每线程的数据,后面详述
  • static_mapping_by_local:以 local 侧的地址端口信息为 key 存储的静态NAT规则哈希表,value 为静态映射表项 snat_static_mapping_t 在 static_mappings 池中的索引。

  • static_mapping_by_external:以 external 侧的地址端口信息为 key 存储的静态NAT规则哈希表,value 为 静态映射表项 snat_static_mapping_t 在 static_mappings 池中的索引

  • static_mappings:静态NAT规则池
  • interfaces: 使能了 NAT 特性的 interface 集合。 ref. snat_interface_add_del
  • outside_fibs:当使能了一个 interface 的 outside 侧 NAT 特性时,这个 interface 关联到的 fib 会以加入到该 vector。 ref. snat_interface_add_del.
typedef struct
{
  /* Main lookup tables */
  clib_bihash_8_8_t out2in; 
  clib_bihash_8_8_t in2out; 

  /* Session pool */
  snat_session_t *sessions;
  
  // ...
} snat_main_per_thread_data_t;

过程

使能 interface 的 inside NAT feature

假设使用命令行使能 interface G1 的 nat 功能,将其配置为 external 侧的接口

本文不考虑使能 output_feature 的情况,且worker线程数目为 1.

vpp# set interface nat44 out G1

调用命令行处理函数,sw_if_index 为 G1 的 index,is_inside 为 0,is_del 为 0

int snat_interface_add_del (u32 sw_if_index, u8 is_inside, int is_del)
{
  // code omitted...
  // 找到该 interface 关联的 fib  
  u32 fib_index = fib_table_get_index_for_sw_if_index (FIB_PROTOCOL_IP4, sw_if_index);

  // 由于我们添加的是 outside interface,所以这里得到的是 "nat44-out2in"
  feature_name = is_inside ? "nat44-in2out" : "nat44-out2in";
  
  // 由于我们添加的是 outside interface,这里将 interface 对应的 fib 添加到 outside_fibs vector 中
  if (!is_inside)
    {
      // code omitted
      if (!is_del)
		{
	  		vec_add2 (sm->outside_fibs, outside_fib, 1);
	  		outside_fib->refcount = 1;
	  		outside_fib->fib_index = fib_index;
		}
    }
  // 接下来进行 feature set,我们假设 G1 之前没有使能过 NAT 功能
  // 这里分配一个 snat 的 interface    
  pool_get (sm->interfaces, i);  // allocate a snat_interface_t
  i->sw_if_index = sw_if_index;  // 
  i->flags = 0;

  // 启动 "ip4-unicast" 这个 arc 上的 "nat44-out2in" 特性.
  vnet_feature_enable_disable ("ip4-unicast", feature_name, sw_if_index, 1, 0, 0);
}

接下来启动 G0 的 inside nat 功能

vpp# set interface nat44 in G0

与 G1 类似,这里会开启 G0 的 “nat44-in2out” 特性。

local 访问 external

此时 ip4-input node 会将报文传递给 snat_in2out_node,调用该 node 的处理函数,is_slow_path 为 0, is_output_feature 为0

static inline uword
snat_in2out_node_fn_inline (vlib_main_t * vm,
			    vlib_node_runtime_t * node,
			    vlib_frame_t * frame, int is_slow_path,
			    int is_output_feature)
{
    // code omitted
    while (n_left_from > 0)
    {
        // 解析出输入的报文的 IP 头和 transport header 以及输入的 interface 和关联的 fib
        ip0 = (ip4_header_t *) ((u8 *) vlib_buffer_get_current (b0) +
			      iph_offset0);
        udp0 = ip4_next_header (ip0);
        tcp0 = (tcp_header_t *) udp0;
        icmp0 = (icmp46_header_t *) udp0;
        
        sw_if_index0 = vnet_buffer (b0)->sw_if_index[VLIB_RX];
        rx_fib_index0 = vec_elt (sm->ip4_main->fib_index_by_sw_if_index, sw_if_index0);
        
        proto0 = ip_proto_to_nat_proto (ip0->protocol);
      
        // 根据 sip+sport+fib+proto 计算一个 key 
        init_nat_k (&kv0, ip0->src_address, vnet_buffer (b0)->ip.reass.l4_src_port, rx_fib_index0,
		            proto0);
        // 用该 key 搜索是否存在现成的 snat_session,
        if (clib_bihash_search_8_8(&sm->per_thread_data[thread_index].in2out, &kv0, &value0))
		{ 
           // 如果没有找到snat session,由于当前我们不是处于 slow path,则这里我们将报文传递给 slow path
	  	   if (is_slow_path)
	    	{
	      		next0 = slow_path (sm, b0, ip0, ip0->src_address, 
                   			   vnet_buffer (b0)->ip.reass.l4_src_port,
                   			   rx_fib_index0, proto0, &s0, node, next0,   
                   			   thread_index, now);
	    	}
	  		else
	    	{
	      		next0 = SNAT_IN2OUT_NEXT_SLOW_PATH;
	      		goto trace0;
	    	}
		}
      else
         // 如果找到,则得到现成的 snat_session
		 s0 = pool_elt_at_index (sm->per_thread_data[thread_index].sessions, value0.value);
    
    }
    
    // code omitted... 
}

没有找到线程 snat_session 的报文会走 slow path。在这里会优先尝试静态映射,如果不成功再尝试动态映射

static u32
slow_path (snat_main_t * sm, vlib_buffer_t * b0,
	   ip4_header_t * ip0,
	   ip4_address_t i2o_addr,
	   u16 i2o_port,
	   u32 rx_fib_index0,
	   nat_protocol_t nat_proto,
	   snat_session_t ** sessionp,  // slow path 的结果
	   vlib_node_runtime_t * node, u32 next0, u32 thread_index, f64 now)
{
	//
	// static mapping 高于 dynamic translation,如果我们配置了满足条件的静态 mapping 规则,则这里会有限使用
    if (snat_static_mapping_match
      (sm, i2o_addr, i2o_port, rx_fib_index0, nat_proto, &sm_addr,
       &sm_port, &sm_fib_index, 0, 0, 0, 0, 0, &identity_nat, 0))
    {
        // 如果没有的话 就会尝试  dynamic translation
        if (snat_alloc_outside_address_and_port (sm->addresses, rx_fib_index0,
					       thread_index,
					       nat_proto,
					       &sm_addr, &sm_port,
					       sm->port_per_thread,
					       sm->per_thread_data
					       [thread_index].snat_thread_index))
		{
	  		b0->error = node->errors[SNAT_IN2OUT_ERROR_OUT_OF_PORTS];
	  		return SNAT_IN2OUT_NEXT_DROP;
		}
    }
    else
    {
       	// code omitted...
		is_sm = 1;
    }
    // 无论是静态还是动态,这里得到映射的结果 转换后的源IP:sm_addr  源端口sm_port outside接口关联的fib:sm_fib_index
}

static mapping NAT

该功能用作 1:1 SNAT.

涉及命令

vpp# set interface nat44 in G0
vpp# set interface nat44 out G1
vpp# nat44 add address 10.100.1.1
vpp# nat44 add static mapping local 192.168.1.1 external 10.100.1.1
开启 interface 的 NAT44 功能

命令行触发

VLIB_CLI_COMMAND (set_interface_snat_command, static) = {
  .path = "set interface nat44",
  .function = snat_feature_command_fn,
  .short_help = "set interface nat44 in <intfc> out <intfc> [output-feature] "
                "[del]",
};

(完)