技术 · 2021年3月27日 3 553  

自建GRE隧道修改服务器回程线路

自建GRE隧道修改服务器回程线路

Olink cloud之前开始销售了一种很有意思的产品 Transit Over GRE,可以在你的服务器和商家的服务器间建立一个GRE隧道,然后让目的地址为大陆的数据包全部经过隧道传输,实现去程走服务器自己的线路,而回程修改成联通的as10099~as9929以达到优化回程线路的效果。感觉这个挺有意思的,第一次看见有商家卖这样的产品,所以自己也想着尝试一下这个技术。要想在修改了回程后依旧可以正常的建立连接,其中有一点非常重要——隧道另一端的机器必须支持伪造源ip(ip spoofing),只有这样才能让到达客户端的数据包的源ip是之前请求的目的ip。

正巧,我发现justhost的vps是可以伪造ip的(只要不要用于攻击,应该问题不大?),所以开始尝试自己整一个Transit Over GRE。

后来转了一圈发现基本所有的VPS的机房都做了严格的urpf(无法伪造源ip了),更加找不到线路优秀同时还支持ip spoofing的机器了,毕竟伪造ip极易被滥用,所以这种操作对于个人来说还是挺难真正用来优化回程的,只能当作是折腾并学习一下了。(谁要是发现有线路优质还支持伪造的vps,请告诉我)
如果想要优化自己的服务器的回程还是老实买商家提供的产品吧。
Olink的德国9929 GRE
Olink的美国9929 GRE

另外,想试试隧道的可以留言告诉我,我买的隧道吃灰中 haha

建立GRE隧道

关于GRE隧道的介绍可以查看此文

什么是GRE

通用路由封装或 GRE 是一种协议,用于将使用一个路由协议的数据包封装在另一协议的数据包中。“封装”是指将一个数据包包装在另一个数据包中,就像将一个盒子放在另一个盒子中一样。GRE 是在网络上建立直接点对点连接的一种方法,目的是简化单独网络之间的连接。它适用于各种网络层协议。

GRE 允许使用网络通常不支持的协议,因为数据包被包装在其他确实使用受支持协议的数据包中。要了解其工作原理,请想像一下汽车和渡轮之间的区别。汽车在陆地上行驶,而渡轮在水上行驶。汽车通常不能在水上行驶,但是可以将汽车装载到渡轮上。

我参考hetzner的教学文档Linux setup GRE tunnel来创建gre隧道,其中使用了两台服务器,作为GRE隧道服务端的Justhost的服务器以及作为客户端的Contabo。

为了便于描述,下文将 Justhost的服务器称为GRE服务端服务器,Contabo的服务器称为GRE客户端服务器

加载内核模块

客户端和服务端都要加载模块。

sudo modprobe ip_gre
lsmod | grep gre

image-20210327134724858

如果显示和上面一样就表示修改成功了,之后为了将数据包转发到隧道里面,我们需要安装iproute2和iptables,现在的系统一般都自带了这两个工具。

配置隧道

在隧道服务端开启ip包转发。

sudo echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
sudo sysctl -p

然后先在隧道服务端服务器创建一个隧道。(去掉方括号)

#创建一个隧道
sudo ip tunnel add gre1 mode gre local [服务端的ip] remote [客户端的ip] ttl 255
#添加一个ip 与虚拟网卡 (gre服务端ip设置为 10.0.0.1)
sudo ip addr add 10.0.0.1/30 dev gre1 #掩码也可以适当大一些,如果要连到隧道的设备比较多的话
#启动虚拟网卡
sudo ip link set gre1 up

之后便可以看见这个虚拟网卡了。

隧道

接着在隧道客户端服务器上也创建一个隧道,操作基本相同,就是源ip和目的ip对换一下,然后本地分配另一个ip。

sudo ip tunnel add gre1 mode gre local 203.0.113.1 remote 198.51.100.1 ttl 255
sudo ip addr add 10.0.0.2/30 dev gre1
sudo ip link set gre1 up

现在客户端也成功启动了gre隧道了。

客户端gre

此时两台服务器已经可以通过隧道直接连接了,在客户端上尝试使用 ping 10.0.0.1

GRE属于三层(网络层)隧道封装,数据都会被封装在里面传输,隧道并不会影响数据的加密等属性。

修改路由表

