Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1209619
  • 博文数量: 56
  • 博客积分: 400
  • 博客等级: 一等列兵
  • 技术积分: 2800
  • 用 户 组: 普通用户
  • 注册时间: 2010-03-30 13:08
个人简介

一个人的差异在于业余时间

文章分类

全部博文(56)

文章存档

2023年(1)

2019年(1)

2018年(1)

2017年(1)

2016年(2)

2015年(20)

2014年(10)

2013年(7)

2012年(12)

2011年(1)

分类: LINUX

2015-11-23 15:55:52

      说起QinQ,在路由器,交换机打过交道的都不陌生,当然如果用交换芯片当然没有问题,问题就在于有时候需要我们软处理一下. 当然前提需要对vlan非常熟悉.
之前我们讲过linux对vlan的处理以及如何软实现,参见《Linux实现的IEEE 802.1Q VLAN 》.
     下面我们先认识几个概念.  8021.ad :
     IEEE 802.1ad(QinQ)标准描述了以太网实现 S+C 的方式。802.1ad 描述了两种接口:Port-based service interface、C-tagged service interface,基于端口的模式是将某一端口统一打上一个外层 Q(普通 QinQ),基于客户 Tag(标签)的模式是根据不同的用户 Tag(标签)打上不同的外层 Q(灵活QinQ)。
在IEEE 802.1ad中规定外层TPID的 EType字段的定义为0x88a8。
     QinQ技术〔也称Stacked VLAN 或Double VLAN〕。标准出自IEEE 802.1ad,其实现将用户私网VLAN Tag封装在公网VLAN Tag中,使报文带着两层VLAN Tag穿越运营商的骨干网络(公网). 
产生背景
1、解决日益紧缺的公网VLAN ID资源问题
4096个VLAN不能满足大规模网络的需求,运营商需要根据VLAN ID对接入用户进行区分。
2、二层VPN技术能够透传用户的VLAN信息
二层VPN技术能够透传用户的VLAN信息及以太网配置信息
QinQ技术能够解决日益紧缺的VLAN ID资源问题为小型城域网或企业网提供一种较为简单的二层VPN解决方案
 报文格式


VLAN Tag包含四个字段,分别是TPID(Tag Protocol Identifier,标签协议标识符)、PCP(Priority Code Point)、CFI(Canonical Format Indicator,标准格式指示位)和VLAN ID。
TPID用来判断本数据帧是否带有VLAN Tag,长度为16bit,缺省取值为0x8100。
Priority表示报文的优先级,长度为3bit。
CFI字段标识MAC地址在不同的传输介质中是否以标准格式进行封装,长度为1bit,取值为0表示MAC地址以标准格式进行封装,为1表示以非标准格式封装,缺省取值为0。
VLAN ID标识该报文所属VLAN的编号,长度为12bit,取值范围为0~4095。由于0和4095为协议保留取值,所以VLAN ID的取值范围为1~4094。
       在802.1Q中规定TPID(Tag Protocol Identifier)的EType的值为0x8100。在QinQ封装中,各个设备厂商的内层TPID的EType的值为0x8100,但是对于外层TPID的EType,各个厂商所使用的值不相同. 通常有0x88a8 , 0x9100,0x9200,0x9300 等.
