在IS-IS协议里,一个系统产生的链路状态信息通常由一组LSP承载。当拓扑很大、前缀很多或者携带流量工程等扩展信息时,单个LSP片段很容易接近链路MTU上限。此时需要把信息拆分到多个LSP片段中,并用片段编号区分同一系统生成的不同片段。用Ruby实现这一机制,关键不在于复杂加密或高性能,而在于把协议字段、TLV边界和分片策略表达清楚。

大LSP为什么必须做分片扩展
IS-IS的LSP并不是单个固定报文,而是一组可以独立泛洪的链路状态记录。每个LSP都由LSP ID、剩余寿命、序列号、校验和以及TLV载荷组成。LSP ID里包含系统ID、伪节点ID和片段编号。片段编号虽然只占一个字节,却决定了同一个系统最多可以直接生成256个片段。当路由表规模增大、接口地址变多或者承载流量工程信息时,单个片段很容易触达链路MTU,继续把所有信息塞进一片会造成报文被丢弃。
从工程角度看,分片不是简单地把字节流切开。每个片段都要有完整头部,能独立被邻居识别、校验和计算、数据库同步和老化。如果分片器只负责切数据,却不维护片段编号和头部字段,接收端就无法判断这些片段是否来自同一组LSP,也无法判断是否已经收齐。因此,Ruby实现的第一步是把LSP看成结构化对象,而不是普通字符串。
还有一个常被忽略的问题:标准TLV通常以单字节长度字段表示值长度,单个TLV的值最多255字节。若要承载更大的对象,要么使用协议已经定义好的扩展机制,要么在实验环境里引入私有扩展。本文采用后一种思路,用一个演示型扩展TLV承载大块数据的分块,重点说明分片器如何与重组器配合。
用Ruby建模LSP头部和TLV
Ruby处理二进制数据时,建议使用二进制编码字符串,并用bytesize和byteslice操作字节,避免字符编码造成误判。TLV可以用一个简单类封装类型和值,编码时先写入1字节类型和1字节长度,再追加值内容。下面这个实现只适合实验,但它能清晰表达TLV边界。
# 简化的TLV对象,仅演示编码结构
class Tlv
attr_reader :type, :value
def initialize(type, value)
@type = type
@value = value.dup.force_encoding(Encoding::BINARY)
end
def encoded
raise 'TLV值超过单字节长度上限' if value.bytesize > 255
[type, value.bytesize].pack('CC') + value
end
end
LSP头部字段较多,完整实现需要处理PDU类型、长度、剩余寿命、序列号和校验和。为了突出分片逻辑,下面的代码把头部抽象成一个哈希,仅保留关键字段。真实实现时,应使用二进制打包函数生成可发送字节流,并按协议要求计算校验和。
| 字段 | 作用 | 实现注意点 |
|---|---|---|
| LSP ID | 标识系统、伪节点和片段 | 片段编号必须从0开始递增 |
| 序列号 | 判断新旧LSP | 每个片段都要可独立比较 |
| 校验和 | 保证内容完整 | 示例未计算,真实实现必须补齐 |
| 剩余寿命 | 控制数据库老化 | 分片更新时要避免寿命不一致 |
在载荷容量上,不建议直接假设以太网MTU就是固定值。不同链路封装、二层开销和实现差异都会影响可用载荷。示例里使用一个可配置的max_payload,这样在实验室里可以按抓包结果调整,而不必修改分片逻辑。
大TLV的扩展承载与分片策略
如果所有TLV都很小,只需按顺序装箱,当前片段放不下就开启新片段。但大LSP场景常常会出现一个TLV超过255字节的情况。为了让演示更接近真实需求,可以定义一个实验用扩展类型,把原始大TLV拆成多个小块。每个块里携带原始类型、原始长度、块序号和总块数,接收端按序号重组。
# 实验用私有扩展类型,真实网络需谨慎
LARGE_CHUNK_TLV_TYPE = 247
CHUNK_HEADER_SIZE = 5
CHUNK_DATA_SIZE = 255 - CHUNK_HEADER_SIZE
def split_large_tlv(tlv)
return [tlv] if tlv.value.bytesize <= 255
total = (tlv.value.bytesize + CHUNK_DATA_SIZE - 1) / CHUNK_DATA_SIZE
raise '单个大TLV分块数超过演示上限' if total > 255
(0...total).map do |index|
part = tlv.value.byteslice(index * CHUNK_DATA_SIZE, CHUNK_DATA_SIZE)
header = [tlv.type, tlv.value.bytesize, index, total].pack('CnCC')
Tlv.new(LARGE_CHUNK_TLV_TYPE, header + part)
end
end
这种扩展的关键不是类型编号本身,而是接收端是否理解它。若在真实生产网络中使用,必须确保所有IS-IS设备都识别该扩展,否则这些TLV会被忽略或导致兼容性问题。更稳妥的做法是优先使用协议已有的扩展能力,例如多拓扑、扩展可达性或厂商定义的标准结构。本文的私有扩展只适合实验环境,用来验证分片和重组流程。
如果总块数超过255,说明单个大对象已经超出演示结构的能力,需要设计二级分块,或者改用更大长度字段的扩展。对于实验原型,先在入口处限制块数,可以尽早暴露数据规模问题,避免生成无法被简单重组器识别的片段集合。
分片器在调用扩展拆分后,再按载荷上限进行装箱。若某个扩展后的TLV仍然超过单片载荷,说明MTU假设过小或者扩展设计不合理,需要继续细分。下面的LspFragmenter把这两步合在一起,输出带片段编号的LSP对象。
class LspFragmenter
DEFAULT_MAX_PAYLOAD = 1400
def initialize(system_id, max_payload: DEFAULT_MAX_PAYLOAD)
@system_id = system_id.dup.force_encoding(Encoding::BINARY)
@max_payload = max_payload
raise 'system_id必须为6字节' unless @system_id.bytesize == 6
end
def fragment(tlvs)
expanded = tlvs.flat_map { |tlv| split_large_tlv(tlv) }
bins = []
current = String.new(encoding: Encoding::BINARY)
expanded.each do |tlv|
encoded = tlv.encoded
if current.bytesize + encoded.bytesize <= @max_payload
current << encoded
else
bins << current unless current.empty?
current = String.new(encoding: Encoding::BINARY)
if encoded.bytesize > @max_payload
raise '单个扩展TLV仍超过分片载荷上限,需要降低MTU假设或继续拆分'
end
current << encoded
end
end
bins << current unless current.empty?
raise '片段数超过可用编号空间' if bins.size > 256
bins.each_with_index.map do |payload, fragment_id|
build_lsp(payload, fragment_id)
end
end
private
def build_lsp(payload, fragment_id)
lsp_id = @system_id + [0, fragment_id].pack('CC')
{
lsp_id: lsp_id,
pdu_type: 20,
length: 27 + payload.bytesize,
remaining_lifetime: 1200,
sequence_number: 1,
checksum: 0,
payload: payload
}
end
end
这里把PDU类型写成20,只是用Level 2 LSP的常见类型做演示。长度字段里的固定偏移也只是示意,真实PDU要按实际头部长度重新计算。原型阶段可以先保证结构清晰,等到接入抓包或真实协议栈时,再逐字段对齐标准编码。
重组、测试和工程边界
接收端需要先按LSP ID中的基础标识归组,再按片段编号排序。基础标识通常由系统ID和伪节点ID组成,片段编号单独取出。下面这个重组器只做最基础的归组,实际系统还要校验序列号、剩余寿命和校验和,并处理过期片段替换。
class LargeChunkBuffer
def initialize
@chunks = {}
@original_type = nil
@original_length = nil
end
def add(value)
original_type, original_length, index, total = value.unpack('CnCC')
data = value.byteslice(CHUNK_HEADER_SIZE, value.bytesize - CHUNK_HEADER_SIZE)
@original_type ||= original_type
@original_length ||= original_length
@chunks[index] = data
return nil unless @chunks.size == total
merged = @chunks.sort.map { |_, part| part }.join
raise '重组长度不一致' unless merged.bytesize == @original_length
Tlv.new(@original_type, merged)
end
end
class LspReassembler
def initialize
@fragments = Hash.new { |hash, key| hash[key] = {} }
end
def add(lsp)
base = lsp[:lsp_id].byteslice(0, 7)
fragment_id = lsp[:lsp_id].byteslice(7, 1).unpack1('C')
@fragments[base][fragment_id] = lsp[:payload]
end
def payload(base)
@fragments[base].sort.map { |_, part| part }.join
end
end
对于扩展大TLV,重组器还要把同一组块按序号拼接。LargeChunkBuffer接收每个扩展块的值,在收齐总块数后还原原始TLV。若中途丢失一块,就不能提前返回完整对象,否则会把不完整数据交给上层路由计算,造成链路状态数据库出现脏数据。
测试时建议覆盖几个典型边界:载荷刚好等于上限、超过上限一个字节、单个大TLV刚好整除块大小、单个大TLV不整除块大小、片段数接近256。这些用例能快速暴露装箱逻辑和序号字段的问题。还可以用十六进制输出检查LSP ID,确认系统ID、伪节点ID和片段编号的排列符合预期。
system_id = ['010203040506'].pack('H*')
large_value = 'A'.b * 1000
tlvs = [
Tlv.new(137, 'edge-router'.b),
Tlv.new(200, large_value)
]
fragmenter = LspFragmenter.new(system_id, max_payload: 1400)
fragments = fragmenter.fragment(tlvs)
puts "生成分片数量: #{fragments.size}"
puts "首片LSP ID十六进制: #{fragments.first[:lsp_id].unpack1('H*')}"
最后需要强调,示例没有实现校验和与泛洪状态机。真实IS-IS实现中,校验和错误会直接导致LSP被丢弃;序列号回绕、数据库过载和分片数量耗尽也都需要明确策略。把这些边界补齐之后,Ruby原型就可以作为抓包分析、控制器实验或协议教学的基础工具。