Open vSwitch源码解析之基于VxLAN实现NSH解析功能

时间:2022-05-06
本文章向大家介绍Open vSwitch源码解析之基于VxLAN实现NSH解析功能,主要内容包括1. NSH-SFC概述、1.2 NSH-SFC(with VxLAN)工作流程、1.3 VxLAN存在的意义、2. OVS源码解读。、2.3 OVS作为classifier对数据进行的NSH封装、3. 总结、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

1. NSH-SFC概述

当前SFC的实现方案主要分为两种:一种基于NSH(network service header)。数据封装时,在L2或者L3数据后添加NSH头,然后进行L3或L4的封装。转发时,根据nshheader去转发SFC中的数据,整个过程都是依据同一个SPI(service path id)和递减的SI(service index);另一种无NSH头,在转发过程中,SFF需要不停对新来的数据包进行判断,确定是否属于某个SFC。ODL的子项目SFC就是第一种的实现。

1.1 NSH-SFC组件简述

Classifier:分类器,根据controller来的policy来决策是否给数据进行nsh封装,并且把数据发送至相应的sff。 Service Function(SF):网络中的Middleware,常见的有loadbalance,firework,NAT等。值得注意的是,在NSH方案下,如果SF没有NSH解析功能,那么需要在SF和其对应的SFF之间增加一个proxy。该proxy用于解析NSH,并且帮助完成nshheader中SI的自减算法(如果SI不能成功自减,那么SFF会认为这个数据没有被forward到该SF,从而再次forward向该SF,最后的结果就是产生网络风暴)。

Service Function Forwarder(SFF):SFC中的数据中转站,负责数据的转发。当SI=1的时候,SFF负责把nsh头部去掉。

1.2 NSH-SFC(with VxLAN)工作流程

  • 控制层面定义SFC:
  • 用户利用SDN提供的北向应用,创建sfc1,controller会做相应的配置工作,产生对应的数据通道(sfc1本身是一个chain,可以和vxlan的tunnel完美匹配。那么,定义了sfc1,同时可以对应一个vxlan。所以,可以理解为sfc1同时绑定了vxlan和NSH中的path信息)。
  • 数据首先通过classifier,classifier为sfc1的数据打上nsh头信息,然后转交sff1。
  • sff1将数据发送至sf1,sf1将数据处理完后,为nshheader中的si执行自减操作,然后返回到sff1.
  • sff1将数据交到sff2,sff2重复sff的职责...
  • 对于没有nsh解析能力的sf2,proxy会帮其完成nsh相关工作.
  • 在si=0(有文档说si=1)的时候,sff会给数据包去nsh,那么整个sfc1流程就结束

1.3 VxLAN存在的意义

首先,NSH不一定需要依托于VxLAN,GRE + NSH、VXLAN-gpe + NSH、Ethernet + NSH都是可行的方案。其实,NSH只是一个封装技术,起了一个标签的作用,本身并不会影响传输。 在SFC中,很重要的一点是SFF能够尽快、准确地把带NSH头的数据包转发出去。那么,SFF需要做的工作实际根据nshheader去择路,而SFF中的路是controller通过流表已经创建好了的。所以,进一步说,SFF只需要把NSH头和某个流表统一起来即可。

struct sw_flow {
   struct rcu_head rcu;
   struct hlist_node hash_node[2];
   u32 hash;
   int stats_last_writer;    /* NUMA-node id of the last writer on
                * 'stats[0]'.
                */
   struct sw_flow_key key; //流表匹配时候用到的key值
   struct sw_flow_key unmasked_key;
   struct sw_flow_mask *mask;
   struct sw_flow_actions __rcu *sf_acts;
   struct flow_stats __rcu *stats[]; /* One for each NUMA node.  First one
                  * is allocated at flow creation time,
                  * the rest are allocated on demand
                  * while holding the 'stats[0].lock'.
                  */
};
struct sw_flow_key {
   u8 tun_opts[255];
   u8 tun_opts_len;
   struct ovs_key_ipv4_tunnel tun_key;  //该流表对应的tunnel信息
   .......
   }
