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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/CHANGELOG.md +31 -0
  4. data/GUIDE.md +201 -212
  5. data/README.md +1 -6
  6. data/Rakefile +1 -1
  7. data/bin/gen-get +144 -0
  8. data/bin/geo-api +40 -1
  9. data/bin/geo-get +18 -36
  10. data/bin/ngeo-get +253 -0
  11. data/bin/packbit +227 -0
  12. data/bin/probe +134 -0
  13. data/document/reference/DNS/346/212/200/346/234/257/346/226/207/346/241/243.md +912 -0
  14. data/network-infra-utility.gemspec +6 -4
  15. data/network.rb +19 -1
  16. data/service/geodb/GeoAPI.md +64 -0
  17. data/service/geodb/api.rb +77 -19
  18. data/service/geoquery/GeoQuery.md +309 -0
  19. data/service/geoquery/cache.rb +78 -0
  20. data/service/geoquery/geo_cache.rb +233 -0
  21. data/service/geoquery/geoquery.rb +327 -0
  22. data/service/geoquery/local.rb +151 -0
  23. data/service/geoquery/merge.rb +52 -0
  24. data/service/geoquery/normalize.rb +102 -0
  25. data/service/geoquery/online.rb +136 -0
  26. data/service/simlab/SimLab.md +140 -0
  27. data/service/simlab/bin/simctl +152 -0
  28. data/service/simlab/lib/simlab.rb +124 -0
  29. data/service/simlab/lib/simlab_engine.rb +146 -0
  30. data/service/simlab/lib/simlab_observer.rb +176 -0
  31. data/service/simlab/lib/simlab_router.rb +330 -0
  32. data/service/simlab/lib/simlab_scenario.rb +200 -0
  33. data/service/simlab/lib/simlab_switch.rb +182 -0
  34. data/service/simlab/lib/simlab_topology.rb +196 -0
  35. data/service/simlab/lib/simlab_traffic.rb +167 -0
  36. data/support/basic/packet.rb +167 -0
  37. data/support/routing/bgp.rb +279 -0
  38. data/support/routing/lpm_trie.rb +145 -0
  39. data/support/routing/ospf.rb +243 -0
  40. data/support/routing/prefix.rb +155 -0
  41. data/support/routing/rib.rb +167 -0
  42. data/support/routing/rip.rb +223 -0
  43. data/support/routing/static_route.rb +102 -0
  44. data/support/switching/frame_forward.rb +87 -0
  45. data/support/switching/mac_table.rb +127 -0
  46. data/support/switching/stp.rb +230 -0
  47. data/support/switching/vlan.rb +155 -0
  48. data/tool/packbit/README.md +136 -0
  49. data/tool/packbit/config/five-tuple.yml +25 -0
  50. data/tool/packbit/config/flow.yml +17 -0
  51. data/tool/packbit/config/packbit.yml +41 -0
  52. data/tool/packbit/erl/packbit.erl +588 -0
  53. data/tool/probe/README.md +88 -0
  54. data/tool/probe/lib/probe.rb +54 -0
  55. data/tool/probe/lib/probe_base.rb +98 -0
  56. data/tool/probe/lib/probe_dns.rb +64 -0
  57. data/tool/probe/lib/probe_icmp.rb +71 -0
  58. data/tool/probe/lib/probe_ntp.rb +51 -0
  59. data/tool/probe/lib/probe_radius.rb +58 -0
  60. data/tool/probe/lib/probe_registry.rb +44 -0
  61. data/tool/probe/lib/probe_result.rb +75 -0
  62. data/tool/probe/lib/probe_snmp.rb +114 -0
  63. data/tool/probe/lib/probe_tcp.rb +82 -0
  64. data/tool/probe/lib/probe_version.rb +7 -0
  65. data/version.rb +1 -1
  66. 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