背景
二、三层转发是网络工程师经常接触到的一个问题,VLAN配置是二、三层转中一个很重要的概念,在配置VLAN的情况下,内核对报文是如何处理的呢?
概念
了解VLAN转发,首先必须知道VLAN相关概念;
VLAN作用
VLAN(Virtual Local Area Network)虚拟局域网,是一组逻辑上的设备和用户,这组设备和用户不受地理位置的限制,相互之间的通信好像在同一网段一样。
VLAN用于逻辑上划分广播域,VLAN内可以通信,VLAN间不可以通信,从而实现网络隔离,将广播报文限制在一个VLAN内,缩小广播域,减小广播风暴对整个通信网络的影响;此外,由于只允许相同VLAN内通信,对于重要的敏感数据降低了泄漏的风险,增加了安全性。
为了实现转发控制,在待转发的以太网帧中添加VLAN标签,设定端口对该标签和帧的处理方式;包括添加标签、去掉标签、转发帧、丢弃帧。
VLAN帧格式
对比看出,打上标签的帧多个4个字节的VLAN标记,又称tag;VLAN Tag具体格式如下:
TIPD:标签协议标识,2个字节,固定为0x8100,表明这是一个带有802.1Q 标签的以太网帧
PRI:表示帧优先级,3bit,取值0-7,用于提供有差别的转发服务
CFI:标准格式指示符,1bit,0 表示经典格式,1 表示非经典格式,如令牌环网,在以太网环境中,通常为0
VLAN ID :VLAN标识,表示一个帧所属VLAN,12bit,VLAN ID 范围为 0 --4095(2^12-1),但0、4095保留,仅限系统使用,因此VLAN ID的有效范围为1-4094。
802.1Q帧由交换机设备处理,而非用户主机;
1)当交换机设备接收到普通以太网帧时,会插入4字节的VLAN tag变成802.1Q帧,称为“打标签”
2)当交换机转发802.1Q帧时,若帧携带VLAN ID 与端口默认VLAN ID一致,则去除VLAN tag,称为“去标签”
端口接收时总是希望能“打标签”,发送时总希望去除“标签”
VLAN划分
VLAN划分即通过某种方法、约定或策略将特定端口、特定报文划分到特定VLAN的方式,目前VLAN划分有如下几种方式:
1)基于端口划分(最常用)
根据端口编号划分 VLAN
2)基于MAC地址划分
根据端口MAC地址划分VLAN
3)基于协议划分
根据数据帧的协议类型(或协议簇类型)、封装格式划分VLAN
4)基于策略划分
使用几个条件的组合来划分VLAN
此外还有基于IP子网的划分等,具体使用何种方式,依据使用场景而定。
VLAN链路
VLAN技术的出现,使得交换网络中出现了带VLAN标签的以太网帧和不带VLAN标签的以太网帧,因此也对以太网链路作了划分:
1)接入链路,通常用于连接用户主机与交换设备,接入链路上的以太网帧通常不带标签
2)干道链路,用于交换机间或交换机与路由器间互连,通过的帧一般为带Tag的帧,也允许不带Tag的帧通过。
转发原理
如前所述,VLAN技术出现以后,以太网中出现了带Tag的以太网帧和不带Tag的以太网帧,同时也规定了端口对帧和Tag的处理方式,在了解转发原理之前,需要知道几个概念。
缺省VLAN
每个端口都有一个默认VLAN,称为缺省VLAN,在思科交换机上称为Native VLAN,华为交换机上称为Port VLAN ID(PVID)。
端口模式
交换机端口分为三种模式:Access模式、Trunk模式、Hybrid模式,因此也对三种不同类型的端口,各模式下端口对tag、以太网帧的处理方式如何?下面分别讨论:
Access口
Access口又称接入口,只能属于一个VLAN;其缺省VLAN 与端口VLAN相同。
接收处理方法:接入端口只能接收不带VLAN Tag的以太网帧,对于带Tag的以太网帧直接丢弃。接收后依据PVID打上VLAN 标签在设备内部转发处理。
发送处理方法:若帧中的VLAN ID和端口的PVID 相同,则剥离Tag发送,否则不转发。
Trunk口
Access口又称为干道端口,可以同时属于多个VLAN。
接收处理方法:
1)对于未打标签的帧,依据PVID打上VLAN Tag。
2)对于已有Tag的帧,若端口允许通过,直接接收,否则丢弃。
发送处理方法:
1)若帧携带打tag与端口PVID相同,剥离tag发送。
2)否则,直接发送。
Hybrid口
混合端口,仅华为交换机有。混合端口既可以用于交换机间或交换机与路由器间(同Trunk口),也可用于用户主机与交换机间(同Access口)。
混合端口可以属于多个VLAN。
接收处理方法(同trunk):
1)若帧未携带Tag,依据PVID 打上VLAN标签
2)对于已携带VLAN tag帧,若端口允许进入,直接接收,否则丢弃。
发送处理方法:查看VID是否在端口去标签列表中
1)在,去标签转发
2)不在,直接转发
综上可见,在增加VLAN技术后,端口对报文的处理实质就是打标签、剥离标签、转发或丢弃的过程,接下来我们看看内核是如何实现的。
内核实现
对于VLAN内核中的处理,我们也从接收、发送两方面学习
接收
内核收包会经过一系列的过程,最终会通过__netif_receive_skb_core接口将报文发送到上层协议栈,这里直接从__netif_receive_skb_core处理分析。具体实现如下:
static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
struct packet_type **ppt_prev)
{
.....
/*设置网络层头部起始位置*/
skb_reset_network_header(skb);
if (!skb_transport_header_was_set(skb))
/*设置传输层部起始位置*/
skb_reset_transport_header(skb);
/* 设置以太网头部长度 */
skb_reset_mac_len(skb);
pt_prev = NULL;
another_round:
skb->skb_iif = skb->dev->ifindex;
__this_cpu_inc(softnet_data.processed);
if (static_branch_unlikely(&generic_xdp_needed_key)) {
int ret2;
migrate_disable();
ret2 = do_xdp_generic(rcu_dereference(skb->dev->xdp_prog), skb);
migrate_enable();
if (ret2 != XDP_PASS) {
ret = NET_RX_DROP;
goto out;
}
}
/* 如果是vlan协议报文802.1Q,提取VLAN信息,去tag */
if (eth_type_vlan(skb->protocol)) {
skb = skb_vlan_untag(skb);
if (unlikely(!skb))
goto out;
}
if (skb_skip_tc_classify(skb))
goto skip_classify;
if (pfmemalloc)
goto skip_taps;
/* 如果报文属于某类协议报文,上送对应协议处理 */
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
skip_taps:
#ifdef CONFIG_NET_INGRESS
if (static_branch_unlikely(&ingress_needed_key)) {
bool another = false;
nf_skip_egress(skb, true);
skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev,
&another);
if (another)
goto another_round;
if (!skb)
goto out;
nf_skip_egress(skb, false);
if (nf_ingress(skb, &pt_prev, &ret, orig_dev) < 0)
goto out;
}
#endif
skb_reset_redirect(skb);
skip_classify:
if (pfmemalloc && !skb_pfmemalloc_protocol(skb))
goto drop;
if (skb_vlan_tag_present(skb)) {
if (pt_prev) {
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = NULL;
}
/* vlan 报文接收处理 */
if (vlan_do_receive(&skb))
goto another_round;
else if (unlikely(!skb))
goto out;
}
.....
return ret;
}
驱动层接收上来的数据包已经设置好了skb的protocol字段(为8021Q或者8021AD)。依次调用skb_vlan_untag与vlan_do_receive函数进行处理。
带tag
skb_vlan_untag主要功能是从数据包中提取vlan信息,保存到skb结构中,然后从数据包中取出vlan相关字段,另外,获取数据包的真实协议类型更新到skb的protocol字段(三层协议类型),替换了之前的ETH_P_8021Q或者ETH_P_8021AD;具体实现如下:
struct sk_buff *skb_vlan_untag(struct sk_buff *skb)
{
struct vlan_hdr *vhdr;
u16 vlan_tci;
if (unlikely(skb_vlan_tag_present(skb))) {
/* vlan_tci is already set-up so leave this for another time */
return skb;
}
/* 共享拷贝skb */
skb = skb_share_check(skb, GFP_ATOMIC);
if (unlikely(!skb))
goto err_free;
/* We may access the two bytes after vlan_hdr in vlan_set_encap_proto(). */
if (unlikely(!pskb_may_pull(skb, VLAN_HLEN + sizeof(unsigned short))))
goto err_free;
/* 获取vlan信息 */
vhdr = (struct vlan_hdr *)skb->data;
vlan_tci = ntohs(vhdr->h_vlan_TCI);
/* 更新vlan信息到skb */
__vlan_hwaccel_put_tag(skb, skb->protocol, vlan_tci);
skb_pull_rcsum(skb, VLAN_HLEN);
/* 更新skb->protocol,以太网帧或802.3帧 */
vlan_set_encap_proto(skb, vhdr);
/* 通过拷贝覆盖,剥离vlan Tag, 重新设置mac头部指针 */
skb = skb_reorder_vlan_header(skb);
if (unlikely(!skb))
goto err_free;
skb_reset_network_header(skb);
if (!skb_transport_header_was_set(skb))
skb_reset_transport_header(skb);
skb_reset_mac_len(skb);
return skb;
err_free:
kfree_skb(skb);
return NULL;
}
提取vlan信息,剥离vlan tag后由vlan_do_receive处理
不带Tag
无tag跳过skb_vlan_untag处理,但所有从vlan下的物理接口接收上来的报文都要经过vlan_do_receive处理;具体流程如下:
1) 系统中必须要有与数据包中vlan id相同的vlan设备,否则结束处理。
2) 找到此vlan设备后将其赋值给skb的dev,此时就完成了接收设备从物理网卡到vlan设备的转换,skb中的vlan_tci也就没有用处了可以清理。
3)VLAN设备可能具有与其所在物理设备不同的MAC地址,在此情况下物理设备驱动程序会赋值PACKET_OTHERHOST到skb的pkt_type,需要进一步判断数据包目的MAC是否为vlan的MAC地址,如果是,修改pkt_type为PACKET_HOST,表示为发往本机的数据包;具体实现如下:
bool vlan_do_receive(struct sk_buff **skbp)
{
struct sk_buff *skb = *skbp;
__be16 vlan_proto = skb->vlan_proto;
u16 vlan_id = skb_vlan_tag_get_id(skb);
struct net_device *vlan_dev;
struct vlan_pcpu_stats *rx_stats;
/* 根据vlan协议,vlan id在系统上查找vlan设备 */
vlan_dev = vlan_find_dev(skb->dev, vlan_proto, vlan_id);
if (!vlan_dev)
return false;
/* 共享拷贝skb */
skb = *skbp = skb_share_check(skb, GFP_ATOMIC);
if (unlikely(!skb))
return false;
if (unlikely(!(vlan_dev->flags & IFF_UP))) {
kfree_skb(skb);
*skbp = NULL;
return false;
}
/* 更新skb->dev为vlan_dev, 将报文上送vlan */
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. */
/* 如果目的mac等于vlan dev的mac,也认为是到本机的 */
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) &&
!netif_is_macvlan_port(vlan_dev) &&
!netif_is_bridge_port(vlan_dev)) {
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_inner_tag(skb, skb->vlan_proto,
skb->vlan_tci, skb->mac_len);
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标识,同普通报文处理 */
__vlan_hwaccel_clear_tag(skb);
/* 获取并更新vlan_dev 统计信息 */
rx_stats = this_cpu_ptr(vlan_dev_priv(vlan_dev)->vlan_pcpu_stats);
u64_stats_update_begin(&rx_stats->syncp);
u64_stats_inc(&rx_stats->rx_packets);
u64_stats_add(&rx_stats->rx_bytes, skb->len);
if (skb->pkt_type == PACKET_MULTICAST)
u64_stats_inc(&rx_stats->rx_multicast);
u64_stats_update_end(&rx_stats->syncp);
return true;
}
发送
内核下通过网络设备发送报文,通常会调用设备的.ndo_start_xmit函数,该接口在设备初始化阶段注册或指定,对于vlan设备具体为:
1)vlan_dev_init初始化过程
2)网络设备操作处理函数指定:dev->netdev_ops = &vlan_netdev_ops;vlan_netdev_ops具体定义如下:
static const struct net_device_ops vlan_netdev_ops = {
......
/* 报文发送处理函数 */
.ndo_start_xmit = vlan_dev_hard_start_xmit,
......
};
由此可见,vlan设备的发送处理函数为:vlan_dev_hard_start_xmit。
打tag
vlan_dev_hard_start_xmit函数的具体实现如下:
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...
*/
/* 以太网协议类型不等于0x8100,说明还没有打上vlan tag */
if (vlan->flags & VLAN_FLAG_REORDER_HDR ||
veth->h_vlan_proto != vlan->vlan_proto) {
u16 vlan_tci;
/* 打tag */
vlan_tci = vlan->vlan_id;
vlan_tci |= vlan_dev_get_egress_qos_mask(dev, skb->priority);
__vlan_hwaccel_put_tag(skb, vlan->vlan_proto, vlan_tci);
}
/* 将skb->dev设置为vlan下的物理口 */
skb->dev = vlan->real_dev;
len = skb->len;
if (unlikely(netpoll_tx_running(dev)))
return vlan_netpoll_send_skb(vlan, skb);
/* 报文发送 */
ret = dev_queue_xmit(skb);
if (likely(ret == NET_XMIT_SUCCESS || ret == NET_XMIT_CN)) {
struct vlan_pcpu_stats *stats;
/* vlan接口发送信息更新 */
stats = this_cpu_ptr(vlan->vlan_pcpu_stats);
u64_stats_update_begin(&stats->syncp);
u64_stats_inc(&stats->tx_packets);
u64_stats_add(&stats->tx_bytes, len);
u64_stats_update_end(&stats->syncp);
} else {
this_cpu_inc(vlan->vlan_pcpu_stats->tx_dropped);
}
return ret;
}
不打(剥离)tag
对于VLAN报文,剥离tag在接收阶段已经处理,若无需打tag,直接通过VLAN下的物理口发送。
参考连接
VLAN技术
VLAN简介
VLAN技术详解