说起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” 字段.
-
static inline unsigned int vlan_proto_idx(__be16 proto)
-
{
-
switch (proto) {
-
case htons(ETH_P_8021Q):
-
return VLAN_PROTO_8021Q;
-
case htons(ETH_P_8021AD):
-
return VLAN_PROTO_8021AD;
-
default:
-
BUG();
-
return 0;
-
}
-
}
和
-
enum vlan_protos {
-
VLAN_PROTO_8021Q = 0,
-
VLAN_PROTO_8021AD,
-
VLAN_PROTO_NUM,
-
};
这里有什么用处呢? 回头看一下vconfig的实现原理,它是调用了ioctl利用内核来创建vlan接口-->ADD_VLAN_CMD:
-
/*
-
* VLAN IOCTL handler.
-
* o execute requested action or pass command to the device driver
-
* arg is really a struct vlan_ioctl_args __user *.
-
*/
-
static int vlan_ioctl_handler(struct net *net, void __user *arg)
-
{
-
int err;
-
struct vlan_ioctl_args args;
-
struct net_device *dev = NULL;
-
-
if (copy_from_user(&args, arg, sizeof(struct vlan_ioctl_args)))
-
return -EFAULT;
-
-
...
-
-
case ADD_VLAN_CMD:
-
err = -EPERM;
-
if (!ns_capable(net->user_ns, CAP_NET_ADMIN))
-
break;
-
#if 1
-
vlan_pt= (args.u.VID >> 16) & 0xffff ;
-
switch(vlan_pt){
-
case ETH_P_8021Q:
-
vlan_pt= ETH_P_8021Q;
-
break;
-
case ETH_P_8021AD:
-
vlan_pt = ETH_P_8021AD;
-
break;
-
case ETH_P_QINQ1:
-
vlan_pt = ETH_P_QINQ1;
-
break;
-
default:
-
vlan_pt = ETH_P_8021Q;
-
break;
-
-
}
-
-
printk("%s,vlan_proto is %x\n",__FUNCTION__,vlan_pt);
-
#endif
-
-
err = register_vlan_device(dev, args.u.VID);
-
break;
这里是我已经改过的代码(改动都在#if/endif里). args.u.VID为用户空间传递过来的值,这里当然vconfig也需要改动,把tpid也传递过来,即我们可以动态的设定tpid.
然后我们赋值给自定义一个变量vlan_pt. 由于register_vlan_device第二个参数为short 型只占两个字节.所以即使4字节都包含了想要的信息也枉然.
继续看register_vlan_device:
-
/* Attach a VLAN device to a mac address (ie Ethernet Card).
-
* Returns 0 if the device was created or a negative error code otherwise.
-
*/
-
static int register_vlan_device(struct net_device *real_dev, u16 vlan_id)
-
{
-
struct net_device *new_dev;
-
struct vlan_dev_priv *vlan;
-
struct net *net = dev_net(real_dev);
-
struct vlan_net *vn = net_generic(net, vlan_net_id);
-
char name[IFNAMSIZ];
-
int err;
-
-
if (vlan_id >= VLAN_VID_MASK)
-
return -ERANGE;
-
-
err = vlan_check_real_dev(real_dev, htons(vlan_pt), vlan_id);
-
if (err < 0)
-
return err;
-
...
-
vlan = vlan_dev_priv(new_dev);
-
vlan->vlan_proto = htons(vlan_pt);//htons(ETH_P_8021Q);
-
vlan->vlan_id = vlan_id;
-
vlan->real_dev = real_dev;
-
vlan->dent = NULL;
-
vlan->flags = VLAN_FLAG_REORDER_HDR;
-
-
new_dev->rtnl_link_ops = &vlan_link_ops;
-
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的值.
它起作用的时候在:
-
static netdev_tx_t vlan_dev_hard_start_xmit(struct sk_buff *skb,
-
struct net_device *dev)
-
{
-
struct vlan_dev_priv *vlan = vlan_dev_priv(dev);
-
struct vlan_ethhdr *veth = (struct vlan_ethhdr *)(skb->data);
-
unsigned int len;
-
int ret;
-
-
/* Handle non-VLAN frames if they are sent to us, for example by DHCP.
-
*
-
* NOTE: THIS ASSUMES DIX ETHERNET, SPECIFICALLY NOT SUPPORTING
-
* OTHER THINGS LIKE FDDI/TokenRing/802.3 SNAPs...
-
*/
-
if (veth->h_vlan_proto != vlan->vlan_proto ||
-
vlan->flags & VLAN_FLAG_REORDER_HDR) {
-
u16 vlan_tci;
-
vlan_tci = vlan->vlan_id;
-
vlan_tci |= vlan_dev_get_egress_qos_mask(dev, skb->priority);
-
skb = __vlan_hwaccel_put_tag(skb, vlan->vlan_proto, vlan_tci);
-
}
最后切入到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.
-
static inline unsigned int vlan_proto_idx(__be16 proto)
-
{
-
switch (proto) {
-
case htons(ETH_P_8021Q):
-
return VLAN_PROTO_8021Q;
-
case htons(ETH_P_8021AD):
-
return VLAN_PROTO_8021AD;
-
case htons(ETH_P_QINQ1):
-
return VLAN_PROTO_8021AD;
-
default:
-
BUG();
-
return 0;
-
}
-
}
为什么我们case的值仍然为VLAN_PROTO_8021AD呢,原因在于对应普通的交换机即基于端口的QinQ,它带的外层tpid只为一种,并且这样也保证了内核模块的一致性,如果要增加一个值也是可以的,但是需要编译整个内核,并升级内核.这样它就可以同时支持多个.但是矛盾之处在于vconfig创建的东西,对于这些东西并不敏感.并且即使tpid不一样,你也只能创建一个双tag接口.至少名字默认会冲突. 如果仅仅为了识别外层tag的vid信息,而不关注tpid,这样已经足够.不用惊动整个内核.
而它也可以保持不变.
如果要明确缺乏外层TPID则需要这两处对应都添加上我们需要的值.
-
enum vlan_protos {
-
VLAN_PROTO_8021Q = 0,
-
VLAN_PROTO_8021AD,
-
VLAN_PROTO_NUM,
-
};
说了发送,那么我们说说接收吧,默认现在3.16.0内核里已经支持了8021ad.
-
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
-
{
-
...
-
-
another_round:
-
skb->skb_iif = skb->dev->ifindex;
-
-
__this_cpu_inc(softnet_data.processed);
-
-
if (skb->protocol == cpu_to_be16(ETH_P_8021Q) ||
-
skb->protocol == cpu_to_be16(ETH_P_8021AD)) {
-
skb = vlan_untag(skb);
-
if (unlikely(!skb))
-
goto unlock;
-
}
-
-
-
...
-
if (vlan_tx_tag_present(skb)) {
-
if (pt_prev) {
-
ret = deliver_skb(skb, pt_prev, orig_dev);
-
pt_prev = NULL;
-
}
-
if (vlan_do_receive(&skb))
-
goto another_round;
-
else if (unlikely(!skb))
-
goto unlock;
-
}
如果判断是0x8100、0x88a8则标记untag,在后面进入vlan_do_receive处理完,回到开始继续处理(主要是找到vlan dev 接口).
但是我们想在不改变内核的情况下,如何仅仅在8021q的模块里支持呢,当然只能注册一个协议模块的形式了.
-
bool vlan_skb_recv(struct sk_buff **skbp)
-
{
-
struct sk_buff *skb = *skbp;
-
struct ethhdr *ethhdr;
-
__be16 vlan_proto ;//= skb->vlan_proto = htons(0x9100);
-
u16 vlan_tci;
-
u16 vlan_id ;// vlan_tx_tag_get_id(skb);
-
struct net_device *vlan_dev;
-
struct vlan_pcpu_stats *rx_stats;
-
struct vlan_hdr *vhdr;
-
-
//vlan untag
-
skb= skb_share_check(skb,GFP_ATOMIC);
-
vhdr=(struct vlan_hdr *)skb->data;
-
vlan_tci =ntohs(vhdr->h_vlan_TCI);
-
__vlan_hwaccel_put_tag(skb,skb->protocol,vlan_tci);
-
-
skb_pull_rcsum(skb,4);
-
vlan_set_encap_proto(skb,vhdr);
-
-
-
skb =vlan_reorder_header(skb);
-
skb_reset_network_header(skb);
-
skb_reset_transport_header(skb);
-
skb_reset_mac_len(skb);
-
-
// vlan do receive
-
vlan_id= vlan_tx_tag_get_id(skb);
-
-
vlan_proto=skb->vlan_proto;
-
printk("%s..enter ..vlan_proto.%x,vlan_id:%d.\n",__FUNCTION__,skb->vlan_proto,vlan_id);
-
hex_dump(skb->data,20);
-
-
ethhdr=(struct ethhdr *)skb_mac_header(skb);
-
printk("ethhdr type is %x\n",ethhdr->h_proto);
-
-
vlan_dev = vlan_find_dev(skb->dev, vlan_proto, vlan_id);
-
if (!vlan_dev)
-
{
-
printk("%s,not find interface !!!\n",__FUNCTION__);
-
return false;
-
}
-
#if 1
-
//printk("%s.....\n",__FUNCTION__);
-
skb = *skbp = skb_share_check(skb, GFP_ATOMIC);
-
if (unlikely(!skb))
-
return false;
-
#endif
-
skb->dev = vlan_dev;
-
-
if (unlikely(skb->pkt_type == PACKET_OTHERHOST)) {
-
/* Our lower layer thinks this is not local, let's make sure.
-
* This allows the VLAN to have a different MAC than the
-
* underlying device, and still route correctly. */
-
if (ether_addr_equal_64bits(eth_hdr(skb)->h_dest, vlan_dev->dev_addr))
-
skb->pkt_type = PACKET_HOST;
-
}
-
-
if (!(vlan_dev_priv(vlan_dev)->flags & VLAN_FLAG_REORDER_HDR)) {
-
unsigned int offset = skb->data - skb_mac_header(skb);
-
-
/*
-
* vlan_insert_tag expect skb->data pointing to mac header.
-
* So change skb->data before calling it and change back to
-
* original position later
-
*/
-
skb_push(skb, offset);
-
skb = *skbp = vlan_insert_tag(skb, skb->vlan_proto,
-
skb->vlan_tci);
-
if (!skb)
-
return false;
-
skb_pull(skb, offset + VLAN_HLEN);
-
skb_reset_mac_len(skb);
-
}
-
-
skb->priority = vlan_get_ingress_priority(vlan_dev, skb->vlan_tci);
-
skb->vlan_tci = 0;
-
-
rx_stats = this_cpu_ptr(vlan_dev_priv(vlan_dev)->vlan_pcpu_stats);
-
-
u64_stats_update_begin(&rx_stats->syncp);
-
rx_stats->rx_packets++;
-
rx_stats->rx_bytes += skb->len;
-
-
-
if (skb->pkt_type == PACKET_MULTICAST)
-
rx_stats->rx_multicast++;
-
u64_stats_update_end(&rx_stats->syncp);
-
pr_info(".....end...%x.,type :%d \n",skb->protocol,skb->pkt_type);
-
hex_dump(skb->data ,20);
-
return true;
-
}
-
-
-
-
-
-
-
static int vlan_qinq_recv(struct sk_buff *skb,struct net_device *dev,
-
struct packet_type *ptye,struct net_device *orig_dev)
-
{
-
vlan_skb_recv(&skb);
-
netif_receive_skb(skb);
-
// kfree(skb);
-
return 0;
-
-
}
-
-
static struct packet_type vlan_1ad_type = {
-
.type = cpu_to_be16(ETH_P_QINQ1),
-
.func = vlan_qinq_recv,
-
-
};
原理很简单,完全参考netif_recevice_skb里的实现.
这里顺便提提一下,通过vlan_netlink也是可以实现的. 参加vlan_netlink.c代码. 原理一样.
具体设置:
-
sudo modprobe 8021q
-
-
sudo ip link add link eth0 eth0.service1 type vlan proto 802.1ad id 1
这仅仅是我在实际应用中遇到的一个小小的问题,当然或许有更复杂的需求,不过经过讲解我想大家应该对QinQ有了更深入的认识和理解.