struct ovs_key_ipv4_tunnel {
   __be64 tun_id;
   __be32 nsp;      /* it contains (nsp - 24 bits | nsi - 8 bits) here */
                       __be32 nshc1;    /* NSH context headers */
                       __be32 nshc2;
                       __be32 nshc3;
                       __be32 nshc4;
                       __be32 ipv4_src;
                       __be32 ipv4_dst;
   __be16 tun_flags;
   u8   ipv4_tos;
   u8   ipv4_ttl;
} __packed __aligned(4); /* Minimize padding. */

每个openflow流表去匹配的时候,sw_key结构体是匹配的关键,该结构体包含了隧道信息。而nshheader本身的SPI就可以看成一种隧道信息,那么用vxlan便很轻易的统一起来了。

综合起来,VxLAN的tunnel信息和openflow流表就可以统一起来了。

上图其实也是sff去匹配流表的过程。

2. OVS源码解读。

首先是NSH和VxLAN的header结构体。

2.1NSHheader和VxLANheader结构体

NSH

struct nshhdr {
   struct nsh_base b;
   struct nsh_ctx c;
};

很清楚的表明:包含base和ctx部分,其中base尤为重要,包含了路径信息;ctx用于sf之间传递metadata。

struct nsh_base {
#if defined(__LITTLE_ENDIAN_BITFIELD) //小端序
   __u8   res1:4;
   __u8   c:1;
   __u8   o:1;
   __u8   ver:2;
 
   __u8   len:6;
   __u8   res2:2;
#elif defined(__BIG_ENDIAN_BITFIELD)  //大端序
   __u8   ver:2;
   __u8   o:1;
   __u8   c:1;
   __u8   res1:4;
 
   __u8   res2:2;
   __u8   len:6;
#else
#error "Bitfield Endianess not defined."
#endif
   __u8   mdtype;            //mdtype决定了ctx的格式
   __u8   proto;             //next protocol
   union {
      struct {
         __u8   svc_path[3]; //spi
         __u8   svc_idx;    //si
      };
      __be32 b2;
   };
};

base的结构体

struct nsh_ctx {
   __be32 c1;
   __be32 c2;
   __be32 c3;
   __be32 c4;
};

ctx结构体

协议图

VxLANheader

struct vxlanhdr {
   __be32 vx_flags;
   __be32 vx_vni;
};

vni是重点

#define VXLAN_HLEN (sizeof(struct udphdr) + sizeof(struct vxlanhdr))
#define NSH_HLEN (sizeof(struct udphdr) + 
        sizeof(struct vxlanhdr) + 
        sizeof(struct nshhdr))

这部分代码说明了数据包封装过后的格式,如下图(请无视gpe)

2.2 VxLAN的创建和数据的监听和处理

首先介绍下linux内核中sock和socket还有skb。

socket和sock的区别和联系:大家都是socket通信中的socket,不过应用的网络层次有区别。socket主要作用于Linux系统中的BSD层(L6层),INET层很少应用, BSD和INET层分别对应ISO中的表示层和会话层,其结构体相对sock而言比较简单;而sock贯穿了硬件层和设备接口层和IP层INET层,贯穿了L2-L5层,而且是各层之间相互联系的重要纽带。

skb:sk_buff 是网络数据报在内核中的表现形式,数据包在内核协议栈中是通过skb的数据结构来实现的。

union {
	struct tcphdr	*th; //传输层tcp,指向首部第一个字节位置
	struct ethhdr	*eth; //链路层上,指向以太网首部第一个字节位置
	struct iphdr	*iph; //网络层上,指向ip首部第一个字节位置
	struct udphdr	*uh; //传输层udp协议,
	unsigned char	*raw; //随层次变化而变化,链路层=eth,网络层=iph
	unsigned long	seq; //针对tcp协议的待发送数据包而言,表示该数据包的ACK值
  } h;

这部分代码是sk_buffer结构体中的联合体,很明显可以看出来skb贯穿了协议栈的各层。

下面是VxLAN的创建过程: 1、vport.c定义了实现了vport最基本的或者共有的一些功能, 如添加、删除等功能,还有数据的收发功能。

2、vport-XXX.c是vport基于其他的一些个性化实现,如vport-vxlan.c实现了一些vxlan的功能:tunnel的创建、删除等。

