本文记录一下vxlan接口内核收发包处理。
VXLAN(Virtual Extensible LAN, 虚拟局域网扩展)是一种网络虚拟化技术,一种大二层隧道技术,将二层包封装在UDP中来构建虚拟的二层网络。
设备厂商特别是大厂vxlan的配置和应用场景要丰富和复杂的多,linux上相对简单,在一些SDN网络,如云计算和容器的一些虚拟化网络中经常用到,还有vxlan上相关的一些支持的特性,如arp proxy、l2miss、l3miss、router等还是比较有意思的。

#####先介绍一下几个重要的数据结构:

struct vxlan_net结构每network namespace(net)一个,保存本namespace中vxlan相关信息。用于vxlan的全局查找,存放在net->gen中。

struct vxlan_net {struct list_head  vxlan_list;    // vxlan设备信息,创建vxlan dev(vxlan_newlink)时挂载的vxlan_devstruct hlist_head sock_list[PORT_HASH_SIZE];  // vxlan socket信息,vxlan_open创建socket时挂载的vxlan_sockspinlock_t      sock_lock;
};

struct vxlan_dev,是vxlan设备的私有数据结构,保存所有的vxlan配置信息,vxlan的fdb表项,vxlan使用的udp sock信息。

/* Pseudo network device */
struct vxlan_dev {struct vxlan_dev_node hlist4; /* vni hash table for IPv4 socket */
#if IS_ENABLED(CONFIG_IPV6)struct vxlan_dev_node hlist6;    /* vni hash table for IPv6 socket */
#endifstruct list_head  next;       /* vxlan's per namespace list */struct vxlan_sock __rcu *vn4_sock; /* listening socket for IPv4 */
#if IS_ENABLED(CONFIG_IPV6)struct vxlan_sock __rcu *vn6_sock;   /* listening socket for IPv6 */
#endifstruct net_device *dev;struct net   *net;     /* netns for packet i/o */struct vxlan_rdst default_dst;    /* default destination */u32          flags;    /* VXLAN_F_* in vxlan.h */struct timer_list age_timer;spinlock_t      hash_lock;unsigned int      addrcnt;struct gro_cells  gro_cells;struct vxlan_config   cfg;                   // vxlan所有配置数据struct hlist_head fdb_head[FDB_HASH_SIZE];  // vxlan专门的fdb表项
};

linux的fdb表,是linux用的二层转发表,一般的fdb表表达了某个mac地址的报文从哪个接口送出。而linux为vxlan专门设计的fdb表则多了对vxlan以及其udp tunnel封装方式的表达。
如下图所示,man bridge命令可以看到bridge fdb add命令专门针对vxlan接口的配置项,解释的很清楚。

如下,我们配置了一个vxlan100,指定了默认的dstport 和 vni,然后又在vxlan上配置了两条fdb表,可以看到可以针对mac地址指定vxlan真正的tunnel封装方式(不同对端),只有在不存在fdb表项的时候才会用静态配置做封装,在SDN网络中非常实用。
除了静态配置的fdb表项,同bridge一样,vxlan也会做src mac学习生产fdb表。

#ip link add vxlan100 type vxlan  dstport 8899  vni 100
// mac为 52:54:00:f7:b4:22的主机的endpoint在172.16.20.12上
#bridge fdb add 52:54:00:f7:b4:22 dev vxlan100 dst 172.16.20.12
// mac为 52:54:00:f7:b4:33的主机的endpoint在172.16.20.13上,port和vni分别为9999和200
#bridge fdb add 52:54:00:f7:b4:33 dev vxlan100 dst 172.16.20.13 port 9999 vni 200

如果在bridge中通过 addif 添加vxlan口,配置fdb表的时候,会在bridge 和vxlan中同时生成fdb表,也就是说bridge中的报文查找bridge的fdb表确认了出接口时vxlan口,进入vxlan_xmit发送,再次查找vxlan的fdb表确认隧道封装。见 rtnl_fdb_add 函数。

struct vxlan_fdb {struct hlist_node hlist;   /* linked list of entries */struct rcu_head   rcu;unsigned long   updated;  /* jiffies */unsigned long    used;struct list_head  remotes;                     // 插入的 vxlan_rdst,表示一个对端(的用户)u8        eth_addr[ETH_ALEN];       // 表项的mac地址u16        state;    /* see ndm_state */u8         flags;    /* see ndm_flags */
};
// 表示一个vxlan对端(的用户)
struct vxlan_rdst {union vxlan_addr  remote_ip;__be16            remote_port;__be32          remote_vni;u32          remote_ifindex;struct list_head     list;struct rcu_head        rcu;struct dst_cache    dst_cache;
};

vxlan接口创建流程,同各类型虚拟接口类似,主要完成对net_device及其私有结构的 vxlan_dev的相关初始化。

static int vxlan_newlink(struct net *src_net, struct net_device *dev,struct nlattr *tb[], struct nlattr *data[])
{// vxlan_config中包含了linux中vxlan支持的所有配置,当然ip link add type vxlan的配置也包含struct vxlan_config conf;memset(&conf, 0, sizeof(conf));......// 根据配置创建vxlan虚拟接口设备return vxlan_dev_configure(src_net, dev, &conf);
}

