GRE隧道封装协议及内核处理解析

Generic Routing Encapsulation (GRE)通用路由封装协议,基于IP网络层协议封装以太网报文,可用于在IPSec VPN网络间传输多播路由信息报文,或者在PPTP协议中,承载PPP数据报文。其在数据帧中的位置如下:

    |-------------------|----------------|----------------------|------------------|
    |  Outer IP Header  |   GRE Header   |    Inner IP Header   |    Payload       |
    |-------------------|----------------|----------------------|------------------|

GRE头部最小4个字节长度,其后为可选字段,第一个字节中的标志位决定是否存在后面的字段,最大长度为16个字节。以下为一个标准的GRE报头格式:

    |------------------------------------------------------------------------------|
    |    bit0-3   |      4-12      | 13-15 |                16 - 31                |
    |-------------|----------------|-------|---------------------------------------|
    | C | | K | S |    Reserved0   |  Ver  |            Protocol Type              |
    |--------------------------------------|---------------------------------------|
    |           Checksum(optional)         |         Reserved1(optional)           |         
    |------------------------------------------------------------------------------|
    |                                 Key(optional)                                |
    |------------------------------------------------------------------------------|
    |                           Sequence Number (optional)                         |
    |------------------------------------------------------------------------------|

C, K, and S      : 分别对应Checksum、Key和Sequence Number字段,置1表示存在相应的字段,否则无此字段。
Ver                   : GRE版本号(为0),对于PPTP GRE,此字段为1。
Protocol Type  : 封装的以太网协议类型(例如IPv4,此处为0x0800)
Checksum       : 如果C位为1,此字段包含由GRE头部开始的所有数据的校验和
Key                  : 如果K位为1,此字段包含秘钥信息
Sequence Number: 如果S位为1,此字段包含GRE数据包的序号


Linux配置命令


需要两台设备(Ubuntu操作系统)进行配置和测试。一台设备A的IP地址为192.168.1.113,另外一台设备B的IP地址为192.168.1.123。首先在设备A上配置GRE隧道,如下命令:


$ sudo ip tunnel add gre01 mode gre remote 192.168.1.123 local 192.168.1.113 ttl 255
$ sudo ip link set gre01 up
$ sudo ip addr add 10.1.1.1/24 dev gre01

以上ip tunnel命令创建gre01隧道设备,其本地地址为192.168.1.113,源端地址为192.168.1.123,发送数据包时添加的外层IP报头的源IP和目的IP地址将使用这两个地址,ttl字段使用255。隧道设备gre01自身的IP地址配置为10.1.1.1,掩码255.255.255.0。使用IP命令检查gre01接口的配置:

$ sudo ip addr list dev gre01
7: gre01@NONE: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1476 qdisc noqueue state UNKNOWN group default qlen 1000
    link/gre 192.168.1.113 peer 192.168.1.123
    inet 10.1.1.1/24 scope global gre01

设备B的配置命令如下:

$ sudo ip tunnel add gre01 mode gre remote 192.168.1.113 local 192.168.1.123 ttl 255
$ sudo ip link set gre01 up
$ sudo ip addr add 10.1.1.2/24 dev gre01

至此,GRE隧道配置完成,使用ping命令测试连通性,在设备A上ping:

$ ping 10.10.10.2
PING 10.10.10.2 (10.10.10.2) 56(84) bytes of data.
64 bytes from 10.10.10.2: icmp_req=1 ttl=64 time=0.619 ms

通过WireShark抓包可见完整的GRE报头信息。

以下配置使能GRE的三个选项字段:Checksum、Key和Sequence Number。其中Linux内核支持三个参数的方向性配置,例如从A到B的秘钥使用0x222222,从B到A的秘钥使用0x111111,如果两个方向秘钥相同可使用key参数指定一次即可。校验和csum(icsum和ocsum)和序列号seq(iseq和oseq)同样支持类似的方向性配置。

设备A:
$ sudo ip tunnel add gre01 mode gre remote 192.168.1.123 local 192.168.1.113 ttl 255 ikey 0x111111 okey 0x222222 csum seq
$ sudo ip addr add 10.1.1.1/24 dev gre01
$ sudo ip link set gre01 up 