2.2.1 OVS相关socket结构体

/* per UDP socket information */
struct vxlan_sock {
   struct hlist_node hlist;
   vxlan_rcv_t     *rcv; //数据处理函数,在收到数据后回调该函数
   void       *data;
   struct work_struct del_work; //调用linux内核工作队列需要用到的结构体, 该结构体定义了新启线程需要完成的工作的抽象
   struct socket   *sock;
   struct rcu_head      rcu; //linux线程锁
};

上面是VxLAN对应的socket,注意其中的struct socket   *sock; 他名字叫做sock,其实是socket结构体的实例。

接着看struct socket   *sock对应的结构体(linux内核中的函数)

struct socket
{
	socket_state		state;
 
	unsigned long		flags;
	struct proto_ops	*ops;
	struct inode		*inode;
	struct fasync_struct	*fasync_list;	/* Asynchronous wake up list	*/
	struct file		*file;		/* File back pointer for gc	*/
	struct sock		*sk;    //指向sock
	wait_queue_head_t	wait;
 
	short			type;
	unsigned char		passcred;
};

注意struct sock *sk;  指向了sock。sock的结构体就不贴了,但是要注意的是sock中也有指向socket的指针,还有指向数据处理函数的指针。

综合起来,VxLAN中的几个socket联系如下:

2.2.2 VxLAN的创建过程,packet和流表的匹配

在用ovs-vsctl工具下命令创建VxLAN的时候,会去调用vport-vxlan.c中的ovs_vxlan_vport_ops.create命令,对应函数如下:

static struct vport *vxlan_tnl_create(const struct vport_parms *parms)
{
   struct net *net = ovs_dp_get_net(parms->dp);
   struct nlattr *options = parms->options;//vxlan
   struct vxlan_port *vxlan_port;
   struct vxlan_sock *vs;
   struct vport *vport;
   struct nlattr *a;
   u16 dst_port;
   int err;
 
   if (!options) {          //option数据不能为空
      err = -EINVAL;
      goto error;
   }
   a = nla_find_nested(options, OVS_TUNNEL_ATTR_DST_PORT); //从option中提取dst port
   if (a && nla_len(a) == sizeof(u16)) {
      dst_port = nla_get_u16(a);
   } else {
      /* Require destination port from userspace. */
      err = -EINVAL;
      goto error;
   }
 
   vport = ovs_vport_alloc(sizeof(struct vxlan_port), //初始化vport,&ovs_vxlan_vport_ops是 vport相关的操作,此处为默认的几种,可以自己添加
            &ovs_vxlan_vport_ops, parms);   /*struct vport *ovs_vport_alloc(int priv_size, const struct vport_ops *ops,
                                                               const struct vport_parms *parms)*/
   if (IS_ERR(vport))  //初始化是否成功
      return vport;
 
   vxlan_port = vxlan_vport(vport); //为vxlan_vport分配私有数据区
   strncpy(vxlan_port->name, parms->name, IFNAMSIZ);
 
   vs = vxlan_sock_add(net, htons(dst_port), vxlan_rcv, vport, true, false); //为vport创建  vxlan_sock
   if (IS_ERR(vs)) {        //判断是否创建成功
      ovs_vport_free(vport);
      return (void *)vs;
   }
   vxlan_port->vs = vs;
 
   return vport;
 
error:
   return ERR_PTR(err);
}

这个函数中比较重要的是vxlan_port = vxlan_vport(vport)和vxlan_sock_add(net, htons(dst_port), vxlan_rcv, vport, true, false);

  • vxlan_port = vxlan_vport(vport)为该VxLAN的port申请私有数据区,该数据区可以存放任何数据。这也为逻辑端口vport和物理端口net_device提供绑定的空间。事实上,vport和net-device之间还隔着一个结构体netdev_vport,该数据区就可以用在存放vport对应的netdev_port。
  • vs = vxlan_sock_add(net, htons(dst_port), vxlan_rcv, vport, true, false)把vxlan_rcv数据处理函数注册到了vs中。

为了思路的连续性,我们先接着看主线vxlan_sock_add,最后回头再看数据处理部分。

