GRE协议与Conntrack的兼容性问题

在一个包含多个跃点的网络中建立若干条GRE隧道, 如果隧道是从网络外的一侧通往网络外部的另一侧, 且通往网络外部的一侧配置了NAT, 那么会遇到比较头疼的问题.

首先我们知道, 对于conntrack来说, 确立一条”连接”需要五个因素: 协议, 源IP, 源端口, 目标IP, 目标端口, 而GRE协议没有端口的, 所以对于conntrack来说如果源IP目标IP一致, 就会认为是同一条隧道. 进而表现为同一时间只有一个GRE隧道能通过NAT且有流量.

但是当我们打开RFC文档, 不难发现即使是最早的RFC 1701版本中也规定了一个key field可以用来在源IP, 目标IP都相同的时候用来给两侧终端区分不同链接. 后续新版的GRE协议, 也有RFC 2890给出了针对GRE协议的key field扩展, 而且与RFC 1701的协议是兼容的. 那为什么conntrack不把key加入到用来区分不同链接的因素中呢?

带着这个疑问, 让我们打开Linux源码(狗头):

net/netfilter/nf_conntrack_proto_gre.c 中, gre_pkt_to_tuple是用来解析GRE报文并提取链接要素的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* gre hdr info to tuple */
bool gre_pkt_to_tuple(const struct sk_buff *skb, unsigned int dataoff,
struct net *net, struct nf_conntrack_tuple *tuple)
{
const struct pptp_gre_header *pgrehdr;
struct pptp_gre_header _pgrehdr;
__be16 srckey;
const struct gre_base_hdr *grehdr;
struct gre_base_hdr _grehdr;

/* first only delinearize old RFC1701 GRE header */
grehdr = skb_header_pointer(skb, dataoff, sizeof(_grehdr), &_grehdr);
if (!grehdr || (grehdr->flags & GRE_VERSION) != GRE_VERSION_1) {
/* try to behave like "nf_conntrack_proto_generic" */
tuple->src.u.all = 0;
tuple->dst.u.all = 0;
return true;
}

/* PPTP header is variable length, only need up to the call_id field */
pgrehdr = skb_header_pointer(skb, dataoff, 8, &_pgrehdr);
if (!pgrehdr)
return true;

if (grehdr->protocol != GRE_PROTO_PPP) {
pr_debug("Unsupported GRE proto(0x%x)\n", ntohs(grehdr->protocol));
return false;
}

tuple->dst.u.gre.key = pgrehdr->call_id;
srckey = gre_keymap_lookup(net, tuple);
tuple->src.u.gre.key = srckey;

return true;
}

可以看到这里 判断了GRE包是否的GRE_VERSION bit 是否不是 GRE_VERSION_1, 这个宏的定义在 include/uapi/linux/if_tunnel.h:

1
2
3
#define GRE_VERSION_0		__cpu_to_be16(0x0000)
#define GRE_VERSION_1 __cpu_to_be16(0x0001)
#define GRE_PROTO_PPP __cpu_to_be16(0x880b)

为什么会有 GRE_VERSION_1 呢? 在RFC 2784中可以找到答案:

1
2
3
4
5
7.1.  GRE Version Numbers

This document specifies GRE version number 0. GRE version number 1 is
used by PPTP [RFC2637]. Additional GRE version numbers are assigned
by IETF Consensus as defined in RFC 2434 [RFC2434].

所以这里conntrack的逻辑是, 只要是普通的GRE协议, 就直接返回. 只有当协议是PPTP的时候才会提取key字段. 这也就解释了为什么同一时间只能有一个有效的GRE隧道.

1
gre      47 169 src=<redacted> dst=<redacted> srckey=0x0 dstkey=0x0 src=<redacted> dst=<redacted> srckey=0x0 dstkey=0x0 [ASSURED] mark=0 use=1

如此看来唯一的解决办法就是绕过Conntrack, 使用如下命令:

1
iptables -t raw -A PREROUTING -p gre -j NOTRACK

注意这会让GRE协议跳过conntrack, 也不会进入nat表, 因此在网络外侧的接收点需要手动或其他方式来配置到网络另一侧节点的IP路由. (原来的时候remote只需要写出网边缘节点的IP即可)