VRRPv3相比VRRPv2最大的变化是同时支持IPv4与IPv6,并且通告报文格式做了精简。在IPv6网络中,VRRPv3通告报文发往组播地址ff02::12,封装在IP上层,协议号为112。由于IPv6本身不提供类似IPv4头部的校验和,上层协议必须借助IPv6伪头部来完成端到端校验,否则中间设备或服务端会判定报文损坏。使用Ruby语言手工构造这类报文,既能帮助理解协议细节,也便于在测试环境模拟网关行为。

VRRPv3通告报文结构与IPv6负载字段解析
VRRPv3通告报文头部固定为8字节(不含IPv6选项)。第一个字节是版本与类型,版本号固定为3,类型值为1表示通告;第二个字节是虚拟路由器ID,范围1到255;第三个字节是优先级,255代表拥有者路由;第四个字节是IPv6专用计数,记录后面附加的IPv6地址数量,这也是所谓负载的一部分;随后两个字节是校验和;最后四个字节是最大广告间隔,单位为厘秒。负载区域紧跟在固定头部之后,每16字节承载一个虚拟IPv6地址。
在Ruby里,可以用Array#pack配合格式串把这些字段压成二进制。比如版本与类型用C,VRID用C,优先级用C,地址数用C,校验和先填0,间隔用n,地址用m或手动切分。需要强调的是,IPv6负载里的地址必须按网络字节序原样写入,不能做主机序转换。下面代码展示不含校验和的初始报文构造:
vrid = 10
priority = 100
max_int = 100 # 100 centiseconds
v6_addr = "2001:db8::1".split(':').map { |x| [x.to_i(16)].pack('n') }.join
payload = v6_addr
header = [0x31, vrid, priority, 1, 0, max_int].pack('CCCCn n')
# 注意上面校验和位置先写0,后面再补
pkt = header + payload
puts pkt.bytesize # 应为 8 + 16 = 24
这段代码中0x31即版本3左移4位加类型1。地址数填1,代表负载里有一个IPv6地址。实际场景中若有多个地址,需循环拼接并保证计数准确。很多初学者把负载地址数算错,导致对端解析偏移,进而校验和覆盖区域错误,这是排错时首先要核对的点。
IPv6伪头部与VRRPv3校验和计算原理
IPv6伪头部用于让上层协议校验时把源地址、目的地址、上层长度、下一个头纳入。VRRPv3校验和覆盖的范围是:伪头部 + VRRPv3报文整体(校验和字段自身置0)。伪头部格式为:源IPv6地址(16字节)、目的IPv6地址(16字节)、上层长度(4字节,网络序)、全零3字节、下一个头值(1字节,112)。Ruby里可以很方便地用pack拼出这个结构。
校验和算法是标准的Internet校验和:把数据按16位分组求和,溢出回卷,最后取反。由于IPv6地址和长度都是偶数长度,基本不会遇到奇数填充问题,但Ruby的字节串可能需补零对齐。下面示例实现伪头部拼接与校验和计算函数:
def checksum(data)
sum = 0
data.bytes.each_slice(2) do |b1, b2|
b2 ||= 0
sum += (b1 << 8) + b2
end
while sum > 0xffff
sum = (sum & 0xffff) + (sum >> 16)
end
(~sum & 0xffff)
end
src = "fe80::1".split(':').map { |x| [x.to_i(16)].pack('n') }.join
dst = "ff02::12".split(':').map { |x| [x.to_i(16)].pack('n') }.join
up_len = [pkt.bytesize].pack('N')
next_hdr = [112].pack('C')
pseudo = src + dst + up_len + "x00x00x00" + next_hdr
csum = checksum(pseudo + pkt)
puts csum.to_s(16)
上述函数把伪头部和原始报文连起来计算。注意up_len用N打包成4字节网络序,而原VRRP报文里间隔字段是n两字节。伪头部的下一个头字节必须精确填112,填错会使对端内核丢弃。实践中若用原始套接字发送,需自己保证IP层源地址正确,否则伪头部源不对,校验和对端仍不匹配。
完整发送流程与常见错误排查
构造完带校验和的报文后,需要通过IPv6原始套接字发出。Ruby标准库没有高级封装,但可以用Socket打开AF_INET6的SOCK_RAW,协议设为112。发送前要把算好的校验和写回报文第6、7字节。下面代码补全发送逻辑:
require 'socket'
pkt_with_csum = pkt.dup
pkt_with_csum[4,2] = [csum].pack('n')
sock = Socket.open(Socket::AF_INET6, Socket::SOCK_RAW, 112)
addr = Socket.sockaddr_in6(0, "ff02::12%eth0")
sock.send(pkt_with_csum, 0, addr)
sock.close
常见错误之一是接口作用域写漏,IPv6组播必须带接口名如%eth0,否则系统不知道从哪块网卡发。其二是校验和字节位置错位,VRRPv3头部里校验和紧跟地址数字段,偏移量4开始,若用字符串赋值要确认长度。其三是伪头部目的地址用了单播而非组播,部分系统驱动会拒绝。把这些点打印出来比对,基本能定位问题。
另一个容易被忽视的是负载中IPv6地址压缩写法解析。像::省略段在split(':')后会产生空串,直接to_i(16)得0,看似正确但其实丢失了位置信息。稳妥做法是先用IPAddr库展开成全格式再打包。这样在多地址负载时不会错位,也减少手工处理字符串的bug。掌握这些细节后,用Ruby模拟VRRPv3网关就变得可靠且易于调试。
RubyVRRPv3IPv6_checksum修改时间:2026-08-16 07:56:34