struct vxlan_sock *vxlan_sock_add(struct net *net, __be16 port,
              vxlan_rcv_t *rcv, void *data,
              bool no_share, bool ipv6)
{
   return vxlan_socket_create(net, port, rcv, data);
}
 
static struct vxlan_sock *vxlan_socket_create(struct net *net, __be16 port,
                     vxlan_rcv_t *rcv, void *data)
{
   struct vxlan_sock *vs; //最后返回的vs
   struct sock *sk;
   struct sockaddr_in vxlan_addr = {
      .sin_family = AF_INET,
      .sin_addr.s_addr = htonl(INADDR_ANY),
      .sin_port = port,
   };
   int rc;
 
   vs = kmalloc(sizeof(*vs), GFP_KERNEL); //为vs分配内存
   if (!vs) {
      pr_debug("memory alocation failuren");
      return ERR_PTR(-ENOMEM);
   }
 
   INIT_WORK(&vs->del_work, vxlan_del_work); //调用linux内核中工作队列,去执行del工作
 
   /* Create UDP socket for encapsulation receive. */
   rc = sock_create_kern(AF_INET, SOCK_DGRAM, IPPROTO_UDP, &vs->sock);//创建用于接收udp包的socket
   if (rc < 0) {
      pr_debug("UDP socket create failedn");
      kfree(vs);
      return ERR_PTR(rc);
   }
 
   /* Put in proper namespace */
   sk = vs->sock->sk;
   sk_change_net(sk, net);
 
   rc = kernel_bind(vs->sock, (struct sockaddr *) &vxlan_addr,  //将刚刚创建的socket和正在创建的vxlan绑定 ,  其中vs->sk指向该socket    kernel_bind为Linux内核里面的绑定函数
         sizeof(vxlan_addr));
   if (rc < 0) {
      pr_debug("bind for UDP socket %pI4:%u (%d)n",
            &vxlan_addr.sin_addr, ntohs(vxlan_addr.sin_port), rc);
      sk_release_kernel(sk);
      kfree(vs);
      return ERR_PTR(rc);
   }
   vs->rcv = rcv;
   vs->data = data;
 
   /* Disable multicast loopback */
   inet_sk(sk)->mc_loop = 0;
   rcu_assign_sk_user_data(vs->sock->sk, vs);
 
   /* Mark socket as an encapsulation socket. */
   udp_sk(sk)->encap_type = 1;                   //将sk转换成udp内部使用的sk类型,
   udp_sk(sk)->encap_rcv = vxlan_udp_encap_recv; //绑定vxlan收到udp包后的处理函数
   udp_encap_enable();
   return vs;
}
  • vs->rcv = rcv;
  • sk = vs->sock->sk;
  • udp_sk(sk)->encap_rcv = vxlan_udp_encap_recv;

主线到这里就算结束了,最后的结果是创建了一个vxlan_sock和一个socket和一个sock,中间的socket只是一个索引,vxlan_sock->socket->sock。其中vxlan_sock和sock注册了各自的数据处理函数。 一个很重要的点是:在linux内核中,无论收发数据,都会被缓存到sock 结构中的缓冲队列中。

整理下主线结构: 创建vxlan的流程 ovs_vport_add->vport_ops_list[i].create->ovs_vxlan_vport_ops.create-> vxlan_tnl_create->vxlan_sock_add->vxlan_socket_create.