主要特点
相对基于MPLS的二层VPN,QinQ具有如下特点:
1.为用户提供了一种更为简单的二层VPN隧道;
2.不需要信令协议的支持,可以通过纯静态配置实现;
3.由于QinQ的实现是基于802.1Q协议中的Trunk端口概念,要求隧道上的设备都必须支持802.1Q协议。
4.QinQ主要可以解决如下几个问题:1.缓解日益紧缺的公网VLAN ID资源问题;2.用户可以规划自己的私网VLAN ID,不会导致和公网VLAN ID冲突;3.为小型城域网或企业网提供一种较为简单的二层VPN解决方案.
1. 没有协议交互过程,不需要任何配置;
2. 与业务不关联,对DSLAM无影响;
3. 扩展了4k VLAN;
4. 二层VLAN统一规划,同时要求运营商二层网络必须支持二层VLAN tag,对设备要求比较高。
5. 报文有效载荷降低,同时造成可能分片、重组;
6. 协议扩展性不强,不支持用户其他控制属性。
QinQ功能应用场景:
应用场合:Internet 业务、VOD/VoIP、大客户接入及VPN、FMC全业务
发展方向
灵活QinQ QinQ实现方式 QinQ实现方式一种是基于端口的QinQ,一种是基于流分类的灵活QinQ。 基于端口的QinQ的实现机理如下: 当该设备端口接收到报文,无论报文是否带有VLAN Tag,交换机都会为该报文打上本端口缺省VLAN的VLAN Tag。这样,如果接收到的是已经带有VLAN Tag的报文,该报文就成为双Tag的报文;如果接收到的是untagged的报文,该报文就成为带有端口缺省VLAN Tag的报文。由于基于端口的QinQ比较容易实现,所以业界主流厂家的三层交换机都支持。 基于端口的QinQ的缺点是外层Vlan Tag封装方式死板,不能根据业务种类选择外层Vlan Tag封装的方式,从而很难有效支持多业务的灵活运营。 基于流分类的灵活QinQ实现机理如下: 基于流的QinQ特性(Selective QinQ),可灵活根据流分类的结果选择是否打外层VLAN tag、打上何种外层VLAN tag:如根据用户Vlan tag、MAC地址、IP协议、源地址、目的地址、优先级、或应用程序的端口号等信息实施灵活QinQ特性。借助上述流分类方法,实际实现了根据不同用户、不同业务、不同优先级等对报文进行外层VLAN tag封装,对多种业务实施不同承载的方案.

   对于常用的交换机的配置: access  trunk 、hybrid 模式,默认都已经能够区分的很清楚和具体的应用场景.  
   这里以linux kernel 3.16.0为基础,说说QinQ的实现. 
   对于内核不断的变化,以及变化之大,这么不再吐槽了.就先从如何配置说起吧.
任何一个linux发行版. kernel  3.16.0  .
 安装了vconfig(例如ubuntu的:apt-get install vlan)