设备B:
$ sudo ip tunnel add gre01 mode gre remote 192.168.1.113 local 192.168.1.123 ttl 255 ikey 0x222222 okey 0x111111 csum seq
$ sudo ip addr add 10.1.1.2/24 dev gre01
$ sudo ip link set gre01 up 

再次WireShark抓包,查看GRE完整的头部信息:


最后,使用如下命令删除GRE隧道设备:

$ sudo ip link set gre0 down
$ sudo ip tunnel del gre0

GRE内核实现

内核注册ipgre_link_ops处理ip命令创建gre隧道的请求,GRE隧道不分类型(gre/gretap/erspan)在内核中统一由ip_tunnel结构体表示。ipgre_newlink函数处理隧道的建立过程。

static struct rtnl_link_ops ipgre_link_ops __read_mostly = {
    .kind       = "gre",              
    .priv_size  = sizeof(struct ip_tunnel),  
    .newlink    = ipgre_newlink,
};

函数ipgre_newlink在进行最初的参数解析工作之后,调用ip_tunnel_newlink函数处理隧道设备的建立,首先是向系统注册隧道网络设备,其次如果用户没有指定此设备的MAC二层地址,随机生成一个地址。

    err = register_netdevice(dev);
    if (dev->type == ARPHRD_ETHER && !tb[IFLA_ADDRESS])
        eth_hw_addr_random(dev);
    mtu = ip_tunnel_bind_dev(dev);
    if (!tb[IFLA_MTU])
        dev->mtu = mtu;
    ip_tunnel_add(itn, nt);

ip_tunnel_bind_dev函数根据隧道的目的IP地址等信息查询出口路由信息,获得物理出口设备。根据出口设备的硬件头部长度和所需头部空间,与隧道头部长度之和设置隧道设备的头部所需空间。隧道设备的MTU值需要减去隧道头部长度与外层IP头部长度之和。对于隧道设备而言,hard_header_len为0。

	int t_hlen = tunnel->hlen + sizeof(struct iphdr);
    if (tdev) {
        hlen = tdev->hard_header_len + tdev->needed_headroom;
        mtu = tdev->mtu;
    }
    dev->needed_headroom = t_hlen + hlen;
    mtu -= (dev->hard_header_len + t_hlen);

GRE隧道发送

GRE隧道设备的操作函数集为ipgre_netdev_ops,其中发送函数ipgre_xmit负责隧道数据帧的发送工作:

static const struct net_device_ops ipgre_netdev_ops = {
    .ndo_init       = ipgre_tunnel_init,
    .ndo_start_xmit     = ipgre_xmit,
};
static void ipgre_tunnel_setup(struct net_device *dev)
{
    dev->netdev_ops     = &ipgre_netdev_ops;
}

隧道数据帧的发送首先是一个组包的过程,内核协议栈在发送数据帧时,根据路由表查找到出口设备,此时为gre隧道设备,为数据包添加二层头部信息,见函数ipgre_header回调。根据设备的隧道信息初始化gre头部的标志和协议字段,以及初始化外层IP头部。

static const struct header_ops ipgre_header_ops = {
    .create = ipgre_header,
    .parse  = ipgre_header_parse,
};
static int ipgre_header(struct sk_buff *skb, ...)
{
    greh = (struct gre_base_hdr *)(iph+1);
    greh->flags = gre_tnl_flags_to_gre_flags(t->parms.o_flags);
    greh->protocol = htons(type);

    memcpy(iph, &t->parms.iph, sizeof(struct iphdr));
    if (saddr)
        memcpy(&iph->saddr, saddr, 4);
    if (daddr)
        memcpy(&iph->daddr, daddr, 4);
}

之后ipgre_xmit函数调用__gre_xmit函数,从隧道设备中取出隧道结构体,判断如果设置了发送方向的序列号功能,增加隧道的发送序列号。随后由函数gre_build_header填充GRE头部信息。由于在ipgre_header函数中已经将外层IP头部信息填写完成,此处主要根据GRE头部的标志字段增加可选字段信息。 最后由函数ip_tunnel_xmit发送完成的数据包。

    struct ip_tunnel *tunnel = netdev_priv(dev);

    if (tunnel->parms.o_flags & TUNNEL_SEQ)
        tunnel->o_seqno++;
    gre_build_header(skb, tunnel->tun_hlen, tunnel->parms.o_flags, proto, tunnel->parms.o_key, htonl(tunnel->o_seqno));
    ip_tunnel_xmit(skb, dev, tnl_params, tnl_params->protocol);