/* Callback from net/ipv4/udp.c to receive packets */
static int vxlan_udp_encap_recv(struct sock *sk, struct sk_buff *skb)
{
   struct vxlan_sock *vs;
   struct vxlanhdr *vxh;
   struct udphdr *udp;
   bool isnsh = false;
   __be32 nsp = 0;
   __be32 c1 = 0;
   __be32 c2 = 0;
   __be32 c3 = 0;
   __be32 c4 = 0;
 
   udp = (struct udphdr *)udp_hdr(skb);
   if (udp->dest == htons(NSH_DST_PORT))  //根据udp目的端口(6633)来判断是否含有nsh
      isnsh = true;                 //htons HBO to NBO
 
   /* Need Vxlan and inner Ethernet header to be present */
   if (!pskb_may_pull(skb, isnsh ? NSH_HLEN : VXLAN_HLEN)) // 检查skb长度是否能够满足把后面的头部去掉 如果不够返回-1 报错
      goto error;
 
   /* Return packets with reserved bits set */
   vxh = vxlan_hdr(skb);                           //提取vxlanheader
   if (vxh->vx_flags != htonl(VXLAN_FLAGS) ||
       (vxh->vx_vni & htonl(0xff))) {
      pr_warn("invalid vxlan flags=%#x vni=%#xn",
         ntohl(vxh->vx_flags), ntohl(vxh->vx_vni));
      goto error;
   }
 
   if (isnsh) {
      struct nshhdr *nsh = nsh_hdr(skb);
      if (unlikely(nsh->b.svc_idx == 0 || nsh->b.ver ||                 //如果 si == 0  sfc结束,直接drop
                     nsh->b.len != 6 || nsh->b.mdtype != 0x01 ||
                     nsh->b.proto != NSH_P_ETHERNET)) {
         pr_warn("NSH service index reached zero or not supportedn");
         goto drop;
      }
 
      nsp = nsh->b.b2; /* same as svc_path | htonl(svc_idx) */
        c1 = nsh->c.c1;  /* NSH Contexts */
        c2 = nsh->c.c2;
        c3 = nsh->c.c3;
        c4 = nsh->c.c4;
   }
 
   if (iptunnel_pull_header(skb, isnsh ? NSH_HLEN : VXLAN_HLEN,
             htons(ETH_P_TEB)))
      goto drop;
 
   vs = rcu_dereference_sk_user_data(sk);
   if (!vs)
      goto drop;
 
   vs->rcv(vs, skb, vxh->vx_vni, nsp, c1, c2, c3, c3); //调用vs->rcv,即
   return 0;
 
drop:
   /* Consume bad packet */
   kfree_skb(skb);
   return 0;
 
error:
   /* Return non vxlan pkt */
   return 1;
}

上面这个为sock注册过的处理函数,主要工作是一层层的剥开skb的封装(当然,里面会判断是否含有nshheader),提取udp,vxlan 和nshheader的头部信息。

  • sock在之前被强制转换成了udp的socket,该函数由linux内核中net/ipv4/udp.c回调.
  • if (udp->dest == htons(NSH_DST_PORT)) nsh的判断依据
  • si=0 的时候,sfc结束,drop packet
  • 核心调用 vs->rcv(vs, skb, vxh->vx_vni, nsp, c1, c2, c3, c3);
static void vxlan_rcv(struct vxlan_sock *vs, struct sk_buff *skb,
            __be32 vx_vni, __be32 nsp, __be32 nshc1, __be32 nshc2,
            __be32 nshc3, __be32 nshc4)
{
   struct ovs_tunnel_info tun_info;
   struct vport *vport = vs->data;
   struct iphdr *iph;
   __be64 key;
 
   /* Save outer tunnel values */
   iph = ip_hdr(skb);
   key = cpu_to_be64(ntohl(vx_vni) >> 8);
   ovs_flow_tun_info_init(&tun_info, iph, key, nsp, nshc1, nshc2,
        nshc3, nshc4, TUNNEL_KEY | TUNNEL_NSP | TUNNEL_NSI | TUNNEL_NSHC,
        NULL, 0);
 
   ovs_vport_receive(vport, skb, &tun_info); //回到vport.c中处理
}0

该函数最重要的一句无疑是ovs_vport_receive(vport, skb, &tun_info).

之前的工作实际是进一步整合传入的header信息,用ovs_flow_tun_info_init去初始化tun_info,然后调用vport中数据处理函数ovs_vport_receive进一步处理。

事实上,不论vxlan还是gre亦或lisp网络,最后的数据处理都会回到ovs_vport_receive,然后在ovs_vport_receive去调用ovs_dp_process_received_packet,回到datapath.c进行统一处理;在ovs_dp_process_received_packet进行流表查询,然后去查表执行对应的action。

在查询流表失败的时候,由ovs_dp_upcall发送upcall到用户空间(ovs-vswitchd),此后处理过程交给ovsd 处理。具体的流表匹配就不展开了。