建立隧道之后我们可以修改路由表,在客户端上执行下列命令,相当于创建了一个名为GRE的路由表,之后源地址为 10.0.0.0/30 的数据包都会交由 GRE 表处理(第二行其实并非必须,如果是想修稿服务器到大陆的回程,10.0.0.0/30 应该替换为大陆的 CIDR地址块)。

#第一行命令的作用是创建一个持久化的路由表
sudo echo '100 GRE' >> /etc/iproute2/rt_tables
#设置GRE表的默认网关为 10.0.0.1 即justhost的那台服务器
#如果目的是 GRE 隧道,那么我们可以设置此表默认网关是 10.0.0.1,但是我们想要修改回程,所以操作有一些不同,不进行下面的操作。
#sudo ip rule add from 10.0.0.0/24 table GRE
#sudo ip route add default via 10.0.0.1 table GRE

#在本文中,我们的目的是修改回程,所以让所有的数据都进入 GRE 表中进行匹配
sudo ip rule add from all table GRE

路由测试

iproute2的学习可以参考此文

之后我们可以测试一下,GRE隧道服务端是否可以帮我们转发数据包,我在第三台服务器上使用tcpdump icmp 监听icmp的数据包,然后将第三台服务器的地址加入GRE表中,假设第三台服务器地址为 90.1.225.1,那么在GRE客户端服务器上添加一条路由规则

#向GRE中添加路由规则
#目的ip为 90.1.225.1 的数据包交由路由表 GRE处理
sudo ip route add 96.9.225.181 via 10.0.0.1 table GRE  
#之后数据包会依据 GRE 表的规则,交由网关 10.0.0.1 (隧道服务端)处理
#查看路由表内容
ip route show table GRE

table

之后再在GRE客户端服务器上ping这个ip,并在服务端抓包,可以看到数据包了。

不过,在被ping的机器上抓包还看不见数据包,而GRE服务端上看到的数据包源ip是10.0.0.2,因为我们还没有对 10.0.0.2 做nat转换,这个数据包是发不出去的,我们需要在GRE服务端服务器上执行下面的命令,来允许GRE客户端服务器可以主动通过GRE隧道进行连接。

iptables -t nat -A POSTROUTING -s 10.0.0.0/24 ! -o gre+ -j SNAT --to-source [gre服务端ip]

之后再次尝试 ping 第三个服务器,发现此时有能得到回应了,而在第三方服务器上的抓包能看见 echo request请求的ip是justhost的ip。

不伪造ip,只做nat

不过,既然我们的服务器允许伪造源ip,其实我们可以SNAT修改源ip为GRE客户端服务器的ip。

iptables -t nat -A POSTROUTING -s 10.0.0.2 ! -o gre+ -j SNAT --to-source [gre客户端服务器ip]

我一开始以为要建立连接需要在隧道服务端伪造请求的源ip,但是经过测试貌似并不需要修改ip,只需要在转发包的时候做一次SNAT,将源ip修改成隧道服务端的ip就可以了。

不过二者还是有一点区别

第一张图,是在不伪造源ip,只在隧道服务端服务器做SNAT的情况下,在被gre服务端服务器上抓包得到的结果。

而接着我们尝试伪造一下源ip,即在做SNAT的时候, –to-source 设置成gre客户端的公网ip。这样,GRE客户端服务器主动发起连接时也不会暴露GRE服务端服务器的ip了。

SNAT时使用伪造的ip

可以看见,此时目的主机上的icmp抓包显示,收到的包不再来自于justhost,而是来自于contabo的服务器。尽管实际上,数据包还是来自于justhost,只是源地址被改写成了contabo的ip

关于为什么需要允许ip伪造

我一开始在不支持ip伪造的机器上进行测试,将其作为隧道的服务端服务器,建立隧道后,我在隧道客户端上使用了mtr命令查看回程路由,看上去效果很不错,路由走了as10099~as9929的线路,延迟也大幅降低了,但是不久之后我就发现一个问题: 从gre客户端服务器上ping国内的ip可以ping通,主动建立连接时也可以成功建立连接,但是如果从国内ping该服务器则得不到响应。