安装了内核头文件,方便编译内核模块。apt-get  install  linux-headers-`uname -r· (ubuntu版本)或内核源码一份
通常我们用vconfig来创建一个vlan接口. 那么如果我们在一个vlan接口上在创建一个vlan接口,那么这个接口最终发送出去的报文就会带双层tag
#vconfig add eth0 100  
#vconfig add eth0.100 100 
那么对于eth0.100接口它识别tpid 0x8100 ,vid 100的报文(收发双向);对于eth0.100.100 ,由于它发送和能够接收双层tag的报文,但是它的双层tag的tpid都是0x8100.
至于双层tag我们上面已经提到说,它应该叫QinQ.  通常外层tpid不会是0x8100  .
我们看看内核代码对QinQ的支持:在内核代码8021q目录搜“VLAN_PROTO_8021AD” 字段.

点击(此处)折叠或打开

  1. static inline unsigned int vlan_proto_idx(__be16 proto)
  2. {
  3.     switch (proto) {
  4.     case htons(ETH_P_8021Q):
  5.         return VLAN_PROTO_8021Q;
  6.     case htons(ETH_P_8021AD):
  7.         return VLAN_PROTO_8021AD;
  8.     default:
  9.         BUG();
  10.         return 0;
  11.     }
  12. }

点击(此处)折叠或打开

  1. enum vlan_protos {
  2.     VLAN_PROTO_8021Q    = 0,
  3.     VLAN_PROTO_8021AD,
  4.     VLAN_PROTO_NUM,
  5. };
这里有什么用处呢? 回头看一下vconfig的实现原理,它是调用了ioctl利用内核来创建vlan接口-->ADD_VLAN_CMD:

点击(此处)折叠或打开

  1. /*
  2.  *    VLAN IOCTL handler.
  3.  *    o execute requested action or pass command to the device driver
  4.  * arg is really a struct vlan_ioctl_args __user *.
  5.  */
  6. static int vlan_ioctl_handler(struct net *net, void __user *arg)
  7. {
  8.     int err;
  9.     struct vlan_ioctl_args args;
  10.     struct net_device *dev = NULL;

  11.     if (copy_from_user(&args, arg, sizeof(struct vlan_ioctl_args)))
  12.         return -EFAULT;

  13. ...

  14.     case ADD_VLAN_CMD:
  15.         err = -EPERM;
  16.         if (!ns_capable(net->user_ns, CAP_NET_ADMIN))
  17.             break;
  18.         #if 1
  19.         vlan_pt= (args.u.VID >> 16) & 0xffff ;
  20.         switch(vlan_pt){
  21.         case ETH_P_8021Q:
  22.             vlan_pt= ETH_P_8021Q;
  23.             break;
  24.         case ETH_P_8021AD:
  25.             vlan_pt = ETH_P_8021AD;
  26.             break;
  27.         case ETH_P_QINQ1:
  28.             vlan_pt = ETH_P_QINQ1;
  29.             break;
  30.         default:
  31.              vlan_pt = ETH_P_8021Q;
  32.             break;
  33.         
  34.         }
  35.     
  36.         printk("%s,vlan_proto is %x\n",__FUNCTION__,vlan_pt);
  37.         #endif

  38.         err = register_vlan_device(dev, args.u.VID);
  39.         break;
这里是我已经改过的代码(改动都在#if/endif里). args.u.VID为用户空间传递过来的值,这里当然vconfig也需要改动,把tpid也传递过来,即我们可以动态的设定tpid.
然后我们赋值给自定义一个变量vlan_pt. 由于register_vlan_device第二个参数为short 型只占两个字节.所以即使4字节都包含了想要的信息也枉然.
继续看register_vlan_device:

点击(此处)折叠或打开

  1. /* Attach a VLAN device to a mac address (ie Ethernet Card).
  2.  * Returns 0 if the device was created or a negative error code otherwise.
  3.  */
  4. static int register_vlan_device(struct net_device *real_dev, u16 vlan_id)
  5. {
  6.     struct net_device *new_dev;
  7.     struct vlan_dev_priv *vlan;
  8.     struct net *net = dev_net(real_dev);
  9.     struct vlan_net *vn = net_generic(net, vlan_net_id);
  10.     char name[IFNAMSIZ];
  11.     int err;

  12.     if (vlan_id >= VLAN_VID_MASK)
  13.         return -ERANGE;

  14.     err = vlan_check_real_dev(real_dev, htons(vlan_pt), vlan_id);
  15.     if (err < 0)
  16.         return err;
  17. ...
  18.     vlan = vlan_dev_priv(new_dev);
  19.     vlan->vlan_proto = htons(vlan_pt);//htons(ETH_P_8021Q);
  20.     vlan->vlan_id = vlan_id;
  21.     vlan->real_dev = real_dev;
  22.     vlan->dent = NULL;
  23.     vlan->flags = VLAN_FLAG_REORDER_HDR;

  24.     new_dev->rtnl_link_ops = &vlan_link_ops;
  25.     err = register_vlan_dev(new_dev);
vlan_check_real_dev检查read_dev是否包含了vlan_info信息. ,一开始注册默认应该为null.
vlan->vlan_proto = htons(vlan_pt);//htons(ETH_P_8021Q);  这个很关键,它涉及到在发送时tpid的值. 
它起作用的时候在:

点击(此处)折叠或打开

  1. static netdev_tx_t vlan_dev_hard_start_xmit(struct sk_buff *skb,
  2.                      struct net_device *dev)
  3. {
  4.     struct vlan_dev_priv *vlan = vlan_dev_priv(dev);
  5.     struct vlan_ethhdr *veth = (struct vlan_ethhdr *)(skb->data);
  6.     unsigned int len;
  7.     int ret;

  8.     /* Handle non-VLAN frames if they are sent to us, for example by DHCP.
  9.      *
  10.      * NOTE: THIS ASSUMES DIX ETHERNET, SPECIFICALLY NOT SUPPORTING
  11.      * OTHER THINGS LIKE FDDI/TokenRing/802.3 SNAPs...
  12.      */
  13.     if (veth->h_vlan_proto != vlan->vlan_proto ||
  14.      vlan->flags & VLAN_FLAG_REORDER_HDR) {
  15.         u16 vlan_tci;
  16.         vlan_tci = vlan->vlan_id;
  17.         vlan_tci |= vlan_dev_get_egress_qos_mask(dev, skb->priority);
  18.         skb = __vlan_hwaccel_put_tag(skb, vlan->vlan_proto, vlan_tci);
  19.     }
最后切入到register_vlan_dev函数:
里面有两个地方需要注意下vlan_vid_add和vlan_group_prealloc_vid
vlan_vid_add定义在vlan_core.c中,在我们编译8021q.ko模块的时候并不会编译vlan_core.c,所以我们的8021q模块还是沿用的原内核的函数代码实现.如果你执意添加然后至少在接口up/down的时候触发vlan通知链就会挂掉.原因引用了非法指针.即VLAN_PROTO_NUM
vlan_group_prealloc_vid中第一次引用了vlan_proto 通过vlan_proto_idx查询索引,如果这个函数里没有对应的case则报错.所以这里必须添加上我们想要支持的QinQ的TPID.

点击(此处)折叠或打开

  1. static inline unsigned int vlan_proto_idx(__be16 proto)
  2. {
  3.     switch (proto) {
  4.     case htons(ETH_P_8021Q):
  5.         return VLAN_PROTO_8021Q;
  6.     case htons(ETH_P_8021AD):
  7.         return VLAN_PROTO_8021AD;
  8.     case htons(ETH_P_QINQ1):
  9.         return VLAN_PROTO_8021AD;
  10.     default:
  11.         BUG();
  12.         return 0;
  13.     }
  14. }
为什么我们case的值仍然为VLAN_PROTO_8021AD呢,原因在于对应普通的交换机即基于端口的QinQ,它带的外层tpid只为一种,并且这样也保证了内核模块的一致性,如果要增加一个值也是可以的,但是需要编译整个内核,并升级内核.这样它就可以同时支持多个.但是矛盾之处在于vconfig创建的东西,对于这些东西并不敏感.并且即使tpid不一样,你也只能创建一个双tag接口.至少名字默认会冲突.  如果仅仅为了识别外层tag的vid信息,而不关注tpid,这样已经足够.不用惊动整个内核. 
而它也可以保持不变. 如果要明确缺乏外层TPID则需要这两处对应都添加上我们需要的值.

点击(此处)折叠或打开

  1. enum vlan_protos {
  2.     VLAN_PROTO_8021Q    = 0,
  3.     VLAN_PROTO_8021AD,
  4.     VLAN_PROTO_NUM,
  5. };
说了发送,那么我们说说接收吧,默认现在3.16.0内核里已经支持了8021ad.

点击(此处)折叠或打开

  1. static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
  2. {
  3. ...

  4. another_round:
  5.     skb->skb_iif = skb->dev->ifindex;

  6.     __this_cpu_inc(softnet_data.processed);

  7.     if (skb->protocol == cpu_to_be16(ETH_P_8021Q) ||
  8.      skb->protocol == cpu_to_be16(ETH_P_8021AD)) {
  9.         skb = vlan_untag(skb);
  10.         if (unlikely(!skb))
  11.             goto unlock;
  12.     }


  13. ...
  14.     if (vlan_tx_tag_present(skb)) {
  15.         if (pt_prev) {
  16.             ret = deliver_skb(skb, pt_prev, orig_dev);
  17.             pt_prev = NULL;
  18.         }
  19.         if (vlan_do_receive(&skb))
  20.             goto another_round;
  21.         else if (unlikely(!skb))
  22.             goto unlock;
  23.     }
如果判断是0x8100、0x88a8则标记untag,在后面进入vlan_do_receive处理完,回到开始继续处理(主要是找到vlan dev 接口).
但是我们想在不改变内核的情况下,如何仅仅在8021q的模块里支持呢,当然只能注册一个协议模块的形式了.

点击(此处)折叠或打开

  1. bool vlan_skb_recv(struct sk_buff **skbp)
  2. {
  3.         struct sk_buff *skb = *skbp;
  4.     struct ethhdr *ethhdr;
  5.         __be16 vlan_proto ;//= skb->vlan_proto = htons(0x9100);
  6.     u16 vlan_tci;
  7.         u16 vlan_id ;// vlan_tx_tag_get_id(skb);
  8.         struct net_device *vlan_dev;
  9.         struct vlan_pcpu_stats *rx_stats;
  10.     struct vlan_hdr *vhdr;

  11.     //vlan untag
  12.     skb= skb_share_check(skb,GFP_ATOMIC);
  13.     vhdr=(struct vlan_hdr *)skb->data;
  14.     vlan_tci =ntohs(vhdr->h_vlan_TCI);
  15.     __vlan_hwaccel_put_tag(skb,skb->protocol,vlan_tci);

  16.     skb_pull_rcsum(skb,4);
  17.     vlan_set_encap_proto(skb,vhdr);
  18.     

  19.     skb =vlan_reorder_header(skb);
  20.     skb_reset_network_header(skb);
  21.     skb_reset_transport_header(skb);
  22.     skb_reset_mac_len(skb);    

  23.     // vlan do receive
  24.     vlan_id= vlan_tx_tag_get_id(skb);

  25.     vlan_proto=skb->vlan_proto;
  26.     printk("%s..enter ..vlan_proto.%x,vlan_id:%d.\n",__FUNCTION__,skb->vlan_proto,vlan_id);
  27.     hex_dump(skb->data,20);
  28.     
  29.     ethhdr=(struct ethhdr *)skb_mac_header(skb);
  30.     printk("ethhdr type is %x\n",ethhdr->h_proto);

  31.         vlan_dev = vlan_find_dev(skb->dev, vlan_proto, vlan_id);
  32.         if (!vlan_dev)
  33.         {
  34.                 printk("%s,not find interface !!!\n",__FUNCTION__);
  35.                 return false;
  36.         }
  37.     #if 1
  38.         //printk("%s.....\n",__FUNCTION__);
  39.         skb = *skbp = skb_share_check(skb, GFP_ATOMIC);
  40.         if (unlikely(!skb))
  41.                 return false;
  42.     #endif
  43.         skb->dev = vlan_dev;

  44.         if (unlikely(skb->pkt_type == PACKET_OTHERHOST)) {
  45.                 /* Our lower layer thinks this is not local, let's make sure.
  46.                  * This allows the VLAN to have a different MAC than the
  47.                  * underlying device, and still route correctly. */
  48.                 if (ether_addr_equal_64bits(eth_hdr(skb)->h_dest, vlan_dev->dev_addr))
  49.                         skb->pkt_type = PACKET_HOST;
  50.         }

  51.         if (!(vlan_dev_priv(vlan_dev)->flags & VLAN_FLAG_REORDER_HDR)) {
  52.                 unsigned int offset = skb->data - skb_mac_header(skb);

  53.                 /*
  54.                  * vlan_insert_tag expect skb->data pointing to mac header.
  55.                  * So change skb->data before calling it and change back to
  56.                  * original position later
  57.                  */
  58.                 skb_push(skb, offset);
  59.                 skb = *skbp = vlan_insert_tag(skb, skb->vlan_proto,
  60.                                               skb->vlan_tci);
  61.                 if (!skb)
  62.                         return false;
  63.                 skb_pull(skb, offset + VLAN_HLEN);
  64.                 skb_reset_mac_len(skb);
  65.         }

  66.         skb->priority = vlan_get_ingress_priority(vlan_dev, skb->vlan_tci);
  67.         skb->vlan_tci = 0;

  68.         rx_stats = this_cpu_ptr(vlan_dev_priv(vlan_dev)->vlan_pcpu_stats);

  69.         u64_stats_update_begin(&rx_stats->syncp);
  70.         rx_stats->rx_packets++;
  71.         rx_stats->rx_bytes += skb->len;


  72.         if (skb->pkt_type == PACKET_MULTICAST)
  73.                 rx_stats->rx_multicast++;
  74.         u64_stats_update_end(&rx_stats->syncp);
  75.     pr_info(".....end...%x.,type :%d \n",skb->protocol,skb->pkt_type);
  76.     hex_dump(skb->data ,20);
  77.         return true;
  78. }






  79. static int vlan_qinq_recv(struct sk_buff *skb,struct net_device *dev,
  80.         struct packet_type *ptye,struct net_device *orig_dev)
  81. {
  82.     vlan_skb_recv(&skb);
  83.     netif_receive_skb(skb);
  84. //    kfree(skb);
  85.     return 0;

  86. }

  87. static struct packet_type vlan_1ad_type = {
  88.     .type = cpu_to_be16(ETH_P_QINQ1),
  89.     .func = vlan_qinq_recv,

  90. };
原理很简单,完全参考netif_recevice_skb里的实现. 
       这里顺便提提一下,通过vlan_netlink也是可以实现的. 参加vlan_netlink.c代码. 原理一样.
        具体设置:

点击(此处)折叠或打开

  1. sudo modprobe 8021q

  2. sudo ip link add link eth0 eth0.service1 type vlan proto 802.1ad id 1
        这仅仅是我在实际应用中遇到的一个小小的问题,当然或许有更复杂的需求,不过经过讲解我想大家应该对QinQ有了更深入的认识和理解.




















阅读(8274) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~