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数据包的序号
需要两台设备(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:
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
内核注册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隧道设备的操作函数集为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通过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