在服务器上使用tcpdump抓包,发现实际上gre客户端服务器收到了icmp的包,icmp的回应也通过gre1虚拟网卡传递到了gre服务端服务器上,但是有一个异常的地方——该用来转发的服务器上抓到的icmp包的源地址依旧是客户端服务器的ip,即使我在 POSTROUTING 链里使用 SNAT 想把该源地址改成转发服务器的ip,但是始终没有生效。折腾了一下午,我在网上发现了一篇文章 追查iptables规则失效原因,总算是解决了我的疑惑。

NAT

This is the realm of the `nat’ table, which is fed packets from two netfilter hooks: for non-local packets, the NF_IP_PRE_ROUTING and NF_IP_POST_ROUTING hooks are perfect for destination and source alterations respectively. If CONFIG_IP_NF_NAT_LOCAL is defined, the hooks NF_IP_LOCAL_OUT and NF_IP_LOCAL_IN are used for altering the destination of local packets.

This table is slightly different from the `filter’ table, in that only the first packet of a new connection will traverse the table: the result of this traversal is then applied to all future packets in the same connection.

Netfilter的nat规则只应用于连接的第一个包,所以我们即使使用iptables向POSTROUTING里面添加了SNAT规则,但是 icmp 的echo reply,tcp的 ack包的源地址依旧是原来的ip,无法改成转发服务器的ip。

所以我们的gre转发服务器转发的数据包的源ip是不可修改的(除非是被转发的服务器主动发起的连接,只有这种情况下,数据包才能进行nat),而如果机房不允许伪造ip,那源地址不属于机房网络的数据包是没办法发出去的,会被直接丢弃,所以我们想要用一台服务器来修改另一台服务器的回程,那么这台服务器所在的机房必须要允许ip欺骗。

检查服务器是否可以伪造ip,可以使用下面的方法:

#首先在目的服务器上运行 tcpdump icmp 监听icmp数据包
iptables -t nat -A POSTROUTING --destination [目的服务器ip] -j SNAT --to-source 1.1.1.1
#ping目的服务器,看看目的服务器上是否可以看见 来自1.1.1.1的icmp数据包
ping [目的服务器ip]
#测试完后删除地址转换规则
iptables -t nat -D POSTROUTING 1

1.1。1.1

如图显示,表示我们的服务器支持ip伪造。

也可以使用Spoofer这个程序:

修改到大陆的路由

要修改到大陆的路由,我们首先要知道所有的大陆ip,可以查看APNIC的delegated-apnic-list,要提取其中的大陆ip,可以使用https://github.com/metowolf/iplist 大陆ip cidr

我写了一个脚本来处理这个列表。

# -*- coding: utf-8 -*-
import requests
url = 'https://raw.githubusercontent.com/metowolf/iplist/master/data/special/china.txt'
r = requests.get(url, allow_redirects=True)
open('ips.txt', 'wb').write(r.content)
# 之后生成添加路由表命令与卸载路由表命令
start = open('startCMD.sh', 'w')
end = open('endCMD.sh', 'w')
with open('ips.txt','r') as f:
  ips = f.read().splitlines()
  for ip in ips:
    start.write('ip route add ' + ip + ' via 10.0.0.1 table GRE\n') 
    end.write('ip route del ' + ip + ' via 10.0.0.1 table GRE\n')     
    # 生成启动脚本与卸载脚本 (暂时没分运营商)
    # print(ip)
start.close()
end.close()

启动的时候用bash startCMD.sh即可,如果不想用则可以使用bash endCMD.sh

GRE一对多连接

上面提到的方式都是在两台服务器之间点对点的建立隧道,如果我们同时想要改变多台服务器的回程,一条一条隧道的建看上去也太麻烦了,所以我想着要是能在一个隧道上创建多个endpoint就好了,网上关于GRE的资料真的不太多,找了挺久终于找到了一篇文章 Creating point-to-multipoint tunnels based on GRE encapsulation in Linux 2.6,作者在三台服务器间建立了条GRE隧道,虽然是十几年前的文章了,但是我觉得可以试试。

在三台服务器上使用相同的密钥创建GRE隧道(和之前不同,这里没有指定本地的公网ip和远程ip)

注意key字段,我们需要给出一个32位的密钥

ip tunnel add mgre0 mode gre key 0xfffffffe ttl 255

假设三台服务器的ip分别是 1.1.1.1 2.2.2.2 3.3.3.3,我们使用1.1.1.1来转发,接着在三台服务器上设置隧道的ip。

