dsh-ensp 0.1.3 → 0.1.7

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.
@@ -0,0 +1,1019 @@
1
+ /**
2
+ * Topology blueprints for the scenario presets.
3
+ *
4
+ * A preset describes *what to configure and in what order*. This module adds the
5
+ * piece that was missing to actually run one: a device list, the links between
6
+ * them, and an addressing plan. `ensp preset <id> --topo out.topo` turns a
7
+ * blueprint into a `.topo` file eNSP can open, so a reader no longer has to
8
+ * rebuild the drawing by hand before the steps make sense.
9
+ *
10
+ * What a blueprint deliberately does **not** contain is a command script. The
11
+ * scenarios stay skeletons (see `presets.mjs` for why), and the addressing plan
12
+ * is a table rather than a paste-ready block. You still type the commands; the
13
+ * lab just removes the geometry and the IP arithmetic.
14
+ *
15
+ * Ports in `links` and `addressing` are VRP names (`GigabitEthernet0/0/1`), the
16
+ * same vocabulary used on the CLI, not eNSP's file-level indices.
17
+ *
18
+ * @module dsh-ensp/labs
19
+ */
20
+
21
+ import { buildTopology, autoLayout } from './build.mjs'
22
+
23
+ /**
24
+ * Shape:
25
+ *
26
+ * ```
27
+ * {
28
+ * devices: [ ['SW1', 'S5700'], { name, model, ip, mask, gateway } ],
29
+ * links: [ ['SW1', 'GigabitEthernet0/0/1', 'PC1', 'Ethernet0/0/1'] ],
30
+ * addressing: [ { device, port, ip, note? } ],
31
+ * positions?: { DEVICE: [x, y] },
32
+ * }
33
+ * ```
34
+ *
35
+ * A PC entry carries its address inline because eNSP stores it in the device
36
+ * settings; every other device gets its address from `addressing`, which is also
37
+ * what the reader copies from.
38
+ */
39
+ const lab = (devices, links, addressing, positions) => ({ devices, links, addressing, positions })
40
+
41
+ const PC = (name, ip, mask, gateway) => ({ name, model: 'PC', ip, mask, gateway })
42
+
43
+ export const LABS = {
44
+ // ------------------------------------------------------------------ switching
45
+ 'vlan-access': lab(
46
+ [['SW1', 'S5700'], PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
47
+ PC('PC2', '192.168.20.1', '255.255.255.0', '192.168.20.254'),
48
+ PC('PC3', '192.168.20.2', '255.255.255.0', '192.168.20.254')],
49
+ [
50
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
51
+ ['PC2', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/2'],
52
+ ['PC3', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/3'],
53
+ ],
54
+ [],
55
+ ),
56
+
57
+ 'trunk-interconnect': lab(
58
+ [['SW1', 'S5700'], ['SW2', 'S5700'],
59
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
60
+ PC('PC2', '192.168.10.2', '255.255.255.0', '192.168.10.254')],
61
+ [
62
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
63
+ ['PC2', 'Ethernet0/0/1', 'SW2', 'GigabitEthernet0/0/1'],
64
+ ['SW1', 'GigabitEthernet0/0/24', 'SW2', 'GigabitEthernet0/0/24'],
65
+ ],
66
+ [],
67
+ ),
68
+
69
+ 'vlanif-routing': lab(
70
+ [['SW1', 'S5700'], ['SW2', 'S5700'],
71
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
72
+ PC('PC2', '192.168.20.1', '255.255.255.0', '192.168.20.254')],
73
+ [
74
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
75
+ ['PC2', 'Ethernet0/0/1', 'SW2', 'GigabitEthernet0/0/1'],
76
+ ['SW1', 'GigabitEthernet0/0/24', 'SW2', 'GigabitEthernet0/0/24'],
77
+ ],
78
+ [
79
+ { device: 'SW1', port: 'Vlanif10', ip: '192.168.10.254/24', note: 'VLAN10 网关' },
80
+ { device: 'SW2', port: 'Vlanif20', ip: '192.168.20.254/24', note: 'VLAN20 网关' },
81
+ { device: 'SW1', port: 'Vlanif200', ip: '10.0.12.1/24', note: '互联 VLAN200' },
82
+ { device: 'SW2', port: 'Vlanif200', ip: '10.0.12.2/24', note: '互联 VLAN200' },
83
+ ],
84
+ ),
85
+
86
+ 'stp-rstp': lab(
87
+ [['SW1', 'S5700'], ['SW2', 'S5700'], ['SW3', 'S5700'],
88
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
89
+ PC('PC2', '192.168.10.2', '255.255.255.0', '192.168.10.254')],
90
+ [
91
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
92
+ ['PC2', 'Ethernet0/0/1', 'SW2', 'GigabitEthernet0/0/1'],
93
+ ['SW1', 'GigabitEthernet0/0/23', 'SW2', 'GigabitEthernet0/0/23'],
94
+ ['SW2', 'GigabitEthernet0/0/24', 'SW3', 'GigabitEthernet0/0/23'],
95
+ ['SW3', 'GigabitEthernet0/0/24', 'SW1', 'GigabitEthernet0/0/24'],
96
+ ],
97
+ [
98
+ { device: 'SW1', port: 'Vlanif10', ip: '192.168.10.251/24', note: '管理地址' },
99
+ { device: 'SW2', port: 'Vlanif10', ip: '192.168.10.252/24', note: '管理地址' },
100
+ { device: 'SW3', port: 'Vlanif10', ip: '192.168.10.253/24', note: '管理地址' },
101
+ ],
102
+ ),
103
+
104
+ 'mstp-load-balance': lab(
105
+ [['CORE1', 'S5700'], ['CORE2', 'S5700'], ['ACC1', 'S5700'], ['ACC2', 'S5700'],
106
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
107
+ PC('PC2', '192.168.20.1', '255.255.255.0', '192.168.20.254')],
108
+ [
109
+ ['ACC1', 'GigabitEthernet0/0/1', 'PC1', 'Ethernet0/0/1'],
110
+ ['ACC2', 'GigabitEthernet0/0/1', 'PC2', 'Ethernet0/0/1'],
111
+ ['ACC1', 'GigabitEthernet0/0/23', 'CORE1', 'GigabitEthernet0/0/1'],
112
+ ['ACC1', 'GigabitEthernet0/0/24', 'CORE2', 'GigabitEthernet0/0/1'],
113
+ ['ACC2', 'GigabitEthernet0/0/23', 'CORE1', 'GigabitEthernet0/0/2'],
114
+ ['ACC2', 'GigabitEthernet0/0/24', 'CORE2', 'GigabitEthernet0/0/2'],
115
+ ],
116
+ [
117
+ { device: 'CORE1', port: 'Vlanif10', ip: '192.168.10.252/24', note: 'VLAN10 网关(实例1 主)' },
118
+ { device: 'CORE1', port: 'Vlanif20', ip: '192.168.20.253/24', note: 'VLAN20 网关(实例2 备)' },
119
+ { device: 'CORE2', port: 'Vlanif10', ip: '192.168.10.253/24', note: 'VLAN10 网关(实例1 备)' },
120
+ { device: 'CORE2', port: 'Vlanif20', ip: '192.168.20.252/24', note: 'VLAN20 网关(实例2 主)' },
121
+ ],
122
+ ),
123
+
124
+ 'link-aggregation': lab(
125
+ [['SW1', 'S5700'], ['SW2', 'S5700'],
126
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
127
+ PC('PC2', '192.168.10.2', '255.255.255.0', '192.168.10.254')],
128
+ [
129
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
130
+ ['PC2', 'Ethernet0/0/1', 'SW2', 'GigabitEthernet0/0/1'],
131
+ ['SW1', 'GigabitEthernet0/0/23', 'SW2', 'GigabitEthernet0/0/23'],
132
+ ['SW1', 'GigabitEthernet0/0/24', 'SW2', 'GigabitEthernet0/0/24'],
133
+ ],
134
+ [],
135
+ ),
136
+
137
+ 'port-security': lab(
138
+ [['SW1', 'S5700'],
139
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
140
+ PC('PC2', '192.168.10.2', '255.255.255.0', '192.168.10.254')],
141
+ [
142
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
143
+ ['PC2', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/2'],
144
+ ],
145
+ [],
146
+ ),
147
+
148
+ 'port-isolation': lab(
149
+ [['SW1', 'S5700'],
150
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
151
+ PC('PC2', '192.168.10.2', '255.255.255.0', '192.168.10.254'),
152
+ PC('PC3', '192.168.10.3', '255.255.255.0', '192.168.10.254')],
153
+ [
154
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
155
+ ['PC2', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/2'],
156
+ ['PC3', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/3'],
157
+ ],
158
+ [],
159
+ ),
160
+
161
+ // -------------------------------------------------------------------- routing
162
+ 'static-route': lab(
163
+ [['R1', 'AR2220'], ['R2', 'AR2220'],
164
+ PC('PC1', '192.168.1.1', '255.255.255.0', '192.168.1.254'),
165
+ PC('PC2', '192.168.2.1', '255.255.255.0', '192.168.2.254')],
166
+ [
167
+ ['PC1', 'Ethernet0/0/1', 'R1', 'GigabitEthernet0/0/1'],
168
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
169
+ ['R2', 'GigabitEthernet0/0/1', 'PC2', 'Ethernet0/0/1'],
170
+ ],
171
+ [
172
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '192.168.1.254/24', note: 'PC1 网关' },
173
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: '互联' },
174
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24', note: '互联' },
175
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '192.168.2.254/24', note: 'PC2 网关' },
176
+ ],
177
+ ),
178
+
179
+ 'ospf-single': lab(
180
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220']],
181
+ [
182
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
183
+ ['R2', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
184
+ ['R1', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/1'],
185
+ ],
186
+ [
187
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: '互联 R2' },
188
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24' },
189
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.23.2/24', note: '互联 R3' },
190
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.23.3/24' },
191
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '10.0.13.1/24', note: '互联 R3' },
192
+ { device: 'R3', port: 'GigabitEthernet0/0/1', ip: '10.0.13.3/24' },
193
+ { device: 'R1', port: 'LoopBack0', ip: '1.1.1.1/32', note: 'router-id 与业务网段' },
194
+ { device: 'R2', port: 'LoopBack0', ip: '2.2.2.2/32' },
195
+ { device: 'R3', port: 'LoopBack0', ip: '3.3.3.3/32' },
196
+ ],
197
+ ),
198
+
199
+ 'ospf-multi-area': lab(
200
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220'], ['R4', 'AR2220']],
201
+ [
202
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
203
+ ['R1', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
204
+ ['R2', 'GigabitEthernet0/0/1', 'R4', 'GigabitEthernet0/0/0'],
205
+ ],
206
+ [
207
+ { device: 'R1', port: 'LoopBack0', ip: '1.1.1.1/32', note: '在 area 0' },
208
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: 'area 0' },
209
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24', note: 'area 0' },
210
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '10.0.13.1/24', note: 'area 1(R1 为 ABR)' },
211
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.13.3/24', note: 'area 1' },
212
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.24.2/24', note: 'area 2(R2 为 ABR)' },
213
+ { device: 'R4', port: 'GigabitEthernet0/0/0', ip: '10.0.24.4/24', note: 'area 2' },
214
+ ],
215
+ ),
216
+
217
+ 'ospf-stub-nssa': lab(
218
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220'], ['R4', 'AR2220']],
219
+ [
220
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
221
+ ['R1', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
222
+ ['R1', 'GigabitEthernet0/0/2', 'R4', 'GigabitEthernet0/0/0'],
223
+ ],
224
+ [
225
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: 'area 0' },
226
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24', note: 'area 0' },
227
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '10.0.13.1/24', note: 'area 1 → stub' },
228
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.13.3/24', note: 'area 1' },
229
+ { device: 'R1', port: 'GigabitEthernet0/0/2', ip: '10.0.14.1/24', note: 'area 2 → nssa' },
230
+ { device: 'R4', port: 'GigabitEthernet0/0/0', ip: '10.0.14.4/24', note: 'area 2' },
231
+ ],
232
+ ),
233
+
234
+ 'ospf-virtual-link': lab(
235
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220']],
236
+ [
237
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
238
+ ['R2', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
239
+ ],
240
+ [
241
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: 'area 0' },
242
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24', note: 'area 0' },
243
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.23.2/24', note: 'area 1(transit)' },
244
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.23.3/24', note: 'area 1' },
245
+ { device: 'R3', port: 'LoopBack0', ip: '3.3.3.3/32', note: '虚拟链路的两端 router-id' },
246
+ ],
247
+ ),
248
+
249
+ 'bgp-ebgp-ibgp': lab(
250
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220']],
251
+ [
252
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
253
+ ['R2', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
254
+ ],
255
+ [
256
+ { device: 'R1', port: 'LoopBack0', ip: '1.1.1.1/32', note: 'AS 100' },
257
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: 'AS 100' },
258
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24', note: 'AS 100' },
259
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.23.2/24', note: 'AS 100 → EBGP' },
260
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.23.3/24', note: 'AS 200' },
261
+ { device: 'R3', port: 'LoopBack0', ip: '3.3.3.3/32', note: 'AS 200 业务网段' },
262
+ ],
263
+ ),
264
+
265
+ 'bgp-route-reflector': lab(
266
+ [['RR', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220'], ['EXT', 'AR2220']],
267
+ [
268
+ ['RR', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
269
+ ['RR', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
270
+ ['R2', 'GigabitEthernet0/0/1', 'EXT', 'GigabitEthernet0/0/0'],
271
+ ],
272
+ [
273
+ { device: 'RR', port: 'LoopBack0', ip: '10.1.1.1/32', note: 'AS 100 内 reflector' },
274
+ { device: 'RR', port: 'GigabitEthernet0/0/0', ip: '10.1.12.1/24', note: 'AS 100' },
275
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.1.12.2/24', note: 'AS 100 client' },
276
+ { device: 'RR', port: 'GigabitEthernet0/0/1', ip: '10.1.13.1/24', note: 'AS 100' },
277
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.1.13.3/24', note: 'AS 100 client' },
278
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.1.24.2/24', note: 'AS 100 → EBGP' },
279
+ { device: 'EXT', port: 'GigabitEthernet0/0/0', ip: '10.1.24.4/24', note: 'AS 200' },
280
+ ],
281
+ ),
282
+
283
+ 'route-policy-pbr': lab(
284
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220'],
285
+ PC('PC1', '192.168.1.1', '255.255.255.0', '192.168.1.254')],
286
+ [
287
+ ['PC1', 'Ethernet0/0/1', 'R1', 'GigabitEthernet0/0/2'],
288
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
289
+ ['R1', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
290
+ ],
291
+ [
292
+ { device: 'R1', port: 'GigabitEthernet0/0/2', ip: '192.168.1.254/24', note: 'PC1 网关' },
293
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: '出口 1' },
294
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24' },
295
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '10.0.13.1/24', note: '出口 2' },
296
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.13.3/24' },
297
+ { device: 'R2', port: 'LoopBack0', ip: '2.2.2.2/32', note: '同一目的网段的两个出口' },
298
+ { device: 'R3', port: 'LoopBack0', ip: '2.2.2.2/32', note: '与出口 1 相同的地址,用于观察选路' },
299
+ ],
300
+ ),
301
+
302
+ // --------------------------------------------------------------- IP services
303
+ 'dhcp': lab(
304
+ [['R1', 'AR2220'], ['SW1', 'S5700'],
305
+ PC('PC1', 'dhcp', '255.255.255.0', '192.168.10.254'),
306
+ PC('PC2', 'dhcp', '255.255.255.0', '192.168.10.254')],
307
+ [
308
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
309
+ ['PC2', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/2'],
310
+ ['SW1', 'GigabitEthernet0/0/23', 'R1', 'GigabitEthernet0/0/0'],
311
+ ],
312
+ [
313
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '192.168.10.254/24', note: '地址池网关' },
314
+ { device: 'R1', port: 'LoopBack0', ip: '1.1.1.1/32', note: 'router-id' },
315
+ ],
316
+ ),
317
+
318
+ 'dhcp-snooping': lab(
319
+ [['SW1', 'S5700'], ['R1', 'AR2220'],
320
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254')],
321
+ [
322
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
323
+ ['SW1', 'GigabitEthernet0/0/24', 'R1', 'GigabitEthernet0/0/0'],
324
+ ],
325
+ [
326
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '192.168.10.254/24', note: 'DHCP 服务器' },
327
+ { device: 'SW1', port: 'Vlanif10', ip: '192.168.10.253/24', note: '管理地址' },
328
+ ],
329
+ ),
330
+
331
+ 'nat': lab(
332
+ [['R1', 'AR2220'], ['ISP', 'AR2220'],
333
+ PC('PC1', '192.168.1.1', '255.255.255.0', '192.168.1.254')],
334
+ [
335
+ ['PC1', 'Ethernet0/0/1', 'R1', 'GigabitEthernet0/0/1'],
336
+ ['R1', 'GigabitEthernet0/0/0', 'ISP', 'GigabitEthernet0/0/0'],
337
+ ],
338
+ [
339
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '192.168.1.254/24', note: '内网网关' },
340
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '200.1.1.1/30', note: '公网侧(NAT outside)' },
341
+ { device: 'ISP', port: 'GigabitEthernet0/0/0', ip: '200.1.1.2/30', note: '模拟运营商' },
342
+ { device: 'ISP', port: 'LoopBack0', ip: '8.8.8.8/32', note: '模拟 Internet 目的地址' },
343
+ ],
344
+ ),
345
+
346
+ 'acl-filter': lab(
347
+ [['R1', 'AR2220'], ['SW1', 'S5700'],
348
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
349
+ PC('PC2', '192.168.20.1', '255.255.255.0', '192.168.20.254')],
350
+ [
351
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
352
+ ['PC2', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/2'],
353
+ ['SW1', 'GigabitEthernet0/0/24', 'R1', 'GigabitEthernet0/0/0'],
354
+ ],
355
+ [
356
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '192.168.10.254/24', note: 'PC1 网关,ACL 挂这里' },
357
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '192.168.20.253/24', note: '同接口从地址' },
358
+ ],
359
+ ),
360
+
361
+ 'qos-basics': lab(
362
+ [['R1', 'AR2220'], ['R2', 'AR2220'],
363
+ PC('PC1', '192.168.1.1', '255.255.255.0', '192.168.1.254')],
364
+ [
365
+ ['PC1', 'Ethernet0/0/1', 'R1', 'GigabitEthernet0/0/1'],
366
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
367
+ ],
368
+ [
369
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '192.168.1.254/24', note: '入方向做流分类' },
370
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: '出方向做限速' },
371
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24' },
372
+ ],
373
+ ),
374
+
375
+ // --------------------------------------------------------------- reliability
376
+ 'vrrp-basics': lab(
377
+ [['SW1', 'S5700'], ['SW2', 'S5700'], ['ACC', 'S5700'],
378
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254')],
379
+ [
380
+ ['PC1', 'Ethernet0/0/1', 'ACC', 'GigabitEthernet0/0/1'],
381
+ ['ACC', 'GigabitEthernet0/0/23', 'SW1', 'GigabitEthernet0/0/1'],
382
+ ['ACC', 'GigabitEthernet0/0/24', 'SW2', 'GigabitEthernet0/0/1'],
383
+ ],
384
+ [
385
+ { device: 'SW1', port: 'Vlanif10', ip: '192.168.10.252/24', note: 'VRRP 主,虚拟地址 .254' },
386
+ { device: 'SW2', port: 'Vlanif10', ip: '192.168.10.253/24', note: 'VRRP 备' },
387
+ { device: 'SW1', port: 'Vlanif100', ip: '10.0.100.1/24', note: '上行互联' },
388
+ { device: 'SW2', port: 'Vlanif100', ip: '10.0.100.2/24', note: '上行互联' },
389
+ ],
390
+ ),
391
+
392
+ 'bfd-fast-convergence': lab(
393
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220']],
394
+ [
395
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
396
+ ['R2', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
397
+ ['R1', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/1'],
398
+ ],
399
+ [
400
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24' },
401
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24' },
402
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.23.2/24' },
403
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.23.3/24' },
404
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '10.0.13.1/24' },
405
+ { device: 'R3', port: 'GigabitEthernet0/0/1', ip: '10.0.13.3/24' },
406
+ { device: 'R1', port: 'LoopBack0', ip: '1.1.1.1/32' },
407
+ { device: 'R3', port: 'LoopBack0', ip: '3.3.3.3/32' },
408
+ ],
409
+ ),
410
+
411
+ 'dual-homed-isp': lab(
412
+ [['R1', 'AR2220'], ['ISP1', 'AR2220'], ['ISP2', 'AR2220'],
413
+ PC('PC1', '192.168.1.1', '255.255.255.0', '192.168.1.254')],
414
+ [
415
+ ['PC1', 'Ethernet0/0/1', 'R1', 'GigabitEthernet0/0/2'],
416
+ ['R1', 'GigabitEthernet0/0/0', 'ISP1', 'GigabitEthernet0/0/0'],
417
+ ['R1', 'GigabitEthernet0/0/1', 'ISP2', 'GigabitEthernet0/0/0'],
418
+ ],
419
+ [
420
+ { device: 'R1', port: 'GigabitEthernet0/0/2', ip: '192.168.1.254/24', note: '内网' },
421
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '200.1.1.1/30', note: 'ISP1' },
422
+ { device: 'ISP1', port: 'GigabitEthernet0/0/0', ip: '200.1.1.2/30' },
423
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '201.1.1.1/30', note: 'ISP2' },
424
+ { device: 'ISP2', port: 'GigabitEthernet0/0/0', ip: '201.1.1.2/30' },
425
+ { device: 'ISP1', port: 'LoopBack0', ip: '8.8.8.8/32', note: '仅 ISP1 宣告,用于观察回程选路' },
426
+ ],
427
+ ),
428
+
429
+ 'firewall-zones': lab(
430
+ [['FW1', 'USG6000V'], ['R-TRUST', 'AR2220'], ['R-UNTRUST', 'AR2220'],
431
+ { name: 'SRV1', model: 'Server' }],
432
+ [
433
+ ['R-TRUST', 'GigabitEthernet0/0/0', 'FW1', 'GigabitEthernet1/0/0'],
434
+ ['FW1', 'GigabitEthernet1/0/1', 'R-UNTRUST', 'GigabitEthernet0/0/0'],
435
+ ['FW1', 'GigabitEthernet1/0/2', 'SRV1', 'Ethernet0/0/1'],
436
+ ],
437
+ [
438
+ { device: 'R-TRUST', port: 'GigabitEthernet0/0/0', ip: '10.1.1.254/24', note: 'trust 侧(接防火墙)' },
439
+ { device: 'FW1', port: 'GigabitEthernet1/0/0', ip: '10.1.1.1/24', note: 'trust 区域' },
440
+ { device: 'FW1', port: 'GigabitEthernet1/0/1', ip: '200.1.1.1/24', note: 'untrust 区域' },
441
+ { device: 'R-UNTRUST', port: 'GigabitEthernet0/0/0', ip: '200.1.1.2/24', note: '外网侧' },
442
+ { device: 'FW1', port: 'GigabitEthernet1/0/2', ip: '10.1.40.1/24', note: 'dmz 区域' },
443
+ { device: 'SRV1', port: 'Ethernet0/0/1', ip: '10.1.40.10/24', note: '对外发布的服务器' },
444
+ ],
445
+ ),
446
+
447
+ 'firewall-hrp': lab(
448
+ [['FW1', 'USG6000V'], ['FW2', 'USG6000V'], ['R-UP', 'AR2220'], ['R-DOWN', 'AR2220']],
449
+ [
450
+ ['R-UP', 'GigabitEthernet0/0/0', 'FW1', 'GigabitEthernet1/0/0'],
451
+ ['R-UP', 'GigabitEthernet0/0/1', 'FW2', 'GigabitEthernet1/0/0'],
452
+ ['FW1', 'GigabitEthernet1/0/1', 'R-DOWN', 'GigabitEthernet0/0/0'],
453
+ ['FW2', 'GigabitEthernet1/0/1', 'R-DOWN', 'GigabitEthernet0/0/1'],
454
+ ['FW1', 'GigabitEthernet1/0/2', 'FW2', 'GigabitEthernet1/0/2'],
455
+ ],
456
+ [
457
+ { device: 'FW1', port: 'GigabitEthernet1/0/0', ip: '10.1.250.1/24', note: 'VRRP 组 350,虚拟 .253' },
458
+ { device: 'FW2', port: 'GigabitEthernet1/0/0', ip: '10.1.250.2/24', note: '备机同组' },
459
+ { device: 'FW1', port: 'GigabitEthernet1/0/1', ip: '10.1.249.1/24', note: 'VRRP 组 349,虚拟 .254' },
460
+ { device: 'FW2', port: 'GigabitEthernet1/0/1', ip: '10.1.249.2/24' },
461
+ { device: 'FW1', port: 'GigabitEthernet1/0/2', ip: '10.1.252.1/24', note: 'HRP 心跳,直连' },
462
+ { device: 'FW2', port: 'GigabitEthernet1/0/2', ip: '10.1.252.2/24' },
463
+ { device: 'R-UP', port: 'GigabitEthernet0/0/0', ip: '10.1.250.10/24', note: '外网侧' },
464
+ { device: 'R-DOWN', port: 'GigabitEthernet0/0/0', ip: '10.1.249.10/24', note: '内网侧' },
465
+ ],
466
+ ),
467
+
468
+ // ------------------------------------------------------------------- multicast
469
+ 'multicast-pim-sm': lab(
470
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220'],
471
+ PC('SRC', '192.168.2.100', '255.255.255.0', '192.168.2.254'),
472
+ PC('RCV', '192.168.3.100', '255.255.255.0', '192.168.3.254')],
473
+ [
474
+ ['SRC', 'Ethernet0/0/1', 'R2', 'GigabitEthernet0/0/1'],
475
+ ['RCV', 'Ethernet0/0/1', 'R3', 'GigabitEthernet0/0/1'],
476
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
477
+ ['R1', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
478
+ ],
479
+ [
480
+ { device: 'R1', port: 'LoopBack0', ip: '1.1.1.1/32', note: '候选 RP(静态 RP 时指向它)' },
481
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24' },
482
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24' },
483
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '10.0.13.1/24' },
484
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.13.3/24' },
485
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '192.168.2.254/24', note: '组播源网段' },
486
+ { device: 'R3', port: 'GigabitEthernet0/0/1', ip: '192.168.3.254/24', note: '接收者网段' },
487
+ ],
488
+ ),
489
+
490
+ // ----------------------------------------------------------------------- MPLS
491
+ 'mpls-basic': lab(
492
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220'], ['R4', 'AR2220']],
493
+ [
494
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
495
+ ['R2', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
496
+ ['R3', 'GigabitEthernet0/0/1', 'R4', 'GigabitEthernet0/0/0'],
497
+ ],
498
+ [
499
+ { device: 'R1', port: 'LoopBack0', ip: '1.1.1.1/32', note: 'LSR-ID' },
500
+ { device: 'R2', port: 'LoopBack0', ip: '2.2.2.2/32' },
501
+ { device: 'R3', port: 'LoopBack0', ip: '3.3.3.3/32' },
502
+ { device: 'R4', port: 'LoopBack0', ip: '4.4.4.4/32' },
503
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24' },
504
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24' },
505
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.23.2/24' },
506
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.23.3/24' },
507
+ { device: 'R3', port: 'GigabitEthernet0/0/1', ip: '10.0.34.3/24' },
508
+ { device: 'R4', port: 'GigabitEthernet0/0/0', ip: '10.0.34.4/24' },
509
+ ],
510
+ ),
511
+
512
+ 'mpls-l3vpn': lab(
513
+ [['PE1', 'AR2220'], ['P', 'AR2220'], ['PE2', 'AR2220'], ['CE1', 'AR2220'], ['CE2', 'AR2220']],
514
+ [
515
+ ['CE1', 'GigabitEthernet0/0/0', 'PE1', 'GigabitEthernet0/0/1'],
516
+ ['PE1', 'GigabitEthernet0/0/0', 'P', 'GigabitEthernet0/0/0'],
517
+ ['P', 'GigabitEthernet0/0/1', 'PE2', 'GigabitEthernet0/0/0'],
518
+ ['PE2', 'GigabitEthernet0/0/1', 'CE2', 'GigabitEthernet0/0/0'],
519
+ ],
520
+ [
521
+ { device: 'PE1', port: 'LoopBack0', ip: '1.1.1.1/32', note: 'MP-BGP 的 router-id' },
522
+ { device: 'P', port: 'LoopBack0', ip: '2.2.2.2/32' },
523
+ { device: 'PE2', port: 'LoopBack0', ip: '3.3.3.3/32' },
524
+ { device: 'PE1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: '骨干' },
525
+ { device: 'P', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24' },
526
+ { device: 'P', port: 'GigabitEthernet0/0/1', ip: '10.0.23.2/24' },
527
+ { device: 'PE2', port: 'GigabitEthernet0/0/0', ip: '10.0.23.3/24' },
528
+ { device: 'PE1', port: 'GigabitEthernet0/0/1', ip: '192.168.1.1/24', note: 'VPN 实例 vrf-a' },
529
+ { device: 'CE1', port: 'GigabitEthernet0/0/0', ip: '192.168.1.10/24' },
530
+ { device: 'PE2', port: 'GigabitEthernet0/0/1', ip: '192.168.1.2/24', note: '同一地址空间,站点隔离' },
531
+ { device: 'CE2', port: 'GigabitEthernet0/0/0', ip: '192.168.1.20/24' },
532
+ ],
533
+ ),
534
+
535
+ // ------------------------------------------------------------------ security
536
+ 'ipsec-vpn': lab(
537
+ [['R1', 'AR2220'], ['R2', 'AR2220'],
538
+ PC('PC1', '192.168.1.1', '255.255.255.0', '192.168.1.254'),
539
+ PC('PC2', '192.168.2.1', '255.255.255.0', '192.168.2.254')],
540
+ [
541
+ ['PC1', 'Ethernet0/0/1', 'R1', 'GigabitEthernet0/0/1'],
542
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
543
+ ['R2', 'GigabitEthernet0/0/1', 'PC2', 'Ethernet0/0/1'],
544
+ ],
545
+ [
546
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '192.168.1.254/24', note: '私网侧' },
547
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '200.1.1.1/24', note: '公网侧' },
548
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '200.1.1.2/24', note: '公网侧' },
549
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '192.168.2.254/24', note: '私网侧' },
550
+ ],
551
+ ),
552
+
553
+ 'aaa-local': lab(
554
+ [['R1', 'AR2220'],
555
+ { name: 'ADMIN', model: 'PC', ip: '192.168.1.100', mask: '255.255.255.0', gateway: '192.168.1.254' }],
556
+ [
557
+ ['ADMIN', 'Ethernet0/0/1', 'R1', 'GigabitEthernet0/0/1'],
558
+ ],
559
+ [
560
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '192.168.1.254/24', note: '管理网段' },
561
+ ],
562
+ ),
563
+
564
+ 'network-management': lab(
565
+ [['R1', 'AR2220'], ['SW1', 'S5700'],
566
+ { name: 'NMS', model: 'PC', ip: '10.1.30.100', mask: '255.255.255.0', gateway: '10.1.30.254' }],
567
+ [
568
+ ['NMS', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
569
+ ['SW1', 'GigabitEthernet0/0/24', 'R1', 'GigabitEthernet0/0/0'],
570
+ ],
571
+ [
572
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.1.30.254/24', note: 'NMS 网段' },
573
+ { device: 'R1', port: 'LoopBack0', ip: '1.1.1.1/32', note: '设备管理地址' },
574
+ { device: 'SW1', port: 'Vlanif30', ip: '10.1.30.2/24', note: '设备管理地址' },
575
+ ],
576
+ ),
577
+
578
+ 'ipv6': lab(
579
+ [['R1', 'AR2220'], ['R2', 'AR2220']],
580
+ [
581
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
582
+ ],
583
+ [
584
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '2001:db8:12::1/64', note: 'IPv6 互联' },
585
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '2001:db8:12::2/64' },
586
+ { device: 'R1', port: 'LoopBack0', ip: '2001:db8:1::1/128' },
587
+ { device: 'R2', port: 'LoopBack0', ip: '2001:db8:2::2/128' },
588
+ ],
589
+ ),
590
+
591
+ 'isis-multi-level': lab(
592
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220'], ['R4', 'AR2220']],
593
+ [
594
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
595
+ ['R2', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
596
+ ['R2', 'GigabitEthernet0/0/2', 'R4', 'GigabitEthernet0/0/0'],
597
+ ],
598
+ [
599
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: 'level-1' },
600
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24', note: 'level-1-2' },
601
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.23.2/24', note: 'level-1' },
602
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.23.3/24', note: 'level-1' },
603
+ { device: 'R2', port: 'GigabitEthernet0/0/2', ip: '10.0.24.2/24', note: 'level-2' },
604
+ { device: 'R4', port: 'GigabitEthernet0/0/0', ip: '10.0.24.4/24', note: 'level-2' },
605
+ { device: 'R1', port: 'LoopBack0', ip: '1.1.1.1/32' },
606
+ { device: 'R3', port: 'LoopBack0', ip: '3.3.3.3/32' },
607
+ { device: 'R4', port: 'LoopBack0', ip: '4.4.4.4/32' },
608
+ ],
609
+ ),
610
+
611
+ // -------------------------------------------------------------- comprehensive
612
+ 'campus-network': lab(
613
+ [
614
+ ['CORE1', 'S5700'], ['CORE2', 'S5700'],
615
+ ['ACC1', 'S5700'], ['ACC2', 'S5700'],
616
+ ['EDGE1', 'AR2220'], ['EDGE2', 'AR2220'], ['FW1', 'USG6000V'],
617
+ ['SRV-SW', 'S5700'], { name: 'SRV1', model: 'Server' },
618
+ PC('PC1', '10.1.10.1', '255.255.255.0', '10.1.10.254'),
619
+ PC('PC2', '10.1.20.1', '255.255.255.0', '10.1.20.254'),
620
+ ],
621
+ [
622
+ ['CORE1', 'GigabitEthernet0/0/23', 'CORE2', 'GigabitEthernet0/0/23'],
623
+ ['CORE1', 'GigabitEthernet0/0/24', 'CORE2', 'GigabitEthernet0/0/24'],
624
+ ['CORE1', 'GigabitEthernet0/0/1', 'ACC1', 'GigabitEthernet0/0/23'],
625
+ ['CORE2', 'GigabitEthernet0/0/1', 'ACC1', 'GigabitEthernet0/0/24'],
626
+ ['CORE1', 'GigabitEthernet0/0/2', 'ACC2', 'GigabitEthernet0/0/23'],
627
+ ['CORE2', 'GigabitEthernet0/0/2', 'ACC2', 'GigabitEthernet0/0/24'],
628
+ ['ACC1', 'GigabitEthernet0/0/1', 'PC1', 'Ethernet0/0/1'],
629
+ ['ACC2', 'GigabitEthernet0/0/1', 'PC2', 'Ethernet0/0/1'],
630
+ ['CORE1', 'GigabitEthernet0/0/3', 'EDGE1', 'GigabitEthernet0/0/0'],
631
+ ['CORE2', 'GigabitEthernet0/0/3', 'EDGE2', 'GigabitEthernet0/0/0'],
632
+ ['EDGE1', 'GigabitEthernet0/0/1', 'FW1', 'GigabitEthernet1/0/0'],
633
+ ['EDGE2', 'GigabitEthernet0/0/1', 'FW1', 'GigabitEthernet1/0/1'],
634
+ ['FW1', 'GigabitEthernet1/0/2', 'SRV-SW', 'GigabitEthernet0/0/1'],
635
+ ['SRV-SW', 'GigabitEthernet0/0/2', 'SRV1', 'Ethernet0/0/1'],
636
+ ],
637
+ [
638
+ { device: 'CORE1', port: 'Vlanif10', ip: '10.1.10.252/24', note: 'PC 网段网关(VRRP 主)' },
639
+ { device: 'CORE2', port: 'Vlanif10', ip: '10.1.10.253/24', note: 'VRRP 备' },
640
+ { device: 'CORE1', port: 'Vlanif20', ip: '10.1.20.252/24' },
641
+ { device: 'CORE2', port: 'Vlanif20', ip: '10.1.20.253/24' },
642
+ { device: 'CORE1', port: 'Vlanif31', ip: '10.1.31.1/24', note: '设备管理网段' },
643
+ { device: 'CORE2', port: 'Vlanif31', ip: '10.1.31.2/24' },
644
+ { device: 'EDGE1', port: 'GigabitEthernet0/0/0', ip: '10.1.31.11/24', note: '出口到核心' },
645
+ { device: 'EDGE2', port: 'GigabitEthernet0/0/0', ip: '10.1.31.12/24' },
646
+ { device: 'EDGE1', port: 'GigabitEthernet0/0/1', ip: '10.1.250.1/24', note: '出口到防火墙(内外两段)' },
647
+ { device: 'EDGE2', port: 'GigabitEthernet0/0/1', ip: '10.1.250.2/24' },
648
+ { device: 'FW1', port: 'GigabitEthernet1/0/0', ip: '10.1.249.1/24', note: 'trust 区域' },
649
+ { device: 'FW1', port: 'GigabitEthernet1/0/1', ip: '10.1.250.254/24', note: 'untrust 区域' },
650
+ { device: 'FW1', port: 'GigabitEthernet1/0/2', ip: '10.1.40.1/24', note: 'dmz 区域' },
651
+ { device: 'SRV1', port: 'Ethernet0/0/1', ip: '10.1.40.10/24', note: '对外发布服务器' },
652
+ ],
653
+ {
654
+ CORE1: [140, 90], CORE2: [420, 90],
655
+ ACC1: [60, 300], ACC2: [500, 300],
656
+ PC1: [20, 470], PC2: [560, 470],
657
+ EDGE1: [140, 220], EDGE2: [420, 220],
658
+ FW1: [660, 220], 'SRV-SW': [860, 220], SRV1: [860, 380],
659
+ },
660
+ ),
661
+
662
+ // ------------------------------------------------- the rest of the catalogue
663
+ // Every scenario ships a blueprint so `ensp preset <id> --topo` never dead-ends.
664
+ // These are built from the device counts already stated in each scenario's
665
+ // `devices` field, kept deliberately small: they exist to remove the drawing
666
+ // and the IP arithmetic, not to preconfigure anything.
667
+
668
+ 'super-vlan': lab(
669
+ [['SW1', 'S5700'],
670
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
671
+ PC('PC2', '192.168.20.1', '255.255.255.0', '192.168.20.254'),
672
+ PC('PC3', '192.168.20.2', '255.255.255.0', '192.168.20.254')],
673
+ [
674
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
675
+ ['PC2', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/2'],
676
+ ['PC3', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/3'],
677
+ ],
678
+ [
679
+ { device: 'SW1', port: 'Vlanif10', ip: '192.168.10.254/24', note: 'super-VLAN 网关' },
680
+ { device: 'SW1', port: 'Vlanif20', ip: '192.168.20.254/24', note: 'sub-VLAN 20' },
681
+ { device: 'SW1', port: 'Vlanif30', ip: '192.168.30.254/24', note: 'sub-VLAN 30' },
682
+ ],
683
+ ),
684
+
685
+ 'rip': lab(
686
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220'],
687
+ PC('PC1', '192.168.1.1', '255.255.255.0', '192.168.1.254'),
688
+ PC('PC2', '192.168.3.1', '255.255.255.0', '192.168.3.254')],
689
+ [
690
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
691
+ ['R2', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
692
+ ['R1', 'GigabitEthernet0/0/1', 'PC1', 'Ethernet0/0/1'],
693
+ ['R3', 'GigabitEthernet0/0/1', 'PC2', 'Ethernet0/0/1'],
694
+ ],
695
+ [
696
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: '互联 R2' },
697
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24' },
698
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.23.2/24', note: '互联 R3' },
699
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.23.3/24' },
700
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '192.168.1.254/24', note: 'PC1 网关' },
701
+ { device: 'R3', port: 'GigabitEthernet0/0/1', ip: '192.168.3.254/24', note: 'PC2 网关' },
702
+ ],
703
+ ),
704
+
705
+ 'ospf-lsa-types': lab(
706
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220'], ['R4', 'AR2220']],
707
+ [
708
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
709
+ ['R2', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
710
+ ['R3', 'GigabitEthernet0/0/1', 'R4', 'GigabitEthernet0/0/0'],
711
+ ],
712
+ [
713
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: 'area 1' },
714
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24' },
715
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.23.2/24', note: 'area 0' },
716
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.23.3/24' },
717
+ { device: 'R3', port: 'GigabitEthernet0/0/1', ip: '10.0.34.3/24', note: 'area 2 / stub' },
718
+ { device: 'R4', port: 'GigabitEthernet0/0/0', ip: '10.0.34.4/24' },
719
+ { device: 'R1', port: 'LoopBack0', ip: '1.1.1.1/32', note: 'Type-1 观察点' },
720
+ { device: 'R2', port: 'LoopBack0', ip: '2.2.2.2/32', note: 'ABR' },
721
+ { device: 'R3', port: 'LoopBack0', ip: '3.3.3.3/32', note: 'ABR' },
722
+ { device: 'R4', port: 'LoopBack0', ip: '4.4.4.4/32', note: 'ASBR 外部路由' },
723
+ ],
724
+ ),
725
+
726
+ 'ospf-troubleshooting': lab(
727
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220']],
728
+ [
729
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
730
+ ['R2', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
731
+ ],
732
+ [
733
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: '故意配错 area 做排障' },
734
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24' },
735
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.23.2/24' },
736
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.23.3/24' },
737
+ { device: 'R2', port: 'LoopBack0', ip: '2.2.2.2/32', note: 'router-id 冲突观察点' },
738
+ { device: 'R3', port: 'LoopBack0', ip: '2.2.2.2/32' },
739
+ ],
740
+ ),
741
+
742
+ 'bgp-attributes': lab(
743
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220'], ['R4', 'AR2220']],
744
+ [
745
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
746
+ ['R1', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
747
+ ['R2', 'GigabitEthernet0/0/1', 'R4', 'GigabitEthernet0/0/0'],
748
+ ['R3', 'GigabitEthernet0/0/1', 'R4', 'GigabitEthernet0/0/1'],
749
+ ],
750
+ [
751
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: 'AS 100 双出口' },
752
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24', note: 'AS 200' },
753
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '10.0.13.1/24' },
754
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.13.3/24', note: 'AS 300' },
755
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.24.2/24' },
756
+ { device: 'R4', port: 'GigabitEthernet0/0/0', ip: '10.0.24.4/24', note: 'AS 400 选路观察点' },
757
+ { device: 'R3', port: 'GigabitEthernet0/0/1', ip: '10.0.34.3/24' },
758
+ { device: 'R4', port: 'GigabitEthernet0/0/1', ip: '10.0.34.4/24' },
759
+ ],
760
+ ),
761
+
762
+ 'bgp-aggregation': lab(
763
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220']],
764
+ [
765
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
766
+ ['R2', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
767
+ ],
768
+ [
769
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: 'AS 100,发布明细' },
770
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24', note: 'AS 200,做聚合' },
771
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.23.2/24' },
772
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.23.3/24', note: 'AS 300,验证 AS_PATH' },
773
+ { device: 'R1', port: 'LoopBack0', ip: '10.10.1.1/24', note: '聚合前明细 1' },
774
+ { device: 'R1', port: 'LoopBack1', ip: '10.10.2.1/24', note: '聚合前明细 2' },
775
+ { device: 'R1', port: 'LoopBack2', ip: '10.10.3.1/24', note: '聚合前明细 3' },
776
+ ],
777
+ ),
778
+
779
+ 'bgp-troubleshooting': lab(
780
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220']],
781
+ [
782
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
783
+ ['R2', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
784
+ ],
785
+ [
786
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/24', note: 'AS 100' },
787
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/24', note: 'AS 200' },
788
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.23.2/24' },
789
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '10.0.23.3/24', note: 'AS 300' },
790
+ { device: 'R1', port: 'LoopBack0', ip: '1.1.1.1/32', note: 'TCP 179 源地址' },
791
+ { device: 'R3', port: 'LoopBack0', ip: '3.3.3.3/32' },
792
+ ],
793
+ ),
794
+
795
+ 'stack-istack': lab(
796
+ [['SW1', 'S5700'], ['SW2', 'S5700'], ['SW3', 'S5700'],
797
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
798
+ PC('PC2', '192.168.10.2', '255.255.255.0', '192.168.10.254')],
799
+ [
800
+ ['SW1', 'GigabitEthernet0/0/1', 'SW2', 'GigabitEthernet0/0/1'],
801
+ ['SW2', 'GigabitEthernet0/0/2', 'SW3', 'GigabitEthernet0/0/1'],
802
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/23'],
803
+ ['PC2', 'Ethernet0/0/1', 'SW3', 'GigabitEthernet0/0/23'],
804
+ ],
805
+ [
806
+ { device: 'SW1', port: 'Vlanif10', ip: '192.168.10.251/24', note: '堆叠主,唯一管理地址' },
807
+ { device: 'SW2', port: 'Vlanif10', ip: '192.168.10.252/24', note: '堆叠备' },
808
+ { device: 'SW3', port: 'Vlanif10', ip: '192.168.10.253/24', note: '接入设备' },
809
+ ],
810
+ ),
811
+
812
+ // eNSP's default AR2220 template declares only GE ports; a Serial card has to be
813
+ // added to the device by hand, and a blueprint cannot express that. VRP runs PPP
814
+ // and HDLC over GE just as well, so the two links below carry the encapsulations
815
+ // without a serial card: `link-protocol ppp` / `link-protocol hdlc` per interface.
816
+ 'ppp-hdlc': lab(
817
+ [['R1', 'AR2220'], ['R2', 'AR2220']],
818
+ [['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
819
+ ['R1', 'GigabitEthernet0/0/1', 'R2', 'GigabitEthernet0/0/1']],
820
+ [
821
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '10.0.12.1/30', note: 'link-protocol ppp + CHAP' },
822
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '10.0.12.2/30' },
823
+ { device: 'R1', port: 'GigabitEthernet0/0/1', ip: '10.0.13.1/30', note: 'link-protocol hdlc 对照' },
824
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '10.0.13.2/30' },
825
+ ],
826
+ ),
827
+
828
+ 'arp-security': lab(
829
+ [['SW1', 'S5700'],
830
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
831
+ PC('PC2', '192.168.10.2', '255.255.255.0', '192.168.10.254'),
832
+ PC('PC3', '192.168.10.3', '255.255.255.0', '192.168.10.254')],
833
+ [
834
+ ['PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1'],
835
+ ['PC2', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/2'],
836
+ ['PC3', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/3'],
837
+ ],
838
+ [
839
+ { device: 'SW1', port: 'Vlanif10', ip: '192.168.10.254/24', note: '网关兼 DHCP Snooping 设备' },
840
+ ],
841
+ ),
842
+
843
+ 'wlan-basic': lab(
844
+ [['AC', 'AC6005'], ['AP1', 'AP6050'], ['SW1', 'S5700'],
845
+ PC('PC1', '192.168.50.101', '255.255.255.0', '192.168.50.254')],
846
+ [
847
+ ['SW1', 'GigabitEthernet0/0/1', 'AC', 'GigabitEthernet0/0/1'],
848
+ ['SW1', 'GigabitEthernet0/0/2', 'AP1', 'GigabitEthernet0/0/1'],
849
+ ['SW1', 'GigabitEthernet0/0/3', 'PC1', 'Ethernet0/0/1'],
850
+ ],
851
+ [
852
+ { device: 'AC', port: 'Vlanif100', ip: '192.168.100.1/24', note: 'CAPWAP 源与管理' },
853
+ { device: 'AC', port: 'Vlanif50', ip: '192.168.50.254/24', note: '业务网关' },
854
+ { device: 'SW1', port: 'Vlanif100', ip: '192.168.100.254/24', note: 'AP 管理网段' },
855
+ ],
856
+ ),
857
+
858
+ 'ospfv3-ipv6': lab(
859
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['R3', 'AR2220']],
860
+ [
861
+ ['R1', 'GigabitEthernet0/0/0', 'R2', 'GigabitEthernet0/0/0'],
862
+ ['R2', 'GigabitEthernet0/0/1', 'R3', 'GigabitEthernet0/0/0'],
863
+ ],
864
+ [
865
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '2001:db8:12::1/64', note: 'OSPFv3 area 0' },
866
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '2001:db8:12::2/64' },
867
+ { device: 'R2', port: 'GigabitEthernet0/0/1', ip: '2001:db8:23::2/64' },
868
+ { device: 'R3', port: 'GigabitEthernet0/0/0', ip: '2001:db8:23::3/64' },
869
+ { device: 'R3', port: 'LoopBack0', ip: '2001:db8:3::3/128', note: 'router-id 用 IPv4 地址' },
870
+ ],
871
+ ),
872
+
873
+ 'spine-leaf-vxlan': lab(
874
+ [['SPINE1', 'S5700'], ['SPINE2', 'S5700'], ['LEAF1', 'S5700'], ['LEAF2', 'S5700'],
875
+ PC('PC1', '192.168.10.1', '255.255.255.0', '192.168.10.254'),
876
+ PC('PC2', '192.168.10.2', '255.255.255.0', '192.168.10.254')],
877
+ [
878
+ ['LEAF1', 'GigabitEthernet0/0/23', 'SPINE1', 'GigabitEthernet0/0/1'],
879
+ ['LEAF1', 'GigabitEthernet0/0/24', 'SPINE2', 'GigabitEthernet0/0/1'],
880
+ ['LEAF2', 'GigabitEthernet0/0/23', 'SPINE1', 'GigabitEthernet0/0/2'],
881
+ ['LEAF2', 'GigabitEthernet0/0/24', 'SPINE2', 'GigabitEthernet0/0/2'],
882
+ ['LEAF1', 'GigabitEthernet0/0/1', 'PC1', 'Ethernet0/0/1'],
883
+ ['LEAF2', 'GigabitEthernet0/0/1', 'PC2', 'Ethernet0/0/1'],
884
+ ],
885
+ [
886
+ { device: 'SPINE1', port: 'LoopBack0', ip: '10.0.255.11/32', note: 'BGP 邻居 / VTEP 源' },
887
+ { device: 'SPINE2', port: 'LoopBack0', ip: '10.0.255.12/32' },
888
+ { device: 'LEAF1', port: 'LoopBack0', ip: '10.0.255.21/32', note: 'VTEP' },
889
+ { device: 'LEAF2', port: 'LoopBack0', ip: '10.0.255.22/32' },
890
+ { device: 'LEAF1', port: 'Vlanif10', ip: '192.168.10.252/24', note: '分布式网关' },
891
+ { device: 'LEAF2', port: 'Vlanif10', ip: '192.168.10.253/24' },
892
+ ],
893
+ ),
894
+
895
+ 'ipsec-gre-tunnel': lab(
896
+ [['R1', 'AR2220'], ['R2', 'AR2220'], ['ISP', 'AR2220']],
897
+ [
898
+ ['R1', 'GigabitEthernet0/0/0', 'ISP', 'GigabitEthernet0/0/0'],
899
+ ['ISP', 'GigabitEthernet0/0/1', 'R2', 'GigabitEthernet0/0/0'],
900
+ ],
901
+ [
902
+ { device: 'R1', port: 'GigabitEthernet0/0/0', ip: '100.1.1.1/30', note: '公网侧' },
903
+ { device: 'ISP', port: 'GigabitEthernet0/0/0', ip: '100.1.1.2/30' },
904
+ { device: 'ISP', port: 'GigabitEthernet0/0/1', ip: '100.2.2.2/30' },
905
+ { device: 'R2', port: 'GigabitEthernet0/0/0', ip: '100.2.2.1/30' },
906
+ { device: 'R1', port: 'Tunnel0/0/0', ip: '10.1.1.1/24', note: 'GRE over IPsec' },
907
+ { device: 'R2', port: 'Tunnel0/0/0', ip: '10.1.1.2/24' },
908
+ ],
909
+ ),
910
+
911
+ 'wlan-roaming': lab(
912
+ [['AC', 'AC6005'], ['SW1', 'S5700'], ['SW2', 'S5700'],
913
+ ['AP1', 'AP6050'], ['AP2', 'AP6050'],
914
+ PC('PC1', '192.168.50.101', '255.255.255.0', '192.168.50.254')],
915
+ [
916
+ ['SW1', 'GigabitEthernet0/0/1', 'AC', 'GigabitEthernet0/0/1'],
917
+ ['SW1', 'GigabitEthernet0/0/2', 'SW2', 'GigabitEthernet0/0/1'],
918
+ ['SW1', 'GigabitEthernet0/0/3', 'AP1', 'GigabitEthernet0/0/1'],
919
+ ['SW2', 'GigabitEthernet0/0/2', 'AP2', 'GigabitEthernet0/0/1'],
920
+ ],
921
+ [
922
+ { device: 'AC', port: 'Vlanif100', ip: '192.168.100.1/24', note: 'CAPWAP 源' },
923
+ { device: 'AC', port: 'Vlanif50', ip: '192.168.50.254/24', note: '业务网关(漫游域)' },
924
+ { device: 'SW1', port: 'Vlanif100', ip: '192.168.100.252/24' },
925
+ { device: 'SW2', port: 'Vlanif100', ip: '192.168.100.253/24' },
926
+ ],
927
+ ),
928
+ }
929
+
930
+ /** Scenario ids that ship a topology blueprint. */
931
+ export const LAB_IDS = Object.keys(LABS)
932
+
933
+ /** Whether a blueprint exists for this scenario. */
934
+ export function hasLab(id) {
935
+ return Object.prototype.hasOwnProperty.call(LABS, id)
936
+ }
937
+
938
+ /**
939
+ * Build a topology for a scenario blueprint.
940
+ *
941
+ * @param {string} id - scenario id
942
+ * @param {{firstComPort?: number}} [options]
943
+ * @returns {object} topology object ready for `writeTopology`
944
+ */
945
+ export function buildScenarioTopology(id, { firstComPort = 2000 } = {}) {
946
+ const spec = LABS[id]
947
+ if (!spec) throw new Error(`no topology blueprint for scenario "${id}"; available: ${LAB_IDS.join(', ')}`)
948
+
949
+ const builder = buildTopology({ firstComPort })
950
+ for (const entry of spec.devices) {
951
+ const [name, model] = Array.isArray(entry) ? entry : [entry.name, entry.model]
952
+ // `ip: 'dhcp'` means the host should ask for an address instead of carrying a
953
+ // static one; eNSP expresses that with -simpc_dhcp_state rather than an address.
954
+ const options = Array.isArray(entry)
955
+ ? {}
956
+ : entry.ip === 'dhcp'
957
+ ? { pc: { dhcp: true } }
958
+ : entry.ip
959
+ ? { pc: { ip: entry.ip, mask: entry.mask, gateway: entry.gateway } }
960
+ : {}
961
+ builder.add(name, model, options)
962
+ }
963
+ for (const [aName, aPort, bName, bPort] of spec.links) {
964
+ builder.link(aName, aPort, bName, bPort)
965
+ }
966
+
967
+ const topo = builder.finish()
968
+ autoLayout(topo)
969
+
970
+ // Blueprints with hand-placed coordinates keep them; the grid is only ever a
971
+ // fallback for the ones that do not care about layout.
972
+ if (spec.positions) {
973
+ for (const device of topo.devices) {
974
+ const at = spec.positions[device.name]
975
+ if (at) {
976
+ device.x = at[0]
977
+ device.y = at[1]
978
+ }
979
+ }
980
+ }
981
+
982
+ topo.meta = { scenario: id }
983
+ return topo
984
+ }
985
+
986
+ /**
987
+ * Addressing plan for a blueprint, as plain rows for display.
988
+ *
989
+ * @param {string} id
990
+ * @returns {{device: string, port: string, ip: string, note: string}[]}
991
+ */
992
+ export function labAddressing(id) {
993
+ const spec = LABS[id]
994
+ if (!spec) return []
995
+ return spec.addressing.map((row) => ({ note: '', ...row }))
996
+ }
997
+
998
+ /** Device summary for a blueprint, e.g. `CORE1 (S5700)`. */
999
+ export function labDevices(id) {
1000
+ const spec = LABS[id]
1001
+ if (!spec) return []
1002
+ return spec.devices.map((entry) =>
1003
+ Array.isArray(entry) ? `${entry[0]} (${entry[1]})` : `${entry.name} (${entry.model})`,
1004
+ )
1005
+ }
1006
+
1007
+ /**
1008
+ * Model names used by a blueprint, deduplicated.
1009
+ *
1010
+ * @param {string} id scenario id
1011
+ * @returns {string[]}
1012
+ */
1013
+ export function labModels(id) {
1014
+ const spec = LABS[id]
1015
+ if (!spec) return []
1016
+ return [
1017
+ ...new Set(spec.devices.map((entry) => (Array.isArray(entry) ? entry[1] : entry.model))),
1018
+ ]
1019
+ }