GRE(Generic Routing Encapsulation)是一种通用的报文隧道协议,可以把任意网络层协议的数据包封装进另一个IP包中传输。使用Ruby语言实现一套简单的GRE隧道,重点在于理解封装与解封装的报文结构,以及怎样通过路由表把特定流量引入隧道接口。Ruby虽然不像C那样能直接操作内核网络栈,但借助原始套接字(raw socket)和系统命令,依然可以搭建出可用的实验性隧道。

GRE报文封装的原理与Ruby实现
GRE头最少占用4个字节,结构包括标志位、协议类型等字段。最基础的GRE头格式为:前16位中的高3位为标志(C、K、S),其余为版本号(通常为0),接着的16位是协议类型,例如0x0800代表IPv4。在Ruby中我们可以用Array#pack把头部字段打包成二进制串,再把内层IP包接在后面,最后交给外层IP头封装发送。
下面的代码展示了如何用Ruby构造一个不带校验和的GRE头,并将内层的原始IP报文封装起来。这里使用Socket::IPPROTO_RAW创建原始套接字,由我们自己构造完整的IP头,因此外层源和目的地址都需要明确写出。
require 'socket'
def build_gre_header(protocol = 0x0800)
# 标志位为0,版本0,协议类型为IPv4
[0, protocol].pack('nn')
end
def build_ip_header(src, dst, payload)
# 简化版IPv4头,20字节
version_ihl = 0x45
total_len = 20 + payload.bytesize
[version_ihl, 0, total_len, 0, 0, 64, Socket::IPPROTO_RAW, 0,
src.split('.').map(&:to_i), dst.split('.').map(&:to_i)].pack('CCnnnCCnCCCCCCCC')
end
src = '192.168.1.1'
dst = '192.168.2.1'
inner_packet = "x45x00x00x1cx00x01x00x00x40x01x00x00x0ax00x00x01x0ax00x00x02"
gre = build_gre_header
ip_outer = build_ip_header(src, dst, gre + inner_packet)
sock = Socket.open(Socket::AF_INET, Socket::SOCK_RAW, Socket::IPPROTO_RAW)
sock.send(ip_outer, 0, Socket.pack_sockaddr_in(0, dst))
sock.close
上面这段程序在真实环境里还需要处理校验和以及IP头中部分字段的字节序问题。Ruby的pack模板使用网络字节序(大端)比较方便,但如果运行在小型机上就要注意转换。此外,自己构造IP头时要把IPPROTO_RAW的发送权限考虑进去,普通用户通常需要root才能创建这类套接字。
从工程角度看,纯Ruby封装适合做原型验证,它的性能远低于内核态的GRE模块。每封装一个包都要经过Ruby解释器,吞吐量很难超过每秒几万包。如果你的场景只是打通两个测试网段,这种方式足够;若是生产环境,还是应当交给系统自带的GRE接口。
接收端与解封装处理逻辑
解封装是封装的逆过程:从原始套接字读取外层IP包,跳过外层IP头(通常20字节,若有选项则更多),再跳过GRE头(至少4字节),剩下的就是内层报文。Ruby里可以用String#unpack或者直接用偏移量切片拿到内部数据,然后决定是交给本地协议栈还是从另一块虚拟网卡转发出去。
下面示例展示了一个简单的接收循环,它监听IPPROTO_GRE类型的原始套接字,把收到的包去掉外层头后打印内层协议类型。注意不同系统对IPPROTO_GRE的定义可能不同,Linux下通常是47。
require 'socket'
begin
sock = Socket.open(Socket::AF_INET, Socket::SOCK_RAW, 47)
rescue Errno::EPERM
abort '需要root权限才能打开GRE原始套接字'
end
loop do
pkt = sock.recv(65535)
# 跳过外层IPv4头(假设无选项)
outer_ihl = (pkt[0].ord & 0x0f) * 4
gre_start = outer_ihl
flags_ver, proto = pkt[gre_start, 4].unpack('nn')
inner = pkt[gre_start + 4..-1]
puts "收到GRE包,内层协议: 0x#{proto.to_s(16)},长度 #{inner.bytesize}"
end
解封装时最易踩的坑是分片。外层IP包如果在传输中被分片,接收端必须等所有分片到齐才能正确解析GRE头。Ruby的raw socket默认不会自动重组分片,你要么依靠内核的IP重组(某些系统会在递交给raw socket前重组),要么自己写分片缓存。实验环境下建议直接依靠内核重组,避免复杂逻辑。
另一个关键是内层包出来后怎么处理。如果是IPv4,你可以再用一个TUN设备写入,让系统路由去决策;也可以直接在Ruby里做应用层代理。前者更通用,但需要配合系统中的虚拟网卡;后者更轻量,但只适合特定协议。
路由表配置与隧道联通
隧道建起来后,必须让系统知道哪些流量该走隧道。在Linux上,最简单的方法是用ip route命令添加一条静态路由,把目标网段指向隧道接口。Ruby可以通过system或IO.popen调用这些命令,也可以解析/proc/net/route来读取现有表项。
以下代码演示了如何用Ruby调用ip命令添加路由,并设置一个较小的MTU以避免GRE开销导致分片。这里把到192.168.100.0/24的流量指向名为gre0的接口,MTU设为1400以留出外层头空间。
# 假设系统已创建gre0接口并配置本地/远端
system('ip link set gre0 up')
system('ip addr add 10.0.0.1/24 dev gre0')
system('ip route add 192.168.100.0/24 dev gre0')
system('ip link set gre0 mtu 1400')
# 读取当前路由表
routes = File.readlines('/proc/net/route')
routes.each do |line|
next if line.start_with?('Iface')
iface, dest = line.split[0, 2]
puts "接口 #{iface} 目标 #{dest}"
end
在配置路由时要小心递归路由:如果隧道的远端地址本身也匹配了你添加的那条路由,包就会在隧道里套隧道,最终TTL耗尽。解决方法是为隧道外层通信单独写一条主机路由,明确走真实物理网卡。Ruby脚本里可以先查默认网关,再据此排除外层地址。
此外,Ruby实现的用户态隧道往往没有内核模块那样自动处理ARP、ICMP重定向等机制。你可能会发现某些应用层程序因为PMTU发现问题而连接缓慢。此时除了调小MTU,还可以在路由上加上advmss参数限制TCP最大报文段,减少中途分片概率。这样整套Ruby GRE隧道加路由方案就能在实验网络中稳定工作。
RubyGRE_tunnelroute_table修改时间:2026-08-16 19:30:19