#机器1
ip addr add 10.0.0.1/24 dev mgre0
#机器2
ip addr add 10.0.0.2/24 dev mgre0
#机器3
ip addr add 10.0.0.3/24 dev mgre0

For Ethernet interfaces, that would be enough. Ethernet has the Adress Resolution Protocol (ARP), which allows systems to independently find the MAC address, knowing the IP address of the destination host. Ethernet is the Broadcast Multiple Access environment, and ARP consists of creating a request to all stations on the network (using the MAC address FF: FF: FF: FF: FF: FF): “Hey, which one of you has an IP address xxxx ?”. If a station with such an IP address is available, it already privately reports that “ xxxx is located at yy: yy: yy: yy: yy: yy ”.

如果三台机器在同一个以太网中的时候,因为ARP的广播,三台机器之间已经可以连接了,但是在因特网中,我们还得设置ip的关联。NHRP协议可能可以为我们自动的解决ip地址关联的问题,大概可以让一台服务器连接到隧道而不需要手动进行配置,但是我们暂时不深入研究。

添加邻居,lladdr is the link layer address of the neighbour lladdr指的是链路层地址,但是文章中作者填的是邻居的公网ip,如果是在同一个以太网下,这个ip大概会被解析成MAC地址,另外,如果只是想修改回程,不需要各个端点之间互联的话,那么添加另据的时候,客户端只添加服务端的ip作为邻居即可,当然用作转发的1.1.1.1需要添加所有邻居的ip。

(on 1.1.1.1)
ip neigh add 10.0.0.2 lladdr 2.2.2.2 dev mgre0
ip neigh add 10.0.0.3 lladdr 3.3.3.3 dev mgre0

(on 2.2.2.2)
ip neigh add 10.0.0.1 lladdr 1.1.1.1 dev mgre0
ip neigh add 10.0.0.3 lladdr 3.3.3.3 dev mgre0

(at 3.3.3.3)
ip neigh add 10.0.0.1 lladdr 1.1.1.1 dev mgre0
ip neigh add 10.0.0.2 lladdr 2.2.2.2 dev mgre0

另外在三台服务器上使用 ip link set mgre0 up 启用隧道。我试着在启动隧道后再添加邻居,发现出现了添加邻居后隧道不通的情况,暂时没找到解决方法,所以先添加邻居再启用隧道,或者添加后先set down 再 set up。

之后我们可以看见隧道之间可以互联了。

多点GRE隧道

之后就只需要照着上文的方法修改路由表,以及在POSRTOURING中做一次SNAT修改对应隧道ip的出口ip即可(需要支持ip欺骗),另外记得在用于转发的 1.1.1.1上开启ipv4包转发。

10.0.1.2上
echo '99 mgre' >> /etc/iproute2/rt_tables
ip rule add from all table mgre
ip route add 96.9.225.181 via 10.0.1.1 table mgre
#也可以修改一下上面的python脚本中的网关和路由表名字生成批量执行脚本 startCMD.sh
10.0.1.1上
echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
sysctl -p
iptables -t nat -A POSTROUTING -s 10.0.1.2 ! -o gre+ -j SNAT --to-source 2.2.2.2

现在就可以让多个服务器用同一条隧道来改变回程了,但是目前配置起来还有一点麻烦,之后有空可以考虑折腾一下NHRP,实现自动添加。

持久化

重启后GRE隧道会失效,所以我们要把上面执行的命令(建立隧道,添加地址,启动虚拟网卡,修改路由表)都写到 /etc/rc.local 里面去。

结语

这篇文章更多的是本着探索的目的来折腾,因为大部分机房或者卖家都通过urpf或者iptables限制了ip spoofing,能伪造ip的机器没几台,而且大多线路炸裂,可能不光没有优化回程,反而劣化了回程。如果真的想通过GRE优化回程还是建议直接买Olink家的圣何塞9929 GRE/法兰克福 9929GRE,欢迎走我的aff支持一下 https://www.olink.cloud/clients/aff.php?aff=167 😄。

参考

什么是GRE隧道

追查iptables失效的原因

linux setup gre tunnel (hetzner)

iptables使用教程

tcpdump命令详解

Creating point-to-multipoint tunnels based on GRE encapsulation in Linux 2.6