以上数据监听到处理的流程解读实际上说明了OVS已经把VxLAN的tunnel信息和NSH信息整合起来了,流表和数据的对应关系如上图。

2.3 OVS作为classifier对数据进行的NSH封装

OVS作为classifier最重要的功能就是对数据的封装和发送。

int vxlan_xmit_skb(struct vxlan_sock *vs,
         struct rtable *rt, struct sk_buff *skb,
         __be32 src, __be32 dst, __u8 tos, __u8 ttl, __be16 df,
         __be16 src_port, __be16 dst_port, __be32 vni, __be32 nsp,
         __be32 nshc1, __be32 nshc2, __be32 nshc3, __be32 nshc4)
{
   bool isnsh = (dst_port == htons(NSH_DST_PORT));
   struct vxlanhdr *vxh;
   struct udphdr *uh;
   int min_headroom;
   int err;
 
   min_headroom = LL_RESERVED_SPACE(rt_dst(rt).dev) + rt_dst(rt).header_len
         + (isnsh ? NSH_HLEN : VXLAN_HLEN) + sizeof(struct iphdr)
         + (vlan_tx_tag_present(skb) ? VLAN_HLEN : 0);//最小长度
 
   /* Need space for new headers (invalidates iph ptr) */
   err = skb_cow_head(skb, min_headroom); //验证skb长度是否足够
   if (unlikely(err))
      return err;
 
   if (vlan_tx_tag_present(skb)) {  
      if (unlikely(!__vlan_put_tag(skb,
                  skb->vlan_proto,
                  vlan_tx_tag_get(skb))))
         return -ENOMEM;
 
      vlan_set_tci(skb, 0);
   }
 
   skb_reset_inner_headers(skb);
 
   if (isnsh) {
      struct nshhdr *nsh;
      uint8_t nsi = ntohl(nsp) & NSH_M_NSI;
      nsh = (struct nshhdr *) __skb_push(skb, sizeof(*nsh));//把nsh头push到skb中  nshheader封装
              memset(&nsh->b, 0, sizeof nsh->b);  //初始化nsh_b
      nsh->b.len = 6;
      nsh->b.mdtype = NSH_M_TYPE1;
      nsh->b.proto = NSH_P_ETHERNET;
      /* b2 should precede svc_idx, else svc_idx will be zero */
      nsh->b.b2 = nsp & htonl(NSH_M_NSP);
      nsh->b.svc_idx = nsi ? nsi : 0x01;
      nsh->c.c1 = nshc1;
      nsh->c.c2 = nshc2;
      nsh->c.c3 = nshc3;
      nsh->c.c4 = nshc4;
   }
 
   vxh = (struct vxlanhdr *) __skb_push(skb, sizeof(*vxh));   //vxlanheader封装
   vxh->vx_flags = htonl(VXLAN_FLAGS);
   vxh->vx_vni = vni;
 
   __skb_push(skb, sizeof(*uh));               //udp header封装
   skb_reset_transport_header(skb);
   uh = udp_hdr(skb);
 
   uh->dest = dst_port;    
   uh->source = src_port;
 
   uh->len = htons(skb->len);
   uh->check = 0;
 
   vxlan_set_owner(vs->sock->sk, skb);       //把skb 绑定的到某个vxlan的sk, 实际是为vxlan设置skb
 
   err = handle_offloads(skb);                
   if (err)
      return err;
 
        return iptunnel_xmit(vs->sock->sk, rt, skb, src, dst, IPPROTO_UDP,    //iptunnel封装
                             tos, ttl, df, false);
}

linux内核中vxlan_xmit_skb函数的功能是对skb进行vxlan封装,在OVS中对该函数进行了重写,增加了NSH封装功能。 该函数在vxlan的send操作被调用,即在classifier发数据前进行了封装。另外,该版本OVS添加了NSH对应的action,主要是一些attribute动作,用于添加NSH信息。

3. 总结

  • OVS在SFC-NSH中,可以是Classifier,SFF和SF对应的proxy.
  • 该版本OVS添加了NSH对应的action,主要是attribute,用于添加NSH,这部分功能主要用于classifier去打NSHheader。
  • NSH只是一个封装技术,便于SFC中的数据传输。