network-infra-utility 0.6.0 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/CHANGELOG.md +31 -0
- data/GUIDE.md +201 -212
- data/README.md +1 -6
- data/Rakefile +1 -1
- data/bin/gen-get +144 -0
- data/bin/geo-api +40 -1
- data/bin/geo-get +18 -36
- data/bin/ngeo-get +253 -0
- data/bin/packbit +227 -0
- data/bin/probe +134 -0
- data/document/reference/DNS/346/212/200/346/234/257/346/226/207/346/241/243.md +912 -0
- data/network-infra-utility.gemspec +6 -4
- data/network.rb +19 -1
- data/service/geodb/GeoAPI.md +64 -0
- data/service/geodb/api.rb +77 -19
- data/service/geoquery/GeoQuery.md +309 -0
- data/service/geoquery/cache.rb +78 -0
- data/service/geoquery/geo_cache.rb +233 -0
- data/service/geoquery/geoquery.rb +327 -0
- data/service/geoquery/local.rb +151 -0
- data/service/geoquery/merge.rb +52 -0
- data/service/geoquery/normalize.rb +102 -0
- data/service/geoquery/online.rb +136 -0
- data/service/simlab/SimLab.md +140 -0
- data/service/simlab/bin/simctl +152 -0
- data/service/simlab/lib/simlab.rb +124 -0
- data/service/simlab/lib/simlab_engine.rb +146 -0
- data/service/simlab/lib/simlab_observer.rb +176 -0
- data/service/simlab/lib/simlab_router.rb +330 -0
- data/service/simlab/lib/simlab_scenario.rb +200 -0
- data/service/simlab/lib/simlab_switch.rb +182 -0
- data/service/simlab/lib/simlab_topology.rb +196 -0
- data/service/simlab/lib/simlab_traffic.rb +167 -0
- data/support/basic/packet.rb +167 -0
- data/support/routing/bgp.rb +279 -0
- data/support/routing/lpm_trie.rb +145 -0
- data/support/routing/ospf.rb +243 -0
- data/support/routing/prefix.rb +155 -0
- data/support/routing/rib.rb +167 -0
- data/support/routing/rip.rb +223 -0
- data/support/routing/static_route.rb +102 -0
- data/support/switching/frame_forward.rb +87 -0
- data/support/switching/mac_table.rb +127 -0
- data/support/switching/stp.rb +230 -0
- data/support/switching/vlan.rb +155 -0
- data/tool/packbit/README.md +136 -0
- data/tool/packbit/config/five-tuple.yml +25 -0
- data/tool/packbit/config/flow.yml +17 -0
- data/tool/packbit/config/packbit.yml +41 -0
- data/tool/packbit/erl/packbit.erl +588 -0
- data/tool/probe/README.md +88 -0
- data/tool/probe/lib/probe.rb +54 -0
- data/tool/probe/lib/probe_base.rb +98 -0
- data/tool/probe/lib/probe_dns.rb +64 -0
- data/tool/probe/lib/probe_icmp.rb +71 -0
- data/tool/probe/lib/probe_ntp.rb +51 -0
- data/tool/probe/lib/probe_radius.rb +58 -0
- data/tool/probe/lib/probe_registry.rb +44 -0
- data/tool/probe/lib/probe_result.rb +75 -0
- data/tool/probe/lib/probe_snmp.rb +114 -0
- data/tool/probe/lib/probe_tcp.rb +82 -0
- data/tool/probe/lib/probe_version.rb +7 -0
- data/version.rb +1 -1
- metadata +74 -2
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# 虚拟路由器:组合 RIB + LpmTrie + StaticRoute + RIP/OSPF/BGP 实例 + 接口表。
|
|
5
|
+
#
|
|
6
|
+
# 每个协议实例是 support 组件的薄封装,只加邻居关系和报文收发口。
|
|
7
|
+
# 对外暴露 receive_packet、announce、install_route。
|
|
8
|
+
# 协议实例按接口绑定邻居。
|
|
9
|
+
#
|
|
10
|
+
# 用法:
|
|
11
|
+
# router = Router.new(name: "R1", interfaces: {"Gi0/0" => "10.0.0.1/24"})
|
|
12
|
+
# router.enable_ospf(router_id: "1.1.1.1")
|
|
13
|
+
# router.enable_rip
|
|
14
|
+
# router.receive_packet(port: "Gi0/0", frame: frame)
|
|
15
|
+
# router.rib_dump
|
|
16
|
+
|
|
17
|
+
require_relative '../../../support/routing/rib'
|
|
18
|
+
require_relative '../../../support/routing/lpm_trie'
|
|
19
|
+
require_relative '../../../support/routing/static_route'
|
|
20
|
+
require_relative '../../../support/routing/rip'
|
|
21
|
+
require_relative '../../../support/routing/ospf'
|
|
22
|
+
require_relative '../../../support/routing/bgp'
|
|
23
|
+
require_relative '../../../support/basic/packet'
|
|
24
|
+
|
|
25
|
+
module NetworkInfraUtility
|
|
26
|
+
module SimLab
|
|
27
|
+
module Device
|
|
28
|
+
class Router
|
|
29
|
+
attr_reader :name, :interfaces, :rib, :static_route
|
|
30
|
+
attr_reader :rip, :ospf, :bgp, :capabilities
|
|
31
|
+
|
|
32
|
+
def initialize(name:, interfaces: {}, **opts)
|
|
33
|
+
@name = name
|
|
34
|
+
@interfaces = {} # port => {ip:, prefix_len:, status:}
|
|
35
|
+
@rib = RIB.new
|
|
36
|
+
@static_route = StaticRoute.new
|
|
37
|
+
@capabilities = Set.new
|
|
38
|
+
@protocols = {}
|
|
39
|
+
@current_time = 0
|
|
40
|
+
|
|
41
|
+
# 初始化接口
|
|
42
|
+
interfaces.each do |port, cidr|
|
|
43
|
+
add_interface(port, cidr)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 添加接口。
|
|
48
|
+
def add_interface(port, cidr)
|
|
49
|
+
ip_str, mask_str = cidr.split('/')
|
|
50
|
+
prefix_len = mask_str.to_i
|
|
51
|
+
@interfaces[port] = {
|
|
52
|
+
ip: ip_str,
|
|
53
|
+
prefix_len: prefix_len,
|
|
54
|
+
cidr: cidr,
|
|
55
|
+
status: :up
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# 安装直连路由
|
|
59
|
+
network = compute_network(cidr)
|
|
60
|
+
@rib.add(
|
|
61
|
+
prefix: network,
|
|
62
|
+
next_hop: '0.0.0.0',
|
|
63
|
+
ifindex: port,
|
|
64
|
+
protocol: :connected,
|
|
65
|
+
metric: 0
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 接口 down。
|
|
70
|
+
def interface_down(port)
|
|
71
|
+
return unless @interfaces[port]
|
|
72
|
+
@interfaces[port][:status] = :down
|
|
73
|
+
|
|
74
|
+
# 移除直连路由
|
|
75
|
+
network = compute_network(@interfaces[port][:cidr])
|
|
76
|
+
@rib.delete(network, protocol: :connected)
|
|
77
|
+
|
|
78
|
+
# 通知协议
|
|
79
|
+
@protocols.each_value { |p| p.respond_to?(:remove_neighbor_by_port) && p.remove_neighbor_by_port(port) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# 接口 up。
|
|
83
|
+
def interface_up(port)
|
|
84
|
+
return unless @interfaces[port]
|
|
85
|
+
@interfaces[port][:status] = :up
|
|
86
|
+
|
|
87
|
+
# 恢复直连路由
|
|
88
|
+
network = compute_network(@interfaces[port][:cidr])
|
|
89
|
+
@rib.add(
|
|
90
|
+
prefix: network,
|
|
91
|
+
next_hop: '0.0.0.0',
|
|
92
|
+
ifindex: port,
|
|
93
|
+
protocol: :connected,
|
|
94
|
+
metric: 0
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 启用 OSPF。
|
|
99
|
+
def enable_ospf(router_id:, area: "0.0.0.0")
|
|
100
|
+
@ospf = OSPF.new(router_id: router_id, rib: @rib, area: area)
|
|
101
|
+
@protocols[:ospf] = @ospf
|
|
102
|
+
@capabilities.add(:ospf)
|
|
103
|
+
|
|
104
|
+
# 注册直连网络
|
|
105
|
+
@interfaces.each do |port, info|
|
|
106
|
+
next if info[:status] == :down
|
|
107
|
+
network = compute_network(info[:cidr])
|
|
108
|
+
@ospf.add_connected(network, port)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@ospf
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# 添加 OSPF 邻居。
|
|
115
|
+
def add_ospf_neighbor(neighbor_rid, port:, cost: 10)
|
|
116
|
+
@ospf&.add_neighbor(neighbor_rid, port: port, cost: cost)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 启用 RIP。
|
|
120
|
+
def enable_rip(split_horizon: true, poison_reverse: false)
|
|
121
|
+
@rip = RIP.new(
|
|
122
|
+
router_id: @name,
|
|
123
|
+
rib: @rib,
|
|
124
|
+
split_horizon: split_horizon,
|
|
125
|
+
poison_reverse: poison_reverse
|
|
126
|
+
)
|
|
127
|
+
@protocols[:rip] = @rip
|
|
128
|
+
@capabilities.add(:rip)
|
|
129
|
+
|
|
130
|
+
# 注册直连网络
|
|
131
|
+
@interfaces.each do |port, info|
|
|
132
|
+
next if info[:status] == :down
|
|
133
|
+
network = compute_network(info[:cidr])
|
|
134
|
+
@rip.add_connected(network, port)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
@rip
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# 添加 RIP 邻居。
|
|
141
|
+
def add_rip_neighbor(neighbor_name, port:, metric: 1)
|
|
142
|
+
@rip&.add_neighbor(neighbor_name, port: port, metric: metric)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# 启用 BGP。
|
|
146
|
+
def enable_bgp(as:, router_id: nil)
|
|
147
|
+
@bgp = BGP.new(as: as, router_id: router_id || @interfaces.values.first&.dig(:ip), rib: @rib)
|
|
148
|
+
@protocols[:bgp] = @bgp
|
|
149
|
+
@capabilities.add(:bgp)
|
|
150
|
+
@bgp
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# 添加 BGP 邻居。
|
|
154
|
+
def add_bgp_peer(addr, remote_as:, port: nil)
|
|
155
|
+
@bgp&.add_peer(addr, remote_as: remote_as, port: port)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# BGP 宣告网络。
|
|
159
|
+
def announce_network(prefix, **opts)
|
|
160
|
+
@bgp&.announce_network(prefix, **opts)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# 安装静态路由。
|
|
164
|
+
def install_static_route(prefix, next_hop, **opts)
|
|
165
|
+
@static_route.install(prefix, next_hop, **opts)
|
|
166
|
+
@rib.add(
|
|
167
|
+
prefix: prefix,
|
|
168
|
+
next_hop: next_hop,
|
|
169
|
+
ifindex: opts[:ifindex],
|
|
170
|
+
protocol: :static,
|
|
171
|
+
metric: opts[:distance] || 1
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# 接收报文。
|
|
176
|
+
# port: 入接口
|
|
177
|
+
# payload: EthernetFrame 或协议消息
|
|
178
|
+
def receive_packet(port:, payload:)
|
|
179
|
+
return unless @interfaces[port] && @interfaces[port][:status] == :up
|
|
180
|
+
|
|
181
|
+
case payload
|
|
182
|
+
when EthernetFrame
|
|
183
|
+
handle_frame(port, payload)
|
|
184
|
+
when IPPacket
|
|
185
|
+
handle_ip_packet(port, payload)
|
|
186
|
+
when RIPUpdate
|
|
187
|
+
@rip&.receive_update(from: payload.from, port: port, routes: payload.routes)
|
|
188
|
+
when LSA
|
|
189
|
+
@ospf&.receive_lsa(port: port, lsa: payload)
|
|
190
|
+
when BGPUpdate
|
|
191
|
+
@bgp&.receive_update(from: payload.from, **{ nlri: payload.nlri, withdrawn: payload.withdrawn, attributes: payload.attributes })
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# 生成要发送的协议消息。
|
|
196
|
+
# 返回 [{port:, payload:}] 列表
|
|
197
|
+
def announce
|
|
198
|
+
messages = []
|
|
199
|
+
|
|
200
|
+
if @rip
|
|
201
|
+
@rip.advertise_all.each do |neighbor, routes|
|
|
202
|
+
next if routes.empty?
|
|
203
|
+
port = @rip.neighbors.dig(neighbor, :port)
|
|
204
|
+
messages << { port: port, payload: RIPUpdate.new(from: @name, routes: routes) } if port
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if @ospf && @ospf.my_lsa
|
|
209
|
+
@ospf.neighbors.each do |rid, info|
|
|
210
|
+
messages << { port: info[:port], payload: @ospf.my_lsa }
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
if @bgp
|
|
215
|
+
@bgp.peers.each do |addr, info|
|
|
216
|
+
next if info[:state] != :established
|
|
217
|
+
adv = @bgp.advertise(addr)
|
|
218
|
+
next unless adv
|
|
219
|
+
messages << {
|
|
220
|
+
port: info[:port],
|
|
221
|
+
payload: BGPUpdate.new(
|
|
222
|
+
from: addr,
|
|
223
|
+
nlri: adv[:nlri],
|
|
224
|
+
withdrawn: [],
|
|
225
|
+
attributes: adv[:attributes]
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
messages
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# 由 engine 驱动的时钟推进。
|
|
235
|
+
def tick(now:)
|
|
236
|
+
@current_time = now
|
|
237
|
+
@protocols.each_value { |p| p.tick(now: now) if p.respond_to?(:tick) }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# 转发 IP 报文。
|
|
241
|
+
# 返回 [{port:, payload:}] 或 nil(丢弃)。
|
|
242
|
+
def forward_ip(ip_packet)
|
|
243
|
+
return nil if ip_packet.expired?
|
|
244
|
+
|
|
245
|
+
route = @rib.lookup(ip_packet.dst)
|
|
246
|
+
return nil unless route # 无路由,丢弃
|
|
247
|
+
|
|
248
|
+
ip_packet.decrement_ttl
|
|
249
|
+
return nil if ip_packet.expired?
|
|
250
|
+
|
|
251
|
+
[{ port: route.ifindex, payload: ip_packet }]
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# RIB 快照。
|
|
255
|
+
def rib_dump
|
|
256
|
+
@rib.to_h
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# 协议表快照。
|
|
260
|
+
def protocol_dump
|
|
261
|
+
result = {}
|
|
262
|
+
result[:rip] = @rip&.to_h
|
|
263
|
+
result[:ospf] = @ospf&.lsdb_dump
|
|
264
|
+
result[:bgp] = @bgp&.to_h
|
|
265
|
+
result
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# 能力列表。
|
|
269
|
+
def capable?(cap)
|
|
270
|
+
@capabilities.include?(cap)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# 设备信息。
|
|
274
|
+
def to_h
|
|
275
|
+
{
|
|
276
|
+
name: @name,
|
|
277
|
+
type: :router,
|
|
278
|
+
interfaces: @interfaces,
|
|
279
|
+
capabilities: @capabilities.to_a,
|
|
280
|
+
rib_size: @rib.size
|
|
281
|
+
}
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# 构建(初始化后调用)。
|
|
285
|
+
def build
|
|
286
|
+
# 触发初始 LSA/路由通告
|
|
287
|
+
@protocols.each_value do |p|
|
|
288
|
+
p.originate_lsa if p.is_a?(OSPF)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
private
|
|
293
|
+
|
|
294
|
+
def handle_frame(port, frame)
|
|
295
|
+
# 如果是 IP 报文,执行路由转发
|
|
296
|
+
if frame.ip?
|
|
297
|
+
handle_ip_packet(port, frame.payload)
|
|
298
|
+
elsif frame.arp?
|
|
299
|
+
# 简化:不处理 ARP
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def handle_ip_packet(port, packet)
|
|
304
|
+
# 检查是否是给本设备的
|
|
305
|
+
if is_local_address?(packet.dst)
|
|
306
|
+
# 本地接收,不转发
|
|
307
|
+
return
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# 转发
|
|
311
|
+
forward_ip(packet)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def is_local_address?(ip)
|
|
315
|
+
@interfaces.each_value do |info|
|
|
316
|
+
return true if info[:ip] == ip.to_s
|
|
317
|
+
end
|
|
318
|
+
false
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def compute_network(cidr)
|
|
322
|
+
ip_str, mask_str = cidr.split('/')
|
|
323
|
+
ip = IPv4.new(ip_str)
|
|
324
|
+
mask = IPv4Mask.number(mask_str.to_i)
|
|
325
|
+
ip.network_with(mask).to_s + "/#{mask_str}"
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# 场景加载器:YAML DSL 描述拓扑 + 协议启用 + 流量计划,输出可执行场景对象。
|
|
5
|
+
#
|
|
6
|
+
# YAML 格式示例:
|
|
7
|
+
# ---
|
|
8
|
+
# topology:
|
|
9
|
+
# routers:
|
|
10
|
+
# R1:
|
|
11
|
+
# interfaces:
|
|
12
|
+
# Gi0/0: 10.0.0.1/24
|
|
13
|
+
# Gi0/1: 10.0.1.1/24
|
|
14
|
+
# ospf:
|
|
15
|
+
# router_id: 1.1.1.1
|
|
16
|
+
# R2:
|
|
17
|
+
# interfaces:
|
|
18
|
+
# Gi0/0: 10.0.0.2/24
|
|
19
|
+
# Gi0/1: 10.0.2.1/24
|
|
20
|
+
# ospf:
|
|
21
|
+
# router_id: 2.2.2.2
|
|
22
|
+
# switches:
|
|
23
|
+
# S1:
|
|
24
|
+
# ports: [Gi0/1, Gi0/2, Gi0/3]
|
|
25
|
+
# links:
|
|
26
|
+
# - [R1:Gi0/0, R2:Gi0/0, {bandwidth: 100000, delay: 10}]
|
|
27
|
+
# - [R1:Gi0/1, S1:Gi0/1]
|
|
28
|
+
# hosts:
|
|
29
|
+
# H1:
|
|
30
|
+
# ip: 10.0.2.2/24
|
|
31
|
+
# gateway: 10.0.2.1
|
|
32
|
+
# traffic:
|
|
33
|
+
# - type: ping
|
|
34
|
+
# from: R1
|
|
35
|
+
# to: 10.0.0.2
|
|
36
|
+
# at: 10
|
|
37
|
+
# - type: link_down
|
|
38
|
+
# device: R1
|
|
39
|
+
# port: Gi0/0
|
|
40
|
+
# at: 50
|
|
41
|
+
# expect:
|
|
42
|
+
# - rib_converged: [R1, R2]
|
|
43
|
+
# - no_loss: true
|
|
44
|
+
|
|
45
|
+
require 'yaml'
|
|
46
|
+
|
|
47
|
+
module NetworkInfraUtility
|
|
48
|
+
module SimLab
|
|
49
|
+
class Scenario
|
|
50
|
+
attr_reader :config
|
|
51
|
+
|
|
52
|
+
def initialize(config = {})
|
|
53
|
+
@config = config
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 从 YAML 文件加载。
|
|
57
|
+
def self.load(file_path)
|
|
58
|
+
content = File.read(file_path)
|
|
59
|
+
config = YAML.safe_load(content, permitted_classes: [Symbol])
|
|
60
|
+
new(config)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 从 Hash 构建。
|
|
64
|
+
def self.from_hash(config)
|
|
65
|
+
new(config)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# 将场景应用到仿真对象。
|
|
69
|
+
def apply_to(simulation)
|
|
70
|
+
build_topology(simulation)
|
|
71
|
+
build_traffic(simulation)
|
|
72
|
+
build_expectations(simulation)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def build_topology(simulation)
|
|
78
|
+
topo = simulation.topology
|
|
79
|
+
|
|
80
|
+
# 路由器
|
|
81
|
+
config.dig('topology', 'routers')&.each do |name, props|
|
|
82
|
+
router = topo.add_router(name, interfaces: props['interfaces'] || {})
|
|
83
|
+
|
|
84
|
+
if props['ospf']
|
|
85
|
+
router.enable_ospf(router_id: props['ospf']['router_id'])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if props['rip']
|
|
89
|
+
router.enable_rip
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if props['bgp']
|
|
93
|
+
router.enable_bgp(as: props['bgp']['as'], router_id: props['bgp']['router_id'])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# 静态路由
|
|
97
|
+
props['static_routes']&.each do |sr|
|
|
98
|
+
router.install_static_route(sr['prefix'], sr['next_hop'])
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# 交换机
|
|
103
|
+
config.dig('topology', 'switches')&.each do |name, props|
|
|
104
|
+
sw = topo.add_switch(name, ports: props['ports'] || [])
|
|
105
|
+
|
|
106
|
+
if props['access_ports']
|
|
107
|
+
props['access_ports'].each do |port, pvid|
|
|
108
|
+
sw.add_access_port(port, pvid: pvid)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if props['trunk_ports']
|
|
113
|
+
props['trunk_ports'].each do |port, cfg|
|
|
114
|
+
sw.add_trunk_port(port, allowed: cfg['allowed'], native: cfg['native'])
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if props['stp']
|
|
119
|
+
sw.enable_stp(bridge_id: props['stp']['bridge_id'])
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# 主机
|
|
124
|
+
config.dig('topology', 'hosts')&.each do |name, props|
|
|
125
|
+
topo.add_host(name, ip: props['ip'], gateway: props['gateway'])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# 链路
|
|
129
|
+
config.dig('topology', 'links')&.each do |link|
|
|
130
|
+
ep_a, ep_b, opts = link
|
|
131
|
+
opts ||= {}
|
|
132
|
+
topo.add_link(ep_a, ep_b,
|
|
133
|
+
bandwidth: opts['bandwidth'] || 1_000_000,
|
|
134
|
+
delay: opts['delay'] || 0,
|
|
135
|
+
cost: opts['cost'] || 10
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# 协议邻居配置
|
|
140
|
+
config.dig('protocols')&.each do |proto_name, proto_cfg|
|
|
141
|
+
case proto_name
|
|
142
|
+
when 'ospf'
|
|
143
|
+
proto_cfg['neighbors']&.each do |nb|
|
|
144
|
+
router = topo.devices[nb['router']]
|
|
145
|
+
next unless router && router.capable?(:ospf)
|
|
146
|
+
router.add_ospf_neighbor(nb['neighbor_rid'], port: nb['port'], cost: nb['cost'] || 10)
|
|
147
|
+
end
|
|
148
|
+
when 'rip'
|
|
149
|
+
proto_cfg['neighbors']&.each do |nb|
|
|
150
|
+
router = topo.devices[nb['router']]
|
|
151
|
+
next unless router && router.capable?(:rip)
|
|
152
|
+
router.add_rip_neighbor(nb['neighbor'], port: nb['port'])
|
|
153
|
+
end
|
|
154
|
+
when 'bgp'
|
|
155
|
+
proto_cfg['peers']&.each do |peer|
|
|
156
|
+
router = topo.devices[peer['router']]
|
|
157
|
+
next unless router && router.capable?(:bgp)
|
|
158
|
+
router.add_bgp_peer(peer['addr'], remote_as: peer['remote_as'], port: peer['port'])
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def build_traffic(simulation)
|
|
165
|
+
traffic = Traffic.new
|
|
166
|
+
|
|
167
|
+
config['traffic']&.each do |event|
|
|
168
|
+
case event['type']
|
|
169
|
+
when 'ping'
|
|
170
|
+
traffic.ping(simulation.engine, simulation.topology,
|
|
171
|
+
from: event['from'], to: event['to'],
|
|
172
|
+
at: event['at'], count: event['count'] || 1)
|
|
173
|
+
when 'link_down'
|
|
174
|
+
traffic.link_down(simulation.engine, simulation.topology,
|
|
175
|
+
event['device'], event['port'], at: event['at'])
|
|
176
|
+
when 'link_up'
|
|
177
|
+
traffic.link_up(simulation.engine, simulation.topology,
|
|
178
|
+
event['device'], event['port'], at: event['at'])
|
|
179
|
+
when 'node_down'
|
|
180
|
+
traffic.node_down(simulation.engine, simulation.topology,
|
|
181
|
+
event['device'], at: event['at'])
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# 周期性协议交换
|
|
186
|
+
interval = config.dig('simulation', 'protocol_interval') || 10
|
|
187
|
+
until_time = config.dig('simulation', 'until') || 100
|
|
188
|
+
traffic.periodic_exchange(simulation.engine, simulation.topology,
|
|
189
|
+
interval: interval, until_time: until_time)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def build_expectations(simulation)
|
|
193
|
+
# 预期断言(简化:记录到 simulation)
|
|
194
|
+
@expectations = config['expect'] || []
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
attr_reader :expectations
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# 虚拟交换机:组合 MACTable + Forward + VLAN + STP。
|
|
5
|
+
#
|
|
6
|
+
# 数据平面逐帧处理流水线:
|
|
7
|
+
# 入端口 → VLAN 处理 → MAC 学习 → STP 状态检查 → 转发决策
|
|
8
|
+
#
|
|
9
|
+
# 与 router 的区别:router 跑控制平面协议,switch 默认只转发。
|
|
10
|
+
#
|
|
11
|
+
# 用法:
|
|
12
|
+
# sw = Switch.new(name: "S1", ports: ["Gi0/1", "Gi0/2", "Gi0/3"])
|
|
13
|
+
# sw.add_access_port("Gi0/1", pvid: 10)
|
|
14
|
+
# sw.add_trunk_port("Gi0/24", allowed: [10, 20], native: 1)
|
|
15
|
+
# sw.receive_frame(port: "Gi0/1", frame: frame)
|
|
16
|
+
|
|
17
|
+
require_relative '../../../support/switching/mac_table'
|
|
18
|
+
require_relative '../../../support/switching/frame_forward'
|
|
19
|
+
require_relative '../../../support/switching/vlan'
|
|
20
|
+
require_relative '../../../support/switching/stp'
|
|
21
|
+
require_relative '../../../support/basic/packet'
|
|
22
|
+
|
|
23
|
+
module NetworkInfraUtility
|
|
24
|
+
module SimLab
|
|
25
|
+
module Device
|
|
26
|
+
class Switch
|
|
27
|
+
attr_reader :name, :ports, :mac_table, :vlan, :stp, :forward
|
|
28
|
+
|
|
29
|
+
def initialize(name:, ports: [], **opts)
|
|
30
|
+
@name = name
|
|
31
|
+
@ports = {} # port_name => {status:}
|
|
32
|
+
@mac_table = MACTable.new(aging_time: opts[:aging_time] || 300)
|
|
33
|
+
@forward = Forward.new
|
|
34
|
+
@vlan = VLAN.new
|
|
35
|
+
@stp = nil
|
|
36
|
+
@current_time = 0
|
|
37
|
+
|
|
38
|
+
ports.each { |p| add_port(p) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 添加端口。
|
|
42
|
+
def add_port(port)
|
|
43
|
+
@ports[port] = { status: :up }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 端口 down。
|
|
47
|
+
def port_down(port)
|
|
48
|
+
return unless @ports[port]
|
|
49
|
+
@ports[port][:status] = :down
|
|
50
|
+
@mac_table.remove_by_port(port)
|
|
51
|
+
@stp&.remove_port(port) if @stp
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# 端口 up。
|
|
55
|
+
def port_up(port)
|
|
56
|
+
return unless @ports[port]
|
|
57
|
+
@ports[port][:status] = :up
|
|
58
|
+
@stp&.add_port(port) if @stp
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 添加 access 端口。
|
|
62
|
+
def add_access_port(port, pvid:)
|
|
63
|
+
add_port(port) unless @ports[port]
|
|
64
|
+
@vlan.add_access_port(port, pvid: pvid)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 添加 trunk 端口。
|
|
68
|
+
def add_trunk_port(port, allowed:, native: nil)
|
|
69
|
+
add_port(port) unless @ports[port]
|
|
70
|
+
@vlan.add_trunk_port(port, allowed: allowed, native: native)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# 启用 STP。
|
|
74
|
+
def enable_stp(bridge_id:, bridge_priority: STP::DEFAULT_PRIORITY)
|
|
75
|
+
@stp = STP.new(bridge_id: bridge_id, bridge_priority: bridge_priority)
|
|
76
|
+
@ports.each_key { |p| @stp.add_port(p, cost: 20000) }
|
|
77
|
+
@stp
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# 添加 STP 端口。
|
|
81
|
+
def add_stp_port(port, cost: 20000)
|
|
82
|
+
@stp&.add_port(port, cost: cost)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 接收帧。
|
|
86
|
+
# port: 入端口
|
|
87
|
+
# frame: EthernetFrame
|
|
88
|
+
# 返回 [{port:, frame:}] 列表(需要从哪些端口发出什么帧)
|
|
89
|
+
def receive_frame(port:, frame:)
|
|
90
|
+
return [] unless @ports[port] && @ports[port][:status] == :up
|
|
91
|
+
|
|
92
|
+
# STP 状态检查
|
|
93
|
+
if @stp
|
|
94
|
+
port_state = @stp.states[port]
|
|
95
|
+
# Blocking 状态不处理数据帧
|
|
96
|
+
return [] if port_state == :blocking
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# VLAN 入口处理
|
|
100
|
+
tagged_frame = @vlan.ingress(frame, port: port)
|
|
101
|
+
return [] unless tagged_frame # VLAN 检查失败,丢弃
|
|
102
|
+
|
|
103
|
+
# MAC 学习
|
|
104
|
+
@mac_table.learn(frame.src_mac, port: port, now: @current_time)
|
|
105
|
+
|
|
106
|
+
# 获取同 VLAN 端口列表
|
|
107
|
+
vlan_id = tagged_frame.vlan_id
|
|
108
|
+
vlan_ports = @vlan.ports_for_vlan(vlan_id)
|
|
109
|
+
|
|
110
|
+
# 转发决策
|
|
111
|
+
decision = @forward.decide(
|
|
112
|
+
tagged_frame,
|
|
113
|
+
in_port: port,
|
|
114
|
+
mac_table: @mac_table,
|
|
115
|
+
vlan_ports: vlan_ports
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
case decision.action
|
|
119
|
+
when :forward, :flood
|
|
120
|
+
decision.ports.map do |out_port|
|
|
121
|
+
next unless @ports[out_port] && @ports[out_port][:status] == :up
|
|
122
|
+
# STP 检查:非指定端口/根端口不转发(简化:只检查 forwarding 状态)
|
|
123
|
+
if @stp && @stp.states[out_port] != :forwarding
|
|
124
|
+
next
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# VLAN 出口处理
|
|
128
|
+
out_frame = @vlan.egress(tagged_frame, port: out_port)
|
|
129
|
+
next unless out_frame
|
|
130
|
+
|
|
131
|
+
{ port: out_port, frame: out_frame }
|
|
132
|
+
end.compact
|
|
133
|
+
when :drop
|
|
134
|
+
[]
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# 由 engine 驱动的时钟推进。
|
|
139
|
+
def tick(now:)
|
|
140
|
+
@current_time = now
|
|
141
|
+
|
|
142
|
+
# MAC 表老化
|
|
143
|
+
@mac_table.age_out(now: now)
|
|
144
|
+
|
|
145
|
+
# STP 状态机推进
|
|
146
|
+
@stp&.tick(now: now)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# MAC 表快照。
|
|
150
|
+
def mac_dump
|
|
151
|
+
@mac_table.to_h
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# VLAN 配置快照。
|
|
155
|
+
def vlan_dump
|
|
156
|
+
@vlan.to_h
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# STP 快照。
|
|
160
|
+
def stp_dump
|
|
161
|
+
@stp&.to_h
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# 设备信息。
|
|
165
|
+
def to_h
|
|
166
|
+
{
|
|
167
|
+
name: @name,
|
|
168
|
+
type: :switch,
|
|
169
|
+
ports: @ports.keys,
|
|
170
|
+
mac_entries: @mac_table.size,
|
|
171
|
+
stp_enabled: !@stp.nil?
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# 构建。
|
|
176
|
+
def build
|
|
177
|
+
@stp&.recompute_roles
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|