GRE隧道接收

GRE通过net_gre_protocol网络协议结构体注册到协议栈处理流程中。所有协议号为IPPROTO_GRE(47)的数据帧都交由net_gre_protocol中的handler即入口函数gre_rcv处理。

static const struct net_protocol net_gre_protocol = {
    .handler     = gre_rcv,
    .err_handler = gre_err,
};
static int __init gre_init(void)
{
    inet_add_protocol(&net_gre_protocol, IPPROTO_GRE);
}

GRE数据包的接收入口函数gre_rcv,位于net/ipv4/gre_demux.c文件中,其根据GRE头部的版本字段,调用不同的处理函数。目前内核支持的GRE协议版本有两个GREPROTO_CISCO(0)和GREPROTO_PPTP(1),不同版本的GRE处理函数通过gre_add_protocol注册到静态GRE协议数组gre_proto中,gre_rcv函数根据版本号调用不同的回调函数进行处理。除GRE版本0的协议处理结构net_gre_protocol外,GRE版本1的处理结构为gre_pptp_protocol。

static int gre_rcv(struct sk_buff *skb)
{
    const struct gre_protocol *proto;

    ver = skb->data[1]&0x7f;
    proto = rcu_dereference(gre_proto[ver]);
    ret = proto->handler(skb);
}
static const struct gre_protocol __rcu *gre_proto[GREPROTO_MAX] __read_mostly;

本文仅涉及GRE版本0的相关处理过程。代码位于net/ipv4/ip_gre.c文件中,内核协议处理结构为gre_protocol,其注册到了上文中的gre_proto数组中。在接收到版本为0的数据包时,调用本文件中的gre_rcv(与文件gre_demux.c中的gre_rcv不同)进行处理。

	static const struct gre_protocol ipgre_protocol = {
		.handler     = gre_rcv,
		.err_handler = gre_err,
	};

函数gre_rcv首先调用gre_parse_header函数解析GRE头部信息,其中最重要的是根据gre头部的第一个字节的标志位确定头部长度,如果存在校验和字段,则进行校验和的验证。需要注意的是如果封装的是WCCP协议,将隧道信息中的协议字段修正为htons(ETH_P_IP)IP协议,并且如果是WCCPv2,GRE头部长度额外增加4个字节。
 

    if (greh->flags == 0 && tpi->proto == htons(ETH_P_WCCP)) {
        tpi->proto = proto;
        if ((*(u8 *)options & 0xF0) != 0x40)
            hdr_len += 4;
    }

头部信息解析完成之后,gre_rcv判断如果内层的协议为交换接口远程分析封装协议(Encapsulated Remote Switched Port Analyzer),调用erspan_rcv进行处理。否则调用ipgre_rcv进行处理,正是此函数处理我们之前使用ip命令配置的gre隧道传输。

    if (unlikely(tpi.proto == htons(ETH_P_ERSPAN))) {
        if (erspan_rcv(skb, &tpi, hdr_len) == PACKET_RCVD)
            return 0;
    }
    if (ipgre_rcv(skb, &tpi, hdr_len) == PACKET_RCVD)
        return 0;

核心处理函数__ipgre_rcv,其根据数据包信息完成tunnel隧道的查找,调用ip_tunnel_rcv函数接收数据包。

static int __ipgre_rcv(struct sk_buff *skb, const struct tnl_ptk_info *tpi, struct ip_tunnel_net *itn, int hdr_len, bool raw_proto)
{
    tunnel = ip_tunnel_lookup(itn, skb->dev->ifindex, tpi->flags, iph->saddr, iph->daddr, tpi->key);
    if (tunnel) 
        ip_tunnel_rcv(tunnel, skb, tpi, tun_dst, log_ecn_error);
}

 
内核版本

Linux-4.15

 

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页