static int vxlan_dev_configure(struct net *src_net, struct net_device *dev,struct vxlan_config *conf)
{struct vxlan_net *vn = net_generic(src_net, vxlan_net_id);struct vxlan_dev *vxlan = netdev_priv(dev), *tmp;struct vxlan_rdst *dst = &vxlan->default_dst;unsigned short needed_headroom = ETH_HLEN;int err;bool use_ipv6 = false;__be16 default_port = vxlan->cfg.dst_port;struct net_device *lowerdev = NULL;if (conf->flags & VXLAN_F_GPE) {/* For now, allow GPE only together with COLLECT_METADATA.* This can be relaxed later; in such case, the other side* of the PtP link will have to be provided.*/if ((conf->flags & ~VXLAN_F_ALLOWED_GPE) ||!(conf->flags & VXLAN_F_COLLECT_METADATA)) {pr_info("unsupported combination of extensions\n");return -EINVAL;}vxlan_raw_setup(dev);} else {// 挂载 netdev_ops,指定设备发送函数,open函数等设备处理函数vxlan_ether_setup(dev);}// 根据配置,对vxlan的配置和 default_dst做赋值vxlan->net = src_net;dst->remote_vni = conf->vni;memcpy(&dst->remote_ip, &conf->remote_ip, sizeof(conf->remote_ip));/* Unless IPv6 is explicitly requested, assume IPv4 */if (!dst->remote_ip.sa.sa_family)dst->remote_ip.sa.sa_family = AF_INET;if (dst->remote_ip.sa.sa_family == AF_INET6 ||vxlan->cfg.saddr.sa.sa_family == AF_INET6) {if (!IS_ENABLED(CONFIG_IPV6))return -EPFNOSUPPORT;use_ipv6 = true;vxlan->flags |= VXLAN_F_IPV6;}if (conf->label && !use_ipv6) {pr_info("label only supported in use with IPv6\n");return -EINVAL;}// 本地绑定接口的校验if (conf->remote_ifindex) {lowerdev = __dev_get_by_index(src_net, conf->remote_ifindex);dst->remote_ifindex = conf->remote_ifindex;if (!lowerdev) {pr_info("ifindex %d does not exist\n", dst->remote_ifindex);return -ENODEV;}。。。。。if (!conf->mtu)dev->mtu = lowerdev->mtu - (use_ipv6 ? VXLAN6_HEADROOM : VXLAN_HEADROOM);needed_headroom = lowerdev->hard_header_len;} else if (vxlan_addr_multicast(&dst->remote_ip)) {pr_info("multicast destination requires interface to be specified\n");return -EINVAL;}if (conf->mtu) {err = __vxlan_change_mtu(dev, lowerdev, dst, conf->mtu, false);if (err)return err;}if (use_ipv6 || conf->flags & VXLAN_F_COLLECT_METADATA)needed_headroom += VXLAN6_HEADROOM;elseneeded_headroom += VXLAN_HEADROOM;dev->needed_headroom = needed_headroom;memcpy(&vxlan->cfg, conf, sizeof(*conf));if (!vxlan->cfg.dst_port) {if (conf->flags & VXLAN_F_GPE)vxlan->cfg.dst_port = htons(4790); /* IANA VXLAN-GPE port */elsevxlan->cfg.dst_port = default_port;}vxlan->flags |= conf->flags;if (!vxlan->cfg.age_interval)vxlan->cfg.age_interval = FDB_AGE_DEFAULT;// vxlan重复性判断,只有dstport+vni+flag,所以即使不同的remote ip,也不能配置相同的dstport+vnilist_for_each_entry(tmp, &vn->vxlan_list, next) {if (tmp->cfg.vni == conf->vni &&(tmp->default_dst.remote_ip.sa.sa_family == AF_INET6 ||tmp->cfg.saddr.sa.sa_family == AF_INET6) == use_ipv6 &&tmp->cfg.dst_port == vxlan->cfg.dst_port &&(tmp->flags & VXLAN_F_RCV_FLAGS) ==(vxlan->flags & VXLAN_F_RCV_FLAGS)) {pr_info("duplicate VNI %u\n", be32_to_cpu(conf->vni));return -EEXIST;}}dev->ethtool_ops = &vxlan_ethtool_ops;/* create an fdb entry for a valid default destination */// 配置了有效的remote ip,会默认生成一跳全0mac的fdb表if (!vxlan_addr_any(&vxlan->default_dst.remote_ip)) {err = vxlan_fdb_create(vxlan, all_zeros_mac,&vxlan->default_dst.remote_ip,NUD_REACHABLE|NUD_PERMANENT,NLM_F_EXCL|NLM_F_CREATE,vxlan->cfg.dst_port,vxlan->default_dst.remote_vni,vxlan->default_dst.remote_ifindex,NTF_SELF);if (err)return err;}/* 注册设备,涉及net_device结构的一些初始化、将其插入到本 namespace的全局列表和hash表、以及产生广播消息通知其它组件本次设备注册事件。*/err = register_netdevice(dev);if (err) {vxlan_fdb_delete_default(vxlan);return err;}/* namespace的全局vxlan信息结构 vxlan_net中包含一个vxlan设备信息列表和一个vxlan socket信息累表,这里插入vxlan设备vxlan_dev结构,vxlan sock结构在后面vxlan open函数中插入*/list_add(&vxlan->next, &vn->vxlan_list);return 0;
}

vxlan open流程,主要创建vxlan udp socket,挂载udp上次协议(vxlan)收包处理函数等。


/* Start ageing timer and join group when device is brought up */
static int vxlan_open(struct net_device *dev)
{struct vxlan_dev *vxlan = netdev_priv(dev);int ret;ret = vxlan_sock_add(vxlan);if (ret < 0)return ret;if (vxlan_addr_multicast(&vxlan->default_dst.remote_ip)) {ret = vxlan_igmp_join(vxlan);if (ret == -EADDRINUSE)ret = 0;if (ret) {vxlan_sock_release(vxlan);return ret;}}if (vxlan->cfg.age_interval)mod_timer(&vxlan->age_timer, jiffies + FDB_AGE_INTERVAL);return ret;
}

创建vxlan udp socket相关流程,主要关注一些数据结构的赋值,特别是,为udp socket(struct sock)挂载了 encap_rcv=vxlan_rcv,sk_user_data = vs用于udp解析为vxlan报文后的vxlan收包处理。

static int __vxlan_sock_add(struct vxlan_dev *vxlan, bool ipv6)
{struct vxlan_net *vn = net_generic(vxlan->net, vxlan_net_id);struct vxlan_sock *vs = NULL;struct vxlan_dev_node *node;// 非非共享情况下,可能多个vxlan共享一个port,这里会查找一下这个port的socket是否已经创建if (!vxlan->cfg.no_share) {spin_lock(&vn->sock_lock);vs = vxlan_find_sock(vxlan->net, ipv6 ? AF_INET6 : AF_INET,vxlan->cfg.dst_port, vxlan->flags);if (vs && !atomic_add_unless(&vs->refcnt, 1, 0)) {spin_unlock(&vn->sock_lock);return -EBUSY;}spin_unlock(&vn->sock_lock);}if (!vs)// 新建vxlan socketvs = vxlan_socket_create(vxlan->net, ipv6,vxlan->cfg.dst_port, vxlan->flags);if (IS_ERR(vs))return PTR_ERR(vs);
#if IS_ENABLED(CONFIG_IPV6)if (ipv6) {rcu_assign_pointer(vxlan->vn6_sock, vs);node = &vxlan->hlist6;} else
#endif{rcu_assign_pointer(vxlan->vn4_sock, vs);node = &vxlan->hlist4;}/* vxlan->cfg.no_share配置为共享时,一个socket会被多个vxlan共享,这里将vxlan私有结构 vxlan_dev 挂到vxlan_sock->vni_list中 */vxlan_vs_add_dev(vs, vxlan, node);return 0;
}/* Create new listen socket if needed */
static struct vxlan_sock *vxlan_socket_create(struct net *net, bool ipv6,__be16 port, u32 flags)
{struct vxlan_net *vn = net_generic(net, vxlan_net_id);struct vxlan_sock *vs;struct socket *sock;unsigned int h;struct udp_tunnel_sock_cfg tunnel_cfg;vs = kzalloc(sizeof(*vs), GFP_KERNEL);if (!vs)return ERR_PTR(-ENOMEM);for (h = 0; h < VNI_HASH_SIZE; ++h)INIT_HLIST_HEAD(&vs->vni_list[h]);// 创建socket结构,只有portsock = vxlan_create_sock(net, ipv6, port, flags);if (IS_ERR(sock)) {pr_info("Cannot bind port %d, err=%ld\n", ntohs(port),PTR_ERR(sock));kfree(vs);return ERR_CAST(sock);}vs->sock = sock;atomic_set(&vs->refcnt, 1);vs->flags = (flags & VXLAN_F_RCV_FLAGS);spin_lock(&vn->sock_lock);hlist_add_head_rcu(&vs->hlist, vs_head(net, port));udp_tunnel_notify_add_rx_port(sock,(vs->flags & VXLAN_F_GPE) ?UDP_TUNNEL_TYPE_VXLAN_GPE :UDP_TUNNEL_TYPE_VXLAN);spin_unlock(&vn->sock_lock);/* Mark socket as an encapsulation socket. */// udp vxlan协议的收包处理函数 vxlan_rcv,关联的数据vsmemset(&tunnel_cfg, 0, sizeof(tunnel_cfg));tunnel_cfg.sk_user_data = vs;tunnel_cfg.encap_type = 1;tunnel_cfg.encap_rcv = vxlan_rcv;tunnel_cfg.encap_destroy = NULL;tunnel_cfg.gro_receive = vxlan_gro_receive;tunnel_cfg.gro_complete = vxlan_gro_complete;setup_udp_tunnel_sock(net, sock, &tunnel_cfg);return vs;
}

#####vxlan发送流程:
vxlan_xmit函数的主要逻辑(所有ipv6逻辑忽略),进来的包已经是二层eth包了:
1、如果vxlan设置了VXLAN_F_COLLECT_METADATA标记,如ip命令创建vxlan时带了external标记,则使用路由中设置的tunnel信息封装vxlan发送。这是一种基于流的轻量级的隧道配置方法。举个例子,下面的配置方法,会直接使用路由中的encap信息encap vxlan tunnel,可以看到这种动态vxlan封装方法和本文开始说的bridge命令配置fdb表的基于mac的封装方法有很大不同,它是基于IP的,记住这个功能:
ip link add vxlan1 type vxlan dstport 4789 external
ip route add 10.1.1.1 encap ip id 30001 dst 20.1.1.2 dev vxlan1
正常情况下skb->_skb_refdst 设置为rtable结构保存了路由信息,这种light weight tunnel,设置了metadata_dst,保存了tunnel的关键参数。

struct metadata_dst {struct dst_entry        dst;union {struct ip_tunnel_info    tun_info;} u;
};

2、arp proxy功能处理,如果vxlan设置了VXLAN_F_PROXY,且报文是arp request,会查询本地arp表项,代答arp reply,而如果没查找表项,又会涉及另一个功能点,L3MIS,如果vxlan的IFLA_VXLAN_L3MISS已设置,会通过Netlink消息RTM_GETNEIGH [L3MISS NOTIFICATION]通知Linux用户态,用户态进程可以监听这个消息,并下发内核arp表项,下次就能代答成功了;
这里让我回想起来bridge的arp代答流程,它要求arp表项中的mac地址在bridge中必须有fdb表项,即这个mac地址是可达的才会做arp reply,而vxlan则没有这个处理。

3、根据报文的mac查询fdb表,如果查到了,会检查是否做route shortcircuit处理,在fdb表项被标记为路由器(NTF_ROUTER),并且vxlan已设置功能IFLA_VXLAN_RSC(路由短路)的情况下,会查找报文的IP地址的ARP表项:
–如果找到,则使用arp表项的mac地址更新更新报文的dmac,使用报文的dmac更新smac;这样做实际上将自己当成路由器,源端到自己是一跳,自己到目的端是一跳。收到的报文的dmac地址是给自己的,所以自己发出去的smac需要改成报文的dmac。
–如果未找到,并且已设置功能IFLA_VXLAN_L3MISS,则Linux内核将Netlink通知RTM_GETNEIGH [L3MISS NOTIFICATION]发送到用户态。用户态进程可以监听此[L3MISS通知]并更新Linux内核ARP,下次报文再来就ok了;

4、如果根据报文的mac未查到fdb表,查询全0 mac的fdb表封装转发,很自然能想到这相当于三层的默认路由,很相似啊。还记得这个表项哪里添加的吗?创建vxlan的时候,如果配置了remote ip,则会创建这条mac全0的fdb表;

5、如果连mac全0的fdb表都没查到,如果vxlan配置了L2MIS特性,则会最l2MIS处理。则先将RTM_GETNEIGH [L2MISS通知]发送给用户,用户区进程可以侦听此[L2MISS通知]并更新Linux内核转发数据库。这是个很常用的特性,老版本的容器网络方案calico用到过;

6、无论如何,查找到fdb表,调用vxlan_xmit_one 封装并发送vxlan报文,否则丢弃报文。

/* Transmit local packets over Vxlan** Outer IP header inherits ECN and DF from inner header.* Outer UDP destination is the VXLAN assigned port.*           source port is based on hash of flow*/
static netdev_tx_t vxlan_xmit(struct sk_buff *skb, struct net_device *dev)
{struct vxlan_dev *vxlan = netdev_priv(dev);const struct ip_tunnel_info *info;struct ethhdr *eth;bool did_rsc = false;struct vxlan_rdst *rdst, *fdst = NULL;struct vxlan_fdb *f;info = skb_tunnel_info(skb);skb_reset_mac_header(skb);// vxlan轻量级隧道实现,基于流的。if (vxlan->flags & VXLAN_F_COLLECT_METADATA) {if (info && info->mode & IP_TUNNEL_INFO_TX)vxlan_xmit_one(skb, dev, NULL, false);elsekfree_skb(skb);return NETDEV_TX_OK;}// arp proxy功能处理,如果vxlan设置了VXLAN_F_PROXY,且报文是arp request,会查询本地arp表项,代答arp reply,// 而如果没查找表项,又会涉及另一个功能点,L3MIS,如果vxlan的IFLA_VXLAN_L3MISS已设置,// 会通过Netlink消息RTM_GETNEIGH [L3MISS NOTIFICATION]通知Linux用户态,用户态进程可以监听这个消息,// 并下发内核arp表项,下次就能代答成功了。if (vxlan->flags & VXLAN_F_PROXY) {eth = eth_hdr(skb);if (ntohs(eth->h_proto) == ETH_P_ARP)return arp_reduce(dev, skb);
#if IS_ENABLED(CONFIG_IPV6)
......
#endif}eth = eth_hdr(skb);// 根据mac找fdb表项f = vxlan_find_mac(vxlan, eth->h_dest);did_rsc = false;/* 这里功能点,叫route shortcircuit,如果fdb表项被标记为路由器(NTF_ROUTER),并且vxlan已设置功能IFLA_VXLAN_RSC(路由短路),则会检查报文的IP地址的ARP表项:--如果找到,则使用arp表项的mac地址更新更新报文的dmac,使用报文的dmac更新smac;这样做实际上将自己当成路由器,源端到自己是一跳,自己到目的端是一跳。--如果未找到,并且已设置功能IFLA_VXLAN_L3MISS,则Linux内核将Netlink通知RTM_GETNEIGH [L3MISS NOTIFICATION]发送到用户态。用户态进程可以监听此[L3MISS通知]并更新Linux内核ARP,下次报文再来就ok了*/ if (f && (f->flags & NTF_ROUTER) && (vxlan->flags & VXLAN_F_RSC) &&(ntohs(eth->h_proto) == ETH_P_IP ||ntohs(eth->h_proto) == ETH_P_IPV6)) {did_rsc = route_shortcircuit(dev, skb);if (did_rsc)f = vxlan_find_mac(vxlan, eth->h_dest);}if (f == NULL) {// 未找到包的目标MAC的fdb表,查询全0 mac的fdb表封装转发,很自然能想到这相当于三层的默认路由,// 还记得这个表项哪里添加的吗?创建vxlan的时候,如果配置了remote ip,则会创建这条fdb表。f = vxlan_find_mac(vxlan, all_zeros_mac);if (f == NULL) {/* 这是个很常用的特性,老版本的容器网络方案calico用到过,如果找不到转发的fdb表项,又配置了L2MIS特性,则先将RTM_GETNEIGH [L2MISS通知]发送给用户,用户区进程可以侦听此[L2MISS通知]并更新Linux内核转发数据库。*/if ((vxlan->flags & VXLAN_F_L2MISS) &&!is_multicast_ether_addr(eth->h_dest))vxlan_fdb_miss(vxlan, eth->h_dest);dev->stats.tx_dropped++;kfree_skb(skb);return NETDEV_TX_OK;}}// 找到了转发表,调用vxlan_xmit_one 函数做封装、发送。list_for_each_entry_rcu(rdst, &f->remotes, list) {struct sk_buff *skb1;if (!fdst) {fdst = rdst;continue;}skb1 = skb_clone(skb, GFP_ATOMIC);if (skb1)vxlan_xmit_one(skb1, dev, rdst, did_rsc);}if (fdst)vxlan_xmit_one(skb, dev, fdst, did_rsc);elsekfree_skb(skb);return NETDEV_TX_OK;
}

查找到fdb表了,有了封装的所有数据,vxlan_xmit_one根据vxlan_rdst 做vxlan头和UDP Tunnel的封装和报文发送。sport使用按照vxlan配置的范围或者系统默认range分配。封装外层头之前会查询remote ip的路由是否存在,不存在会丢包,在未配置local ip的情况下还会使用route 的sip作为外层sip。
封装完成后,走ip_local_out 本地发送流程,再入协议栈。


static void vxlan_xmit_one(struct sk_buff *skb, struct net_device *dev,struct vxlan_rdst *rdst, bool did_rsc)
{struct dst_cache *dst_cache;struct ip_tunnel_info *info;struct vxlan_dev *vxlan = netdev_priv(dev);struct sock *sk;struct rtable *rt = NULL;const struct iphdr *old_iph;union vxlan_addr *dst;union vxlan_addr remote_ip, local_ip;struct vxlan_metadata _md;struct vxlan_metadata *md = &_md;__be16 src_port = 0, dst_port;__be32 vni, label;__be16 df = 0;__u8 tos, ttl;int err;u32 flags = vxlan->flags;bool udp_sum = false;bool xnet = !net_eq(vxlan->net, dev_net(vxlan->dev));info = skb_tunnel_info(skb);rcu_read_lock();if (rdst) {dst_port = rdst->remote_port ? rdst->remote_port : vxlan->cfg.dst_port;vni = rdst->remote_vni;dst = &rdst->remote_ip;local_ip = vxlan->cfg.saddr;dst_cache = &rdst->dst_cache;} else {......}if (vxlan_addr_any(dst)) {if (did_rsc) {/* short-circuited back to local bridge */vxlan_encap_bypass(skb, vxlan, vxlan);goto out_unlock;}goto drop;}old_iph = ip_hdr(skb);ttl = vxlan->cfg.ttl;if (!ttl && vxlan_addr_multicast(dst))ttl = 1;tos = vxlan->cfg.tos;if (tos == 1)// 外部ip头将继承内部ip头的Tos,tos==1(一般Tos最低位为0)才会这么做。// 好奇专门验证了一下,不知道是有什么讲究还是黑科技。tos = ip_tunnel_get_dsfield(old_iph, skb);label = vxlan->cfg.label;// 选UDP的源端口,可以配置指定也可以用系统默认范围src_port = udp_flow_src_port(dev_net(dev), skb, vxlan->cfg.port_min,vxlan->cfg.port_max, true);if (info) {ttl = info->key.ttl;tos = info->key.tos;label = info->key.label;udp_sum = !!(info->key.tun_flags & TUNNEL_CSUM);if (info->options_len)md = ip_tunnel_info_opts(info);} else {md->gbp = skb->mark;}if (dst->sa.sa_family == AF_INET) {struct vxlan_sock *sock4 = rcu_dereference(vxlan->vn4_sock);if (!sock4)goto drop;sk = sock4->sock->sk;// 这里查一遍路由,以检查remote ip地址是否可达,顺便通过路由找到源地址,特别有我们通常配置vxlan的时候不指定local ip,就在这里做了赋值rt = vxlan_get_route(vxlan, skb,rdst ? rdst->remote_ifindex : 0, tos,dst->sin.sin_addr.s_addr,&local_ip.sin.sin_addr.s_addr,dst_cache, info);if (IS_ERR(rt)) {netdev_dbg(dev, "no route to %pI4\n",&dst->sin.sin_addr.s_addr);dev->stats.tx_carrier_errors++;goto tx_error;}if (rt->dst.dev == dev) {netdev_dbg(dev, "circular route to %pI4\n",&dst->sin.sin_addr.s_addr);dev->stats.collisions++;goto rt_tx_error;}/* Bypass encapsulation if the destination is local */if (!info && rt->rt_flags & RTCF_LOCAL &&// bypass,忽略         。。。}if (!info)udp_sum = !(flags & VXLAN_F_UDP_ZERO_CSUM_TX);else if (info->key.tun_flags & TUNNEL_DONT_FRAGMENT)df = htons(IP_DF);// ECN封装,流控的一个特性,工作上正好用过,未继承Tos的情况下也得继承CE标记tos = ip_tunnel_ecn_encap(tos, old_iph, skb);ttl = ttl ? : ip4_dst_hoplimit(&rt->dst);// 封装vxlan头err = vxlan_build_skb(skb, &rt->dst, sizeof(struct iphdr),vni, md, flags, udp_sum);if (err < 0)goto xmit_tx_error;// 封装UDP头、外部IP头,最后走ip_local_out,走本地三层发送流程udp_tunnel_xmit_skb(rt, sk, skb, local_ip.sin.sin_addr.s_addr,dst->sin.sin_addr.s_addr, tos, ttl, df,src_port, dst_port, xnet, !udp_sum);
#if IS_ENABLED(CONFIG_IPV6)// ipv6 支持,忽略......
#endif}
......
}

#####然后是接收流程:
vxlan报文,首先是UDP报文,如上面vxlan_open函数中看到的,创建vxlan UDP套接字的时候,为其挂载了encap_rcv==vxlan_rcv,所以在vxlan的UDP报文后,调用vxlan_rcv处理。
vxlan_rcv整个流程相对简单,根据socket接收的报文和sock,找到vxlan相关的信息,包括vxlan_dev、vxlan_sock,脱去vxlan头,内部一个完整的二层包送入协议栈,重新走一遍2、3、4层协议栈。


/* Callback from net/ipv4/udp.c to receive packets */
static int vxlan_rcv(struct sock *sk, struct sk_buff *skb)
{struct pcpu_sw_netstats *stats;struct vxlan_dev *vxlan;struct vxlan_sock *vs;struct vxlanhdr unparsed;struct vxlan_metadata _md;struct vxlan_metadata *md = &_md;__be16 protocol = htons(ETH_P_TEB);bool raw_proto = false;void *oiph;/* Need UDP and VXLAN header to be present */if (!pskb_may_pull(skb, VXLAN_HLEN))goto drop;// vxlan头,vni+flagunparsed = *vxlan_hdr(skb);/* VNI flag always required to be set */if (!(unparsed.vx_flags & VXLAN_HF_VNI)) {netdev_dbg(skb->dev, "invalid vxlan flags=%#x vni=%#x\n",ntohl(vxlan_hdr(skb)->vx_flags),ntohl(vxlan_hdr(skb)->vx_vni));/* Return non vxlan pkt */goto drop;}unparsed.vx_flags &= ~VXLAN_HF_VNI;unparsed.vx_vni &= ~VXLAN_VNI_MASK;// 如vxlan_open流程中提到的,sock中挂载了vxlan_rcv(encap_rcv)和vxlan_sock(sk_user_data)这里提取出来vs = rcu_dereference_sk_user_data(sk);if (!vs)goto drop;// 一个port(sock)可能根据vni关联多个vxlan_dev,这里查找到vxlan_devvxlan = vxlan_vs_find_vni(vs, vxlan_vni(vxlan_hdr(skb)->vx_vni));if (!vxlan)goto drop;/* For backwards compatibility, only allow reserved fields to be* used by VXLAN extensions if explicitly requested.*/if (vs->flags & VXLAN_F_GPE) {if (!vxlan_parse_gpe_hdr(&unparsed, &protocol, skb, vs->flags))goto drop;raw_proto = true;}// 去掉vxlan头,从eth报文中解析上层协议类型if (__iptunnel_pull_header(skb, VXLAN_HLEN, protocol, raw_proto,!net_eq(vxlan->net, dev_net(vxlan->dev))))goto drop;if (vxlan_collect_metadata(vs)) {__be32 vni = vxlan_vni(vxlan_hdr(skb)->vx_vni);struct metadata_dst *tun_dst;tun_dst = udp_tun_rx_dst(skb, vxlan_get_sk_family(vs), TUNNEL_KEY,key32_to_tunnel_id(vni), sizeof(*md));if (!tun_dst)goto drop;md = ip_tunnel_info_opts(&tun_dst->u.tun_info);skb_dst_set(skb, (struct dst_entry *)tun_dst);} else {memset(md, 0, sizeof(*md));}if (vs->flags & VXLAN_F_REMCSUM_RX)if (!vxlan_remcsum(&unparsed, skb, vs->flags))goto drop;if (vs->flags & VXLAN_F_GBP)vxlan_parse_gbp_hdr(&unparsed, skb, vs->flags, md);/* Note that GBP and GPE can never be active together. This is* ensured in vxlan_dev_configure.*/if (unparsed.vx_flags || unparsed.vx_vni) {/* If there are any unprocessed flags remaining treat* this as a malformed packet. This behavior diverges from* VXLAN RFC (RFC7348) which stipulates that bits in reserved* in reserved fields are to be ignored. The approach here* maintains compatibility with previous stack code, and also* is more robust and provides a little more security in* adding extensions to VXLAN.*/goto drop;}if (!raw_proto) {// vxlan配置了VXLAN_F_LEARN,则根据eth smac做fdb学习if (!vxlan_set_mac(vxlan, vs, skb))goto drop;} else {skb_reset_mac_header(skb);skb->dev = vxlan->dev;skb->pkt_type = PACKET_HOST;}oiph = skb_network_header(skb);skb_reset_network_header(skb);if (!vxlan_ecn_decapsulate(vs, oiph, skb)) {++vxlan->dev->stats.rx_frame_errors;++vxlan->dev->stats.rx_errors;goto drop;}stats = this_cpu_ptr(vxlan->dev->tstats);u64_stats_update_begin(&stats->syncp);stats->rx_packets++;stats->rx_bytes += skb->len;u64_stats_update_end(&stats->syncp);// 此函数将vxlan内部的eth报文,送入协议栈,eth收包结束gro_cells_receive(&vxlan->gro_cells, skb);return 0;drop:/* Consume bad packet */kfree_skb(skb);return 0;
}static inline int gro_cells_receive(struct gro_cells *gcells, struct sk_buff *skb)
{struct gro_cell *cell;struct net_device *dev = skb->dev;if (!gcells->cells || skb_cloned(skb) || !(dev->features & NETIF_F_GRO))// 非NAPI收包处理,虚拟口接收如果需要软中断触发处理一般都这么做。return netif_rx(skb);cell = this_cpu_ptr(gcells->cells);if (skb_queue_len(&cell->napi_skbs) > netdev_max_backlog) {atomic_long_inc(&dev->rx_dropped);kfree_skb(skb);return NET_RX_DROP;}__skb_queue_tail(&cell->napi_skbs, skb);if (skb_queue_len(&cell->napi_skbs) == 1)napi_schedule(&cell->napi);return NET_RX_SUCCESS;
}

而外补充一个fdb边学习

static bool vxlan_set_mac(struct vxlan_dev *vxlan,struct vxlan_sock *vs,struct sk_buff *skb)
{union vxlan_addr saddr;__be16 sport = udp_hdr(skb)->source;__be32 vni = vxlan_vni(vxlan_hdr(skb)->vx_vni)// 二层头更新为内部二层头skb_reset_mac_header(skb);skb->protocol = eth_type_trans(skb, vxlan->dev);skb_postpull_rcsum(skb, eth_hdr(skb), ETH_HLEN);/* Ignore packet loops (and multicast echo) */if (ether_addr_equal(eth_hdr(skb)->h_source, vxlan->dev->dev_addr))return false;/* vxlan_rcv进入这个函数的时候,network header还是out ip header,Get address from the outer IP header */if (vxlan_get_sk_family(vs) == AF_INET) {saddr.sin.sin_addr.s_addr = ip_hdr(skb)->saddr;saddr.sa.sa_family = AF_INET;
#if IS_ENABLED(CONFIG_IPV6)} else {saddr.sin6.sin6_addr = ipv6_hdr(skb)->saddr;saddr.sa.sa_family = AF_INET6;
#endif}// vxlan 默认带有 VXLAN_F_LEARN 标志,上面的流程提取了fdb需要的dst mac、tunnel remote ip、dstport、vni// vxlan_snoop 用这些信息学习一条fdb表if ((vxlan->flags & VXLAN_F_LEARN) &&vxlan_snoop(skb->dev, &saddr, sport, vni, eth_hdr(skb)->h_source))return false;return true;
}static bool vxlan_snoop(struct net_device *dev,union vxlan_addr *src_ip, __be16 src_port, __be32 vni, const u8 *src_mac)
{struct vxlan_dev *vxlan = netdev_priv(dev);struct vxlan_fdb *f;// 查找是否存在这条smac的fdb表f = vxlan_find_mac(vxlan, src_mac);if (likely(f)) {struct vxlan_rdst *rdst = first_remote_rcu(f);// 存在的情况下,一个mac只能属于一个对端,如果remote_ip不一样,更新表项// 理论上需要加上vni判断的,vni区分用户,不同用户mac可以重复if (likely(vxlan_addr_equal(&rdst->remote_ip, src_ip)))return false;/* Don't migrate static entries, drop packets */if (f->state & NUD_NOARP)return true;if (net_ratelimit())netdev_info(dev,"%pM migrated from %pIS to %pIS\n",src_mac, &rdst->remote_ip.sa, &src_ip->sa);rdst->remote_ip = *src_ip;rdst->remote_port = src_port;rdst->remote_vni = vni;f->updated = jiffies;vxlan_fdb_notify(vxlan, f, rdst, RTM_NEWNEIGH);} else {/* learned new entry */spin_lock(&vxlan->hash_lock);// 创建一条新fdb表项/* close off race between vxlan_flush and incoming packets */if (netif_running(dev))vxlan_fdb_create(vxlan, src_mac, src_ip,NUD_REACHABLE,NLM_F_EXCL|NLM_F_CREATE,vxlan->dst_port,vxlan->default_dst.remote_vni,0, NTF_SELF);spin_unlock(&vxlan->hash_lock);}return false;
}

Linux网络协议栈8--vxlan相关推荐

  1. linux 虚拟机大量udp请求失败_理解 Linux 网络栈:Linux 网络协议栈简单总结分析...

    1. Linux 网络路径 1.1 发送端 1.1.1 应用层 (1) Socket 应用层的各种网络应用程序基本上都是通过 Linux Socket 编程接口来和内核空间的网络协议栈通信的.Linu ...

  2. Linux网络协议栈:中断下半部处理

    <Linux中断处理:上半部和下半部> <Linux网络协议栈:中断下半部处理> 目录 数据包上送 网络中断下半部处理 总结 推荐阅读 在<Linux网络协议栈:网络包接 ...

  3. Linux网络协议栈:用eBPF写TCP拥塞控制算法

    其实不想用这个题目的,只因为TCP相关的东西比较吸引人的眼球,这篇文章的主题还是eBPF,而不是TCP. 用eBPF写TCP拥塞控制算法只是本文所讲内容的一个再平凡不过的例子. 先看两个问题,或者说是 ...

  4. Linux网络协议栈:关闭一个还有没发送数据完的TCP连接

    <监视和调整Linux网络协议栈:接收数据> <监控和调整Linux网络协议栈的图解指南:接收数据> <Linux网络 - 数据包的接收过程> <Linux网 ...

  5. Linux网络协议栈:一个TCP链接的耗时

    <一次系统调用开销到底有多大?strace.time.perf命令> 目录 一 正常TCP连接建立过程 二 TCP连接建立时的异常情况 1)客户端connect系统调用耗时失控 2)半/全 ...

  6. Linux网络协议栈:网络包接收过程

    目录 一 Linux网络收包总览 二 Linux启动 2.1 创建ksoftirqd内核线程 2.2 网络子系统初始化 2.3 协议栈注册 2.4 网卡驱动初始化 2.5 启动网卡 三 迎接数据的到来 ...

  7. Linux网络协议栈:网卡收包分析

    Table of Contents 网卡收包 一,框架 二,初始化 三,驱动收包 四,内核处理 参考文章 推荐阅读 网卡收包 内核网络模块如何初始化? 内核如何通过网卡驱动收发数据包? 驱动收到的数据 ...

  8. Linux网络协议栈:NAPI机制与处理流程分析(图解)

    Table of Contents NAPI机制 NAPI缺陷 使用 NAPI 先决条件 非NAPI帧的接收 netif_rx - 将网卡中收到的数据包放到系统中的接收队列中 enqueue_to_b ...

  9. 监视和调整Linux网络协议栈:发送数据

    目录 有关监视和调整Linux网络堆栈的一般建议 总览 详细外观 协议族注册 通过套接字发送网络数据 sock_sendmsg,__sock_sendmsg和__sock_sendmsg_nosec ...

  10. 监控和调整Linux网络协议栈的图解指南:接收数据

    Table of Contents 入门 最初设定 数据到达 网络数据处理开始 网络数据处理继续 协议栈和用户态套接字 结论 监视和调整Linux网络协议栈:接收数据(图解):https://rtoa ...

最新文章

  1. 业界对生成图片缩略图的做法归纳
  2. SQL 数据库的使用
  3. 求交错序列前N项和(15 分)
  4. LoadRunner性能测试技术培训
  5. oracle 事务实现原理,数据库事务的实现原理
  6. 图像处理VintaSoftImaging.NET SDK控件发布v7.0版本
  7. 常用连接linux工具
  8. 尔雅 科学通史(吴国盛) 个人笔记及课后习题 2018 第五章 欧洲科技文明的起源
  9. Oracle客户端使用
  10. qttabbar文件浏览器突然坏掉了!
  11. android enable ipv6,安卓开启ipv6网络支持小米手机(miui)IPv6无法使用的问题
  12. 电脑自动同步服务器时间bat,用Internet时间服务器来同步本机系统时间的批处理...
  13. 什么是世界观、人生观、价值观?
  14. Ubuntu桌面版以太网无法设置IP
  15. 毒舌电影 是怎么成长起来的?为什么这么快就被封了
  16. 锐龙4750u和4800u的区别
  17. c语言 北京时间转换utc时间_UTC时间转换成北京时间C语言函数代码
  18. Python简单处理excel数据(拆分合并单元格、根据表头合并sheet、添加列数、添加内容操作)
  19. 神器Overleaf!
  20. C#读Visio模型数据

热门文章

  1. 2020年CFA考试第三次延期更改时间公布!
  2. 无法更改成家庭计算机,怎么都无法设置成功WIN7家庭中两个电脑局域网共享?WIN7家庭 爱问知识人...
  3. 基于半交互式的裂缝检测方法
  4. 群晖无密码共享 Windows访问
  5. 面试嵌入式工程师过程中的常见问题和回答
  6. 基于.NET的体育用品网站
  7. CBAM:融合通道和空间注意力的注意力模块
  8. 室外排水设计规范_房屋建筑工程现行规范标准目录汇编(2020版)—给排水—图集篇...
  9. 别用SE16,SE16N或SQVI了,试试增强版SE16H!
  10. simulink中Bus creator与Demux模块有什么区别啊?