@takodotid/circuit 0.9.0 → 0.11.0

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 (48) hide show
  1. package/README.md +3 -2
  2. package/dist/chunks/index-ex1wcst7.js +109 -0
  3. package/dist/chunks/index-fq57hya4.js +20 -0
  4. package/dist/chunks/new-7n8qvtjd.js +168 -0
  5. package/dist/cli/index.js +4079 -0
  6. package/dist/index.js +48 -0
  7. package/dist/presets/index.js +269 -0
  8. package/dist/types/adapters/devices/catalog.d.ts +58 -0
  9. package/dist/types/adapters/devices/derive.d.ts +5 -0
  10. package/dist/types/adapters/devices/index.d.ts +3 -0
  11. package/dist/types/adapters/devices/lines.d.ts +56 -0
  12. package/dist/types/adapters/devices/raisecom-ros/index.d.ts +23 -0
  13. package/dist/types/adapters/devices/raisecom-ros/models.d.ts +14 -0
  14. package/dist/types/adapters/devices/routeros/export.d.ts +39 -0
  15. package/dist/types/adapters/devices/routeros/index.d.ts +17 -0
  16. package/dist/types/adapters/devices/routeros/models.d.ts +14 -0
  17. package/dist/types/adapters/devices/routeros/plan.d.ts +4 -0
  18. package/dist/types/adapters/devices/routeros/render.d.ts +96 -0
  19. package/dist/types/adapters/devices/types.d.ts +34 -0
  20. package/dist/types/adapters/devices/vrp/index.d.ts +24 -0
  21. package/dist/types/adapters/devices/vrp/models.d.ts +12 -0
  22. package/dist/types/adapters/registries/index.d.ts +3 -0
  23. package/dist/types/adapters/registries/peeringdb.d.ts +16 -0
  24. package/dist/types/core/addr.d.ts +24 -0
  25. package/dist/types/core/define.d.ts +50 -0
  26. package/dist/types/core/merge.d.ts +4 -0
  27. package/dist/types/core/secrets.d.ts +21 -0
  28. package/dist/types/core/time.d.ts +2 -0
  29. package/dist/types/core/validate.d.ts +2 -0
  30. package/dist/types/index.d.ts +6 -0
  31. package/dist/types/presets/index.d.ts +101 -0
  32. package/dist/types/schema/common.d.ts +24 -0
  33. package/dist/types/schema/device.d.ts +80 -0
  34. package/dist/types/schema/firewall.d.ts +118 -0
  35. package/dist/types/schema/index.d.ts +7 -0
  36. package/dist/types/schema/interfaces.d.ts +181 -0
  37. package/dist/types/schema/policy.d.ts +88 -0
  38. package/dist/types/schema/routing.d.ts +115 -0
  39. package/dist/types/schema/system.d.ts +186 -0
  40. package/dist/types/transport/ssh.d.ts +22 -0
  41. package/package.json +19 -5
  42. package/src/cli/index.ts +10 -2
  43. package/src/cli/new.ts +60 -12
  44. package/src/core/secrets.ts +7 -9
  45. package/src/presets/index.ts +17 -3
  46. package/templates/project/AGENTS.md +10 -10
  47. package/templates/project/README.md +7 -7
  48. package/templates/project/tsconfig.json +1 -1
@@ -0,0 +1,4079 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ contains,
4
+ familyOf,
5
+ host,
6
+ isAddress,
7
+ isCidr,
8
+ isGlobal,
9
+ netmask,
10
+ network,
11
+ split,
12
+ wildcard
13
+ } from "../chunks/index-ex1wcst7.js";
14
+ import {
15
+ __require,
16
+ __toESM
17
+ } from "../chunks/index-fq57hya4.js";
18
+
19
+ // src/cli/index.ts
20
+ import { createPrivateKey, createPublicKey } from "node:crypto";
21
+ import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
22
+ import { basename, dirname, join as join2, resolve as resolve2 } from "node:path";
23
+
24
+ // src/core/secrets.ts
25
+ import { spawnSync } from "node:child_process";
26
+ import { existsSync, readFileSync } from "node:fs";
27
+ import { join } from "node:path";
28
+ var root = process.cwd();
29
+ var envFile;
30
+ var fromOnePassword = new Map;
31
+ function useRoot(directory) {
32
+ root = directory;
33
+ envFile = undefined;
34
+ }
35
+ function dotenv() {
36
+ if (envFile)
37
+ return envFile;
38
+ envFile = {};
39
+ const path = join(root, ".env");
40
+ if (!existsSync(path))
41
+ return envFile;
42
+ for (const line of readFileSync(path, "utf8").split(`
43
+ `)) {
44
+ const trimmed = line.trim();
45
+ if (!trimmed || trimmed.startsWith("#"))
46
+ continue;
47
+ const separator = trimmed.indexOf("=");
48
+ if (separator <= 0)
49
+ continue;
50
+ const value = trimmed.slice(separator + 1).trim();
51
+ const unquoted = /^(["']).*\1$/.test(value) ? value.slice(1, -1) : value;
52
+ envFile[trimmed.slice(0, separator).trim()] = unquoted;
53
+ }
54
+ return envFile;
55
+ }
56
+ function readOnePassword(name, reference) {
57
+ const cached = fromOnePassword.get(reference);
58
+ if (cached !== undefined)
59
+ return cached;
60
+ const result = spawnSync("op", ["read", "--no-newline", reference], { encoding: "utf8" });
61
+ if (result.error)
62
+ throw new Error(`secret ${name} is a 1Password reference, and the 1Password CLI, op, is not installed`);
63
+ if (result.status !== 0) {
64
+ const reason = result.stderr.trim() || `op exited with ${result.status}`;
65
+ throw new Error(`secret ${name}: 1Password could not read ${reference}: ${reason}`);
66
+ }
67
+ const value = result.stdout;
68
+ fromOnePassword.set(reference, value);
69
+ return value;
70
+ }
71
+ var isSecret = (value) => typeof value === "object" && value !== null && (("secret" in value) || ("secret_file" in value));
72
+ var referenceOf = (secret) => ("secret" in secret) ? secret.secret : `file:${secret.secret_file}`;
73
+ function sourceOf(reference) {
74
+ if (reference.startsWith("file:"))
75
+ return existsSync(join(root, reference.slice("file:".length))) ? "a file" : undefined;
76
+ const value = process.env[reference] || dotenv()[reference];
77
+ if (!value)
78
+ return;
79
+ const where = process.env[reference] && process.env[reference] !== dotenv()[reference] ? "the environment" : ".env";
80
+ return value.startsWith("op://") ? `1Password, through ${where}` : where;
81
+ }
82
+ function valueOf(reference) {
83
+ if (reference.startsWith("file:")) {
84
+ const path = reference.slice("file:".length);
85
+ const fullPath = join(root, path);
86
+ if (!existsSync(fullPath))
87
+ throw new Error(`secret file ${path} does not exist`);
88
+ const value2 = readFileSync(fullPath, "utf8").trimEnd();
89
+ if (!value2)
90
+ throw new Error(`secret file ${path} is empty`);
91
+ return value2;
92
+ }
93
+ const value = process.env[reference] || dotenv()[reference];
94
+ if (!value)
95
+ throw new Error(`secret ${reference} is not set in the environment or in .env`);
96
+ return value.startsWith("op://") ? readOnePassword(reference, value) : value;
97
+ }
98
+ var resolve = (secret) => valueOf(typeof secret === "string" ? secret : referenceOf(secret));
99
+ var marker = (secret) => `<secret:${referenceOf(secret)}>`;
100
+ var MARKER = /<secret:([A-Za-z0-9_./:-]+)>/g;
101
+ var substitute = (text) => text.replace(MARKER, (_, reference) => valueOf(reference));
102
+ var valueOfReference = valueOf;
103
+
104
+ // src/core/time.ts
105
+ function offset(zone) {
106
+ const name = new Intl.DateTimeFormat("en-US", {
107
+ timeZone: zone,
108
+ timeZoneName: "longOffset"
109
+ }).formatToParts(new Date).find((entry) => entry.type === "timeZoneName").value;
110
+ const found = /GMT([+-])(\d{2}):(\d{2})/.exec(name);
111
+ if (!found)
112
+ return 0;
113
+ const [, sign, hours, minutes] = found;
114
+ const sizeInMinutes = Number(hours) * 60 + Number(minutes);
115
+ return sign === "-" ? -sizeInMinutes : sizeInMinutes;
116
+ }
117
+
118
+ // src/transport/ssh.ts
119
+ import { Client } from "ssh2";
120
+ function connect(login) {
121
+ return new Promise((resolve2, reject) => {
122
+ const client = new Client;
123
+ client.on("ready", () => resolve2(client)).on("error", reject).connect({
124
+ host: login.host,
125
+ port: login.port ?? 22,
126
+ username: login.user,
127
+ password: login.password,
128
+ readyTimeout: 30000,
129
+ ...login.hostKeys ? { algorithms: { serverHostKey: login.hostKeys } } : {}
130
+ });
131
+ });
132
+ }
133
+ function exec(client, command, timeout = 120000) {
134
+ return new Promise((resolve2, reject) => {
135
+ client.exec(command, (error, stream) => {
136
+ if (error)
137
+ return reject(error);
138
+ let output = "";
139
+ const append = (chunk) => output += chunk.toString("utf8");
140
+ const timer = setTimeout(() => resolve2(output.trim()), timeout);
141
+ stream.on("data", append);
142
+ stream.stderr.on("data", append);
143
+ stream.on("close", () => {
144
+ clearTimeout(timer);
145
+ resolve2(output.trim());
146
+ });
147
+ });
148
+ });
149
+ }
150
+ function shell(client, columns = 200, rows = 1000) {
151
+ return new Promise((resolve2, reject) => {
152
+ client.shell({ cols: columns, rows }, (error, channel) => error ? reject(error) : resolve2(channel));
153
+ });
154
+ }
155
+ function readUntil(channel, done, timeout, settle = 400) {
156
+ return new Promise((resolve2) => {
157
+ let output = "";
158
+ let finished = false;
159
+ const finish = () => {
160
+ if (finished)
161
+ return;
162
+ finished = true;
163
+ channel.removeListener("data", onData);
164
+ resolve2(output);
165
+ };
166
+ const onData = (chunk) => {
167
+ output += chunk.toString("utf8");
168
+ if (done(output))
169
+ setTimeout(finish, settle);
170
+ };
171
+ channel.on("data", onData);
172
+ setTimeout(finish, timeout);
173
+ });
174
+ }
175
+ function send(channel, line, timeout = 20000, settle = 1500) {
176
+ channel.write(line + `
177
+ `);
178
+ return new Promise((resolve2) => {
179
+ let output = "";
180
+ let timer;
181
+ const finish = () => {
182
+ channel.removeListener("data", onData);
183
+ resolve2(output);
184
+ };
185
+ const onData = (chunk) => {
186
+ output += chunk.toString("utf8");
187
+ clearTimeout(timer);
188
+ timer = setTimeout(finish, settle);
189
+ };
190
+ channel.on("data", onData);
191
+ timer = setTimeout(finish, timeout);
192
+ });
193
+ }
194
+ function upload(client, path, content) {
195
+ return new Promise((resolve2, reject) => {
196
+ client.sftp((error, sftp) => {
197
+ if (error)
198
+ return reject(error);
199
+ sftp.writeFile(path, content, (writeError) => {
200
+ sftp.end();
201
+ if (writeError)
202
+ reject(writeError);
203
+ else
204
+ resolve2();
205
+ });
206
+ });
207
+ });
208
+ }
209
+
210
+ // src/adapters/devices/raisecom-ros/models.ts
211
+ var models = {
212
+ "RAX721-C-6C48": {
213
+ ports: { "100g": 6, "25g": 24, "10g": 24 },
214
+ spelling: { "100g": "hundredgige 1/1/{n}", "25g": "twenty-fivegige 1/2/{n}", "10g": "tengigabitethernet 1/3/{n}" }
215
+ }
216
+ };
217
+
218
+ // src/adapters/devices/routeros/models.ts
219
+ var models2 = {
220
+ "CCR2216-1G-12XS-2XQ": {
221
+ ports: { "100g": 2, "25g": 12, "1g": 1 },
222
+ spelling: { "100g": "qsfp28-{n}-1", "25g": "sfp28-{n}", "1g": "ether{n}" }
223
+ }
224
+ };
225
+
226
+ // src/adapters/devices/vrp/models.ts
227
+ var models3 = {
228
+ "CE6855-48S6Q-HI": { ports: { "10g": 48, "40g": 6 }, spelling: { "10g": "10GE1/0/{n}", "40g": "40GE1/0/{n}" } }
229
+ };
230
+
231
+ // src/adapters/devices/catalog.ts
232
+ var catalog = { routeros: models2, vrp: models3, "raisecom-ros": models };
233
+ function portsOf(platform, model) {
234
+ const spec = catalog[platform]?.[model];
235
+ if (!spec)
236
+ throw new Error(`${platform} has no model ${model}`);
237
+ return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));
238
+ }
239
+ function spell(platform, model, port) {
240
+ const spec = catalog[platform]?.[model];
241
+ const parts = /^(\d+[gm])-(\d+)$/.exec(port);
242
+ const template = parts && spec?.spelling[parts[1]];
243
+ if (!template)
244
+ throw new Error(`${model} has no port ${port}`);
245
+ return template.replace("{n}", parts[2]);
246
+ }
247
+
248
+ // src/adapters/devices/derive.ts
249
+ function lagIds(device) {
250
+ const numbers = new Map;
251
+ let position = 0;
252
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
253
+ if (iface.type !== "lag")
254
+ continue;
255
+ position++;
256
+ numbers.set(name, iface.id ?? position);
257
+ }
258
+ return numbers;
259
+ }
260
+ function ipv4Networks(device, iface) {
261
+ const settings = device.interfaces?.[iface] ?? device.ports?.[iface];
262
+ const addresses = (settings?.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
263
+ return addresses.filter((address) => familyOf(address) === "ipv4").map((address) => network(address));
264
+ }
265
+
266
+ // src/adapters/devices/lines.ts
267
+ var indentOf = (raw) => raw.length - raw.trimStart().length;
268
+ function entries(text, dialect) {
269
+ const lines = text.replace(/\r/g, "").split(`
270
+ `).filter((raw) => raw.trim() !== "");
271
+ const result = [];
272
+ const open = [];
273
+ for (let i = 0;i < lines.length; i++) {
274
+ const raw = lines[i];
275
+ const indent = indentOf(raw);
276
+ const line = raw.trim().replace(/\s+/g, " ");
277
+ if (line === dialect.terminator) {
278
+ if (indent === 0)
279
+ open.length = 0;
280
+ continue;
281
+ }
282
+ if (dialect.ignore(line))
283
+ continue;
284
+ if (dialect.indented) {
285
+ while (open.length && open.at(-1).indent >= indent)
286
+ open.pop();
287
+ } else if (indent === 0 && dialect.block.test(line)) {
288
+ open.length = 0;
289
+ }
290
+ result.push({ path: open.map((block) => block.header), line });
291
+ const isContent = (candidate) => candidate.trim() !== dialect.terminator && !dialect.ignore(candidate.trim());
292
+ const nextLine = lines.slice(i + 1).find(isContent);
293
+ const opensBlock = dialect.indented ? nextLine !== undefined && indentOf(nextLine) > indent : indent === 0 && dialect.block.test(line);
294
+ if (opensBlock)
295
+ open.push({ indent, header: line });
296
+ }
297
+ return result;
298
+ }
299
+ function head(line) {
300
+ const rule = /^rule \d+/.exec(line);
301
+ if (rule)
302
+ return rule[0];
303
+ if (/^(description|name) /.test(line))
304
+ return line.split(" ")[0];
305
+ const words = line.split(" ");
306
+ return words.slice(0, Math.max(1, words.length - 1)).join(" ");
307
+ }
308
+ function compare(desired, current, dialect) {
309
+ const canon = dialect.canon ?? ((line) => line);
310
+ const keyOf = (entry) => [...entry.path, canon(entry.line)].join("\x00");
311
+ const pathKey = (path) => path.join("\x00");
312
+ const wanted = entries(desired, dialect);
313
+ const existing = entries(current, dialect);
314
+ const wantedKeys = new Set(wanted.map(keyOf));
315
+ const existingKeys = new Set(existing.map(keyOf));
316
+ const wantedBlocks = new Set(wanted.map((entry) => pathKey([...entry.path, entry.line])));
317
+ const groups = new Map;
318
+ const groupFor = (path) => {
319
+ const key = pathKey(path);
320
+ if (!groups.has(key))
321
+ groups.set(key, { path, remove: [], add: [] });
322
+ return groups.get(key);
323
+ };
324
+ for (const entry of wanted) {
325
+ if (existingKeys.has(keyOf(entry)))
326
+ continue;
327
+ const ownBlock = pathKey([...entry.path, entry.line]);
328
+ const opensBlock = wanted.some((other) => {
329
+ const deeper = other.path.length > entry.path.length;
330
+ return deeper && pathKey(other.path.slice(0, entry.path.length + 1)) === ownBlock;
331
+ });
332
+ if (opensBlock)
333
+ continue;
334
+ groupFor(entry.path).add.push(entry.line);
335
+ }
336
+ for (const entry of [...existing].reverse()) {
337
+ if (wantedKeys.has(keyOf(entry)))
338
+ continue;
339
+ const insideRemovedBlock = entry.path.some((_, depth) => {
340
+ const enclosing = pathKey(entry.path.slice(0, depth + 1));
341
+ return !wantedBlocks.has(enclosing);
342
+ });
343
+ if (insideRemovedBlock)
344
+ continue;
345
+ const replaced = wanted.some((other) => {
346
+ const sameBlock = pathKey(other.path) === pathKey(entry.path);
347
+ const isNew = !existingKeys.has(keyOf(other));
348
+ return sameBlock && isNew && head(other.line) === head(entry.line);
349
+ });
350
+ if (!replaced)
351
+ groupFor(entry.path).remove.push(entry.line);
352
+ }
353
+ return [...groups.values()].filter((group) => group.add.length || group.remove.length);
354
+ }
355
+ function vlanListChange(groups, prefix, parse, format, removal) {
356
+ const top = groups.find((group) => group.path.length === 0);
357
+ if (!top)
358
+ return;
359
+ const old = top.remove.find((line) => line.startsWith(prefix));
360
+ const fresh = top.add.find((line) => line.startsWith(prefix));
361
+ if (!old && !fresh)
362
+ return;
363
+ const before = old ? parse(old) : [];
364
+ const after = fresh ? parse(fresh) : [];
365
+ top.remove = top.remove.filter((line) => line !== old);
366
+ top.add = top.add.filter((line) => line !== fresh);
367
+ const added = after.filter((id) => !before.includes(id));
368
+ const removed = before.filter((id) => !after.includes(id));
369
+ if (added.length)
370
+ top.add.unshift(...format(added));
371
+ if (removed.length)
372
+ top.remove.push(...removal(removed));
373
+ }
374
+ var nothingAround = () => ({ before: [], after: [] });
375
+ function steps(groups, dialect, around = nothingAround) {
376
+ const rank = dialect.order ?? (() => 0);
377
+ const ordered = [...groups].sort((a, b) => rank(a.path) - rank(b.path));
378
+ return ordered.map((group) => {
379
+ const { before, after } = around(group);
380
+ const early = group.add.filter((line) => dialect.first?.test(line));
381
+ const late = group.add.filter((line) => !early.includes(line));
382
+ const added = late.flatMap((line) => dialect.block.test(line) ? [line, dialect.leave] : [line]);
383
+ const inner = [...early, ...group.remove.map(dialect.undo), ...added];
384
+ const leave = group.path.map(() => dialect.leave);
385
+ return {
386
+ title: group.path.join(" > ") || "(top level)",
387
+ show: [...group.remove.map((line) => `- ${line}`), ...group.add.map((line) => `+ ${line}`)],
388
+ send: [...before, ...group.path, ...inner, ...leave, ...after]
389
+ };
390
+ });
391
+ }
392
+ var STORED_CREDENTIAL = /(password (?:irreversible-)?cipher|community read cipher|^snmp-server community)( +)\S+/gm;
393
+ var scrub = (text) => text.replace(STORED_CREDENTIAL, "$1$2<redacted>");
394
+
395
+ // src/adapters/devices/raisecom-ros/index.ts
396
+ var DEVICE_FACTS = [
397
+ "Being processed",
398
+ "System current configuration",
399
+ "recv-mode",
400
+ "mpls te periodic-flooding",
401
+ "spanning-tree region-configuration",
402
+ "interface NULL",
403
+ "ip rarp",
404
+ "quit",
405
+ "config",
406
+ "exit",
407
+ "portswitch"
408
+ ];
409
+ var OPPOSITES = [
410
+ [/^mode manual$/, "mode lacp"],
411
+ [/^ip ospf passive-interface enable$/, "ip ospf passive-interface disable"],
412
+ [/^lldp disable$/, "lldp enable"],
413
+ [/^spanning-tree disable$/, "spanning-tree enable"],
414
+ [/^switchport mode trunk$/, "switchport mode access"],
415
+ [/^telnet-server disable$/, "telnet-server enable"],
416
+ [/^shutdown$/, "no shutdown"]
417
+ ];
418
+ var UNDO_BY_KEYWORD = /^(description|switchport access vlan|switchport trunk native vlan|switchport trunk allowed vlan|storm-control broadcast|storm-control mode|ip address|port-channel|ip ospf cost|ip ospf network)\b/;
419
+ function undo(line) {
420
+ for (const [pattern, opposite] of OPPOSITES)
421
+ if (pattern.test(line))
422
+ return opposite;
423
+ if (/^rule \d+/.test(line))
424
+ return `no ${line.split(" ").slice(0, 2).join(" ")}`;
425
+ const vrrp = /^(no )?vrrp (\d+) (ip \S+|priority|timers advertise-interval|preempt)/.exec(line);
426
+ if (vrrp)
427
+ return vrrp[1] ? `vrrp ${vrrp[2]} preempt` : `no vrrp ${vrrp[2]} ${vrrp[3]}`;
428
+ const ospfProcess = /^router ospf (\d+)/.exec(line);
429
+ if (ospfProcess)
430
+ return `no router ospf ${ospfProcess[1]}`;
431
+ const snmpUser = /^snmp-server user (\S+)/.exec(line);
432
+ if (snmpUser)
433
+ return `no snmp-server user ${snmpUser[1]}`;
434
+ const snmpGroup = /^snmp-server group \S+ user (\S+) usm$/.exec(line);
435
+ if (snmpGroup)
436
+ return `no snmp-server group user ${snmpGroup[1]} usm`;
437
+ const snmpAccess = /^snmp-server access (\S+) .* usm (\S+)$/.exec(line);
438
+ if (snmpAccess)
439
+ return `no snmp-server access ${snmpAccess[1]} usm ${snmpAccess[2]}`;
440
+ const keyword = UNDO_BY_KEYWORD.exec(line);
441
+ if (keyword)
442
+ return `no ${keyword[1]}`;
443
+ return `no ${line}`;
444
+ }
445
+ var dialect = {
446
+ block: /^(interface |access-list |router ospf )/,
447
+ indented: false,
448
+ terminator: "!",
449
+ ignore: (line) => line.startsWith("!") || DEVICE_FACTS.some((fact) => line.startsWith(fact)),
450
+ undo,
451
+ leave: "exit",
452
+ canon: (line) => line.replace(/password (cipher )?(\S+)/, "password <secret>").replace(/community <secret:\w+>/, "community <redacted>").replace(/^snmp-server user (\S+) (?:authentication|authkey) (\S+) \S+ (?:privacy|privkey)\s+(\S+) \S+$/, "snmp-server user $1 $2 $3 <secret>").replace(/^(snmp-server access \S+ read \S+) notify internet (usm \S+)$/, "$1 $2"),
453
+ order: (path) => {
454
+ const header = path[0] ?? "";
455
+ if (header.startsWith("interface port-channel"))
456
+ return 1;
457
+ if (header.startsWith("interface "))
458
+ return 2;
459
+ if (header.startsWith("router ospf"))
460
+ return 3;
461
+ return 0;
462
+ }
463
+ };
464
+ var OSPF_PROCESS = 1;
465
+ var SNMP_GROUP = "circuit";
466
+ var SNMP_AUTH = { sha1: "sha" };
467
+ var MANAGEMENT_ACL = 1000;
468
+ function vlanList(ids) {
469
+ const sorted = [...new Set(ids)].sort((a, b) => a - b);
470
+ const ranges = [];
471
+ for (let i = 0;i < sorted.length; i++) {
472
+ let end = i;
473
+ while (sorted[end + 1] === sorted[end] + 1)
474
+ end++;
475
+ ranges.push(i === end ? `${sorted[i]}` : `${sorted[i]}-${sorted[end]}`);
476
+ i = end;
477
+ }
478
+ return ranges.join(",");
479
+ }
480
+ function render(device) {
481
+ const lines = [];
482
+ const vlans = device.vlans ?? {};
483
+ const vlanId = (name) => vlans[name].id;
484
+ const management = device.management ?? {};
485
+ const allow = management.allow ?? [];
486
+ const ids = Object.values(vlans).map((vlan) => vlan.id);
487
+ if (ids.length)
488
+ lines.push(`create vlan ${vlanList(ids)} active`, "!");
489
+ const lagNumbers = lagIds(device);
490
+ for (const [name, lag] of Object.entries(device.interfaces ?? {})) {
491
+ if (lag.type !== "lag")
492
+ continue;
493
+ lines.push(`interface port-channel ${lagNumbers.get(name)}`);
494
+ if (lag.description)
495
+ lines.push(`description ${lag.description}`);
496
+ if (lag.mode === "static")
497
+ lines.push("mode manual");
498
+ lines.push(...switching(lag, vlanId), "!");
499
+ }
500
+ if (allow.length) {
501
+ lines.push(`access-list ${MANAGEMENT_ACL}`, " description management");
502
+ allow.forEach((cidr, index) => lines.push(` rule ${(index + 1) * 10} permit ${host(cidr)} ${netmask(split(cidr).len)}`));
503
+ lines.push(" rule 100 deny any", "!");
504
+ }
505
+ for (const [name, user] of Object.entries(device.users ?? {}))
506
+ lines.push(`user name ${name} password ${marker(user.password)}`);
507
+ if (management.privilege_password)
508
+ lines.push(`enable password ${marker(management.privilege_password)}`);
509
+ const timezone = device.system?.timezone;
510
+ if (timezone) {
511
+ const minutes = offset(timezone);
512
+ const absolute = Math.abs(minutes);
513
+ lines.push(`clock timezone ${minutes < 0 ? "-" : "+"} ${Math.floor(absolute / 60)} ${absolute % 60} 0`);
514
+ }
515
+ lines.push("!");
516
+ for (const port of portsOf(device.platform, device.model)) {
517
+ const settings = device.ports?.[port];
518
+ lines.push(`interface ${spell(device.platform, device.model, port)}`, ...physicalPort(settings, vlanId, lagNumbers), "!");
519
+ }
520
+ const ospf = device.routing?.ospf;
521
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
522
+ if (iface.type !== "vlan")
523
+ continue;
524
+ lines.push(`interface vlan ${vlanId(iface.vlan)}`);
525
+ for (const entry of iface.addresses ?? []) {
526
+ const address = typeof entry === "string" ? entry : entry.address;
527
+ if (familyOf(address) === "ipv4")
528
+ lines.push(`ip address ${host(address)} ${netmask(split(address).len)}`);
529
+ }
530
+ const ospfSettings = Object.values(ospf?.areas ?? {}).find((area) => area.interfaces[name])?.interfaces[name];
531
+ if (ospfSettings?.network === "point-to-point")
532
+ lines.push("ip ospf network ptp");
533
+ if (ospfSettings?.cost !== undefined)
534
+ lines.push(`ip ospf cost ${ospfSettings.cost}`);
535
+ if (ospfSettings?.passive)
536
+ lines.push("ip ospf passive-interface enable");
537
+ for (const group of iface.vrrp ?? [])
538
+ lines.push(...vrrpLines(group));
539
+ lines.push("!");
540
+ }
541
+ if (ospf) {
542
+ const routerId = device.routing?.router_id;
543
+ lines.push(`router ospf ${OSPF_PROCESS}${routerId ? ` router-id ${routerId}` : ""}`);
544
+ for (const [areaId, area] of Object.entries(ospf.areas)) {
545
+ for (const iface of Object.keys(area.interfaces)) {
546
+ for (const prefix of ipv4Networks(device, iface)) {
547
+ lines.push(`network ${host(prefix)} ${wildcard(split(prefix).len)} area ${areaId}`);
548
+ }
549
+ }
550
+ }
551
+ for (const source of ospf.redistribute ?? [])
552
+ lines.push(`redistribute ${source}`);
553
+ lines.push("!");
554
+ }
555
+ if (Object.values(device.ports ?? {}).some((port) => port?.storm_control))
556
+ lines.push("storm-control detection enable");
557
+ if (device.system?.transceiver_monitoring)
558
+ lines.push("transceiver ddm enable");
559
+ if (device.lldp)
560
+ lines.push("lldp enable");
561
+ if (device.stp) {
562
+ lines.push("spanning-tree enable");
563
+ if (device.stp.priority !== undefined)
564
+ lines.push(`spanning-tree priority ${device.stp.priority}`);
565
+ }
566
+ lines.push("no snmp-server community public", "no snmp-server community private");
567
+ const snmp = management.snmp;
568
+ if (snmp?.community)
569
+ lines.push(`snmp-server community ${marker(snmp.community)} ro`);
570
+ const snmpUsers = Object.entries(snmp?.users ?? {});
571
+ if (snmpUsers.length)
572
+ lines.push(`snmp-server access ${SNMP_GROUP} read internet usm authpriv`);
573
+ for (const [name, user] of snmpUsers) {
574
+ const authentication = `authentication ${SNMP_AUTH[user.auth]} ${marker(user.auth_password)}`;
575
+ const privacy = `privacy ${user.privacy} ${marker(user.privacy_password)}`;
576
+ lines.push(`snmp-server user ${name} ${authentication} ${privacy}`, `snmp-server group ${SNMP_GROUP} user ${name} usm`);
577
+ }
578
+ if ((snmp?.community || snmpUsers.length) && allow.length)
579
+ lines.push(`snmp-server access-list ${MANAGEMENT_ACL}`);
580
+ for (const route of device.routing?.static ?? []) {
581
+ const { addr, len } = split(route.prefix);
582
+ if (familyOf(addr) === "ipv4" && route.via)
583
+ lines.push(`ip route ${addr} ${netmask(len)} ${route.via}`);
584
+ }
585
+ if (management.telnet) {
586
+ lines.push("telnet-server enable");
587
+ if (allow.length)
588
+ lines.push(`telnet-server access-list ${MANAGEMENT_ACL}`);
589
+ } else
590
+ lines.push("telnet-server disable");
591
+ const ssh = management.ssh;
592
+ if (ssh?.auth_timeout)
593
+ lines.push(`ssh2 server authentication-timeout ${ssh.auth_timeout}`);
594
+ if (ssh?.auth_retries)
595
+ lines.push(`ssh2 server authentication-retries ${ssh.auth_retries}`);
596
+ if (ssh && allow.length)
597
+ lines.push(`ssh2 access-list ${MANAGEMENT_ACL}`);
598
+ const ntp = device.system?.ntp;
599
+ if (ntp?.servers.length)
600
+ lines.push("ntp enable", ...ntp.servers.map((server) => `ntp server ${server}`));
601
+ return lines.join(`
602
+ `) + `
603
+ `;
604
+ }
605
+ function switching(settings, vlanId) {
606
+ const lines = [];
607
+ if (settings.access_vlan)
608
+ lines.push(`switchport access vlan ${vlanId(settings.access_vlan)}`);
609
+ if (settings.trunk_vlans) {
610
+ if (settings.native_vlan)
611
+ lines.push(`switchport trunk native vlan ${vlanId(settings.native_vlan)}`);
612
+ lines.push(`switchport trunk allowed vlan ${vlanList(settings.trunk_vlans.map(vlanId))}`, "switchport mode trunk");
613
+ }
614
+ return lines;
615
+ }
616
+ function vrrpLines(group) {
617
+ const lines = [`vrrp ${group.id} ip ${group.address}`];
618
+ if (group.interval !== undefined && group.interval !== 1)
619
+ lines.push(`vrrp ${group.id} timers advertise-interval ${group.interval}`);
620
+ if (group.priority !== undefined && group.priority !== 100)
621
+ lines.push(`vrrp ${group.id} priority ${group.priority}`);
622
+ if (!group.preempt)
623
+ lines.push(`no vrrp ${group.id} preempt`);
624
+ return lines;
625
+ }
626
+ function physicalPort(settings, vlanId, lagNumbers) {
627
+ if (!settings)
628
+ return ["shutdown"];
629
+ const lines = [];
630
+ if (settings.description)
631
+ lines.push(`description ${settings.description}`);
632
+ if (settings.lag)
633
+ return [...lines, `port-channel ${lagNumbers.get(settings.lag)}`];
634
+ lines.push(...switching(settings, vlanId));
635
+ if (settings.lldp === false)
636
+ lines.push("lldp disable");
637
+ if (settings.stp === false)
638
+ lines.push("spanning-tree disable");
639
+ const broadcast = settings.storm_control?.broadcast;
640
+ if (broadcast && "pps" in broadcast)
641
+ lines.push("storm-control mode pps", `storm-control broadcast pps ${broadcast.pps}`);
642
+ return lines;
643
+ }
644
+ var HOST_KEYS = ["ssh-rsa"];
645
+ async function open(device) {
646
+ const user = device.users?.[device.connection.user];
647
+ if (!user)
648
+ throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
649
+ const client = await connect({
650
+ host: device.connection.host,
651
+ port: device.connection.port,
652
+ user: device.connection.user,
653
+ password: resolve(user.password),
654
+ hostKeys: HOST_KEYS
655
+ });
656
+ const channel = await shell(client);
657
+ const close = () => {
658
+ channel.end();
659
+ client.end();
660
+ };
661
+ const banner = await readUntil(channel, (text) => /[#>]\s*$/.test(text.trimEnd()), 30000);
662
+ const version = await send(channel, "show version", 15000, 1500);
663
+ if (!/[#>]\s*$/.test(banner.trimEnd()) || !version.includes("ROS")) {
664
+ close();
665
+ throw new Error(`${device.name} did not answer as ROS`);
666
+ }
667
+ await send(channel, "terminal page-break disable", 15000, 1000);
668
+ return { channel, close };
669
+ }
670
+ async function read(device) {
671
+ const { channel, close } = await open(device);
672
+ try {
673
+ const dump = await send(channel, "show running-config", 150000, 6000);
674
+ const lines = dump.replace(/\r/g, "").split(`
675
+ `);
676
+ const start = lines.findIndex((line) => line.trim().startsWith("!ROS version"));
677
+ if (start < 0)
678
+ throw new Error(`${device.name} returned no configuration`);
679
+ const body = lines.slice(start).filter((line) => line.startsWith("!") || !/[#>]\s*$/.test(line.trim()));
680
+ const text = body.join(`
681
+ `).replace(/^!(System (up )?time|Boot times):.*\n/gm, "").trim();
682
+ return scrub(text + `
683
+ `).replace(/(authkey \S+|privkey\s+\S+) \S+/g, "$1 <redacted>");
684
+ } finally {
685
+ close();
686
+ }
687
+ }
688
+ function unsupported(device) {
689
+ const found = [];
690
+ for (const [name, port] of Object.entries(device.ports ?? {})) {
691
+ if (!port)
692
+ continue;
693
+ for (const field of ["speed", "acl", "mtu", "addresses", "vrrp", "vrf"]) {
694
+ if (port[field] !== undefined)
695
+ found.push(`ports.${name}.${field}`);
696
+ }
697
+ if (port.lldp === true)
698
+ found.push(`ports.${name}.lldp true, LLDP is turned on per device`);
699
+ if (typeof port.stp === "object" || port.stp === true)
700
+ found.push(`ports.${name}.stp other than false`);
701
+ const storm = port.storm_control;
702
+ if (storm && (storm.multicast || storm.unknown_unicast || storm.broadcast && ("percent" in storm.broadcast))) {
703
+ found.push(`ports.${name}.storm_control other than broadcast in pps`);
704
+ }
705
+ }
706
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
707
+ if (iface.type === "lag") {
708
+ for (const field of ["addresses", "vrrp", "vrf", "mtu", "storm_control", "stp"]) {
709
+ if (iface[field] !== undefined)
710
+ found.push(`interfaces.${name}.${field}, a LAG here only switches`);
711
+ }
712
+ continue;
713
+ }
714
+ if (iface.type !== "vlan") {
715
+ found.push(`interfaces.${name}, a ${iface.type}`);
716
+ continue;
717
+ }
718
+ if (iface.vrf)
719
+ found.push(`interfaces.${name}.vrf`);
720
+ for (const group of iface.vrrp ?? []) {
721
+ if (familyOf(group.address) === "ipv6")
722
+ found.push(`interfaces.${name}.vrrp for IPv6`);
723
+ }
724
+ }
725
+ const routing = device.routing;
726
+ for (const field of ["bgp", "rpki"]) {
727
+ if (routing?.[field])
728
+ found.push(`routing.${field}`);
729
+ }
730
+ const ospf = routing?.ospf;
731
+ if (ospf?.families?.includes("ipv6"))
732
+ found.push("routing.ospf for IPv6");
733
+ for (const [areaId, area] of Object.entries(ospf?.areas ?? {})) {
734
+ for (const [name, settings] of Object.entries(area.interfaces)) {
735
+ if (device.interfaces?.[name]?.type !== "vlan")
736
+ found.push(`routing.ospf.areas.${areaId}.${name}, OSPF runs on VLAN interfaces here`);
737
+ if (settings?.bfd)
738
+ found.push(`routing.ospf.areas.${areaId}.${name}.bfd`);
739
+ }
740
+ }
741
+ for (const route of routing?.static ?? []) {
742
+ if (!route.via || route.vrf)
743
+ found.push(`routing.static ${route.prefix}, only an IPv4 next hop in the default table`);
744
+ }
745
+ const UNSUPPORTED_SECTIONS = [
746
+ "firewall",
747
+ "dhcp",
748
+ "dhcp_relay",
749
+ "acls",
750
+ "policies",
751
+ "prefix_sets",
752
+ "hardware_offload",
753
+ "certificates",
754
+ "vrfs",
755
+ "flow_export"
756
+ ];
757
+ for (const field of UNSUPPORTED_SECTIONS) {
758
+ if (device[field] !== undefined)
759
+ found.push(field);
760
+ }
761
+ const management = device.management ?? {};
762
+ for (const field of ["http", "https", "api", "api_tls", "ftp", "native", "console"]) {
763
+ if (management[field])
764
+ found.push(`management.${field}`);
765
+ }
766
+ for (const [name, user] of Object.entries(management.snmp?.users ?? {})) {
767
+ if (!SNMP_AUTH[user.auth])
768
+ found.push(`management.snmp.users.${name}.auth ${user.auth}, the platform authenticates with SHA1 or MD5`);
769
+ }
770
+ if (device.lldp && device.lldp !== true)
771
+ found.push("lldp.interfaces, LLDP is per device");
772
+ if (device.stp?.mode && device.stp.mode !== "mstp")
773
+ found.push("stp.mode other than mstp");
774
+ for (const [name, user] of Object.entries(device.users ?? {})) {
775
+ if (user.role !== "admin")
776
+ found.push(`users.${name}.role other than admin`);
777
+ }
778
+ const system = device.system ?? {};
779
+ if (system.dns || system.logging || system.release_channel || system.banner || system.ntp?.serve) {
780
+ found.push("system.dns, logging, release_channel, banner and ntp.serve");
781
+ }
782
+ return found;
783
+ }
784
+ var REFUSED = /^(%|Error|Incomplete|Unrecognized)|can not|cannot |is in use|Wrong parameter|invalid|unsuccessful/im;
785
+ var CONFIRM = /^(portswitch|switchport trunk allowed vlan|switchport mode)/;
786
+ var RELEASE = /^no (\S+ access-list \d+)$/;
787
+ async function apply(device, _network, plan) {
788
+ const { channel, close } = await open(device);
789
+ const released = [];
790
+ try {
791
+ await send(channel, "config", 1e4);
792
+ for (const step of plan.steps) {
793
+ for (const line of step.send) {
794
+ let answer = await send(channel, substitute(line), 20000, 2000);
795
+ if (/[[(]y\/n[\])]|input 'y'/i.test(answer) || CONFIRM.test(line))
796
+ answer += await send(channel, "y", 1e4, 1000);
797
+ if (REFUSED.test(answer))
798
+ throw new Error(`${step.title}: ${line}
799
+ device said: ${answer.trim().slice(0, 300)}`);
800
+ const release = RELEASE.exec(line);
801
+ if (release)
802
+ released.push(release[1]);
803
+ else if (released.includes(line))
804
+ released.splice(released.indexOf(line), 1);
805
+ }
806
+ }
807
+ await send(channel, "exit", 1e4);
808
+ } catch (error) {
809
+ await send(channel, "end", 1e4).catch(() => {
810
+ return;
811
+ });
812
+ await send(channel, "config", 1e4).catch(() => {
813
+ return;
814
+ });
815
+ for (const binding of released)
816
+ await send(channel, binding, 1e4).catch(() => {
817
+ return;
818
+ });
819
+ if (released.length)
820
+ console.log(` bound again: ${released.join(", ")}`);
821
+ throw error;
822
+ } finally {
823
+ close();
824
+ }
825
+ const proof = await open(device).catch(() => {
826
+ return;
827
+ });
828
+ if (!proof)
829
+ throw new Error("applied, but a fresh login failed. Not written: a reboot restores the previous configuration. Recover from the console.");
830
+ await send(proof.channel, "write", 60000, 3000);
831
+ proof.close();
832
+ console.log(" applied, login proved, written");
833
+ }
834
+ var holders = (config) => [...config.matchAll(new RegExp(`^([a-z0-9-]+) access-list ${MANAGEMENT_ACL}$`, "gm"))].map((match) => match[1]);
835
+ function createdIds(line) {
836
+ const list = line.replace(/^create vlan /, "").replace(/ active$/, "");
837
+ const ids = [];
838
+ for (const part of list.split(",")) {
839
+ const [first, last] = part.split("-").map(Number);
840
+ for (let id = first;id <= (last ?? first); id++)
841
+ ids.push(id);
842
+ }
843
+ return ids;
844
+ }
845
+ function plan(desired, current) {
846
+ const groups = compare(desired, current, dialect);
847
+ const existing = entries(current, dialect);
848
+ for (const group of groups) {
849
+ if (!group.path[0]?.startsWith("access-list "))
850
+ continue;
851
+ for (const line of group.add) {
852
+ const number = /^rule (\d+) /.exec(line)?.[1];
853
+ if (!number)
854
+ continue;
855
+ const old = existing.find((entry) => entry.path.join(`
856
+ `) === group.path.join(`
857
+ `) && entry.line.startsWith(`rule ${number} `) && entry.line !== line);
858
+ if (old && !group.remove.includes(old.line))
859
+ group.remove.push(old.line);
860
+ }
861
+ }
862
+ vlanListChange(groups, "create vlan ", createdIds, (ids) => [`create vlan ${vlanList(ids)} active`], (ids) => ids.map((id) => `vlan ${id}`));
863
+ for (const group of groups) {
864
+ const isPort = group.path[0]?.startsWith("interface ") ?? false;
865
+ const switches = group.add.some((line) => line.startsWith("switchport") || line.startsWith("port-channel"));
866
+ if (isPort && switches)
867
+ group.add.unshift("portswitch");
868
+ }
869
+ const lagRemovals = [];
870
+ for (const group of groups) {
871
+ if (group.path.length)
872
+ continue;
873
+ lagRemovals.push(...group.remove.filter((line) => line.startsWith("interface port-channel")));
874
+ group.remove = group.remove.filter((line) => !line.startsWith("interface port-channel"));
875
+ }
876
+ const releaseAndBind = (group) => {
877
+ if (group.path[0] !== `access-list ${MANAGEMENT_ACL}`)
878
+ return { before: [], after: [] };
879
+ return {
880
+ before: holders(current).map((service) => `no ${service} access-list ${MANAGEMENT_ACL}`),
881
+ after: holders(desired).map((service) => `${service} access-list ${MANAGEMENT_ACL}`)
882
+ };
883
+ };
884
+ const planned = steps(groups.filter((group) => group.add.length || group.remove.length), dialect, releaseAndBind);
885
+ if (lagRemovals.length) {
886
+ const removeLags = {
887
+ title: "LAGs no longer used",
888
+ show: lagRemovals.map((line) => `- ${line}`),
889
+ send: lagRemovals.map(undo)
890
+ };
891
+ planned.push(removeLags);
892
+ }
893
+ return { steps: planned, problems: [] };
894
+ }
895
+ var raisecom = { extension: ".txt", unsupported, render, read, plan, apply };
896
+
897
+ // src/adapters/devices/routeros/export.ts
898
+ var unwrap = (text) => text.replace(/\r/g, "").replace(/\\\n\s*/g, "");
899
+ function unquote(value) {
900
+ if (!value.startsWith('"'))
901
+ return value;
902
+ return value.slice(1, -1).replace(/\\(.)/g, (_, escaped) => escaped === "_" ? " " : escaped);
903
+ }
904
+ function words(line) {
905
+ const result = [];
906
+ let current = "";
907
+ let inQuotes = false;
908
+ let bracketDepth = 0;
909
+ for (let i = 0;i < line.length; i++) {
910
+ const char = line[i];
911
+ if (inQuotes) {
912
+ current += char;
913
+ if (char === "\\")
914
+ current += line[++i] ?? "";
915
+ else if (char === '"')
916
+ inQuotes = false;
917
+ continue;
918
+ }
919
+ if (char === '"')
920
+ inQuotes = true;
921
+ if (char === "[")
922
+ bracketDepth++;
923
+ if (char === "]")
924
+ bracketDepth--;
925
+ if (char === " " && bracketDepth === 0) {
926
+ if (current)
927
+ result.push(current);
928
+ current = "";
929
+ continue;
930
+ }
931
+ current += char;
932
+ }
933
+ if (current)
934
+ result.push(current);
935
+ return result;
936
+ }
937
+ function parseLine(menu, line) {
938
+ const [verb, ...rest] = words(line);
939
+ let selector = "";
940
+ const first = rest[0];
941
+ if (verb === "set" && first && (first.startsWith("[") || !first.includes("="))) {
942
+ selector = rest.shift().replace(/\s+/g, " ");
943
+ }
944
+ const flags = [];
945
+ const attrs = {};
946
+ let group = "";
947
+ for (const word of rest) {
948
+ const equals = word.indexOf("=");
949
+ if (equals < 0) {
950
+ flags.push(word);
951
+ continue;
952
+ }
953
+ let name = word.slice(0, equals);
954
+ if (name.startsWith("."))
955
+ name = group + name;
956
+ else if (name.includes("."))
957
+ group = name.split(".")[0];
958
+ attrs[name] = unquote(word.slice(equals + 1));
959
+ }
960
+ return { menu, verb, selector, flags, attrs };
961
+ }
962
+ function parse(text) {
963
+ const commands = [];
964
+ const generated = new Set;
965
+ let menu = "";
966
+ for (const raw of unwrap(text).split(`
967
+ `)) {
968
+ const line = raw.trim();
969
+ if (line.startsWith("/")) {
970
+ menu = line;
971
+ continue;
972
+ }
973
+ const marker2 = /^# generated (\S+)$/.exec(line);
974
+ if (marker2)
975
+ generated.add(marker2[1]);
976
+ if (menu && /^(add|set)( |$)/.test(line))
977
+ commands.push(parseLine(menu, line));
978
+ }
979
+ return { commands, generated };
980
+ }
981
+ var DEFAULTS = {
982
+ "*": { disabled: "no", comment: "", instance: "default", "routing-table": "main", vrf: "main" },
983
+ "/interface bridge": { "protocol-mode": "rstp", priority: "0x8000", "vlan-filtering": "no", pvid: "1" },
984
+ "/interface bridge port": { pvid: "1", "frame-types": "admit-all", edge: "auto", "path-cost": "10", priority: "0x80" },
985
+ "/interface bonding": { mode: "balance-rr", "transmit-hash-policy": "layer-2", mtu: "1500" },
986
+ "/interface ethernet": { mtu: "1500", "auto-negotiation": "yes" },
987
+ "/interface ethernet switch port": {
988
+ "storm-rate": "100",
989
+ "limit-broadcasts": "yes",
990
+ "limit-unknown-multicasts": "no",
991
+ "limit-unknown-unicasts": "no"
992
+ },
993
+ "/interface vlan": { "l3-hw-offloading": "yes", mtu: "1500" },
994
+ "/interface vrrp": { priority: "100", "preemption-mode": "yes", interval: "1s", version: "3", "v3-protocol": "ipv4" },
995
+ "/interface vxlan": { port: "4789" },
996
+ "/interface wireguard peers": {
997
+ "client-allowed-address": "",
998
+ "endpoint-address": "",
999
+ "endpoint-port": "0",
1000
+ "persistent-keepalive": "0s"
1001
+ },
1002
+ "/ipv6 address": { advertise: "yes" },
1003
+ "/ip dhcp-server": { "lease-time": "30m" },
1004
+ "/ip dns": { servers: "", "allow-remote-requests": "no" },
1005
+ "/ip service": { address: "", certificate: "none" },
1006
+ "/ip ssh": { "strong-crypto": "no" },
1007
+ "/ip traffic-flow": { enabled: "no", interfaces: "all", "packet-sampling": "no" },
1008
+ "/tool mac-server": { "allowed-interface-list": "all" },
1009
+ "/tool mac-server mac-winbox": { "allowed-interface-list": "all" },
1010
+ "/tool mac-server ping": { enabled: "yes" },
1011
+ "/tool bandwidth-server": { enabled: "yes" },
1012
+ "/ip neighbor discovery-settings": { "discover-interface-list": "!dynamic", protocol: "cdp,lldp,mndp" },
1013
+ "/snmp": { enabled: "no", contact: "", location: "" },
1014
+ "/snmp community": {
1015
+ addresses: "::/0",
1016
+ name: "public",
1017
+ security: "none",
1018
+ "authentication-protocol": "MD5",
1019
+ "encryption-protocol": "DES",
1020
+ "read-access": "yes",
1021
+ "write-access": "no"
1022
+ },
1023
+ "/ip settings": { "send-redirects": "yes", "tcp-syncookies": "no" },
1024
+ "/system clock": { "time-zone-autodetect": "yes", "time-zone-name": "manual" },
1025
+ "/system note": { note: "", "show-at-login": "yes" },
1026
+ "/system ntp client": { enabled: "no" },
1027
+ "/system ntp server": { enabled: "no" },
1028
+ "/system logging action": { "disk-file-count": "2", "disk-lines-per-file": "1000" },
1029
+ "/system package update": { channel: "stable" },
1030
+ "/system routerboard settings": { "enter-setup-on": "any-key" },
1031
+ "/interface ethernet switch": { "l3-hw-offloading": "no" },
1032
+ "/interface ethernet switch l3hw-settings": { autorestart: "no", "ipv6-hw": "no" }
1033
+ };
1034
+ var SERVICE_DISABLED = {
1035
+ ftp: "no",
1036
+ ssh: "no",
1037
+ telnet: "no",
1038
+ www: "no",
1039
+ "www-ssl": "yes",
1040
+ "reverse-proxy": "no",
1041
+ winbox: "no",
1042
+ api: "no",
1043
+ "api-ssl": "no"
1044
+ };
1045
+ var HELPER_DISABLED = { irc: "yes", rtsp: "yes" };
1046
+ function defaultOf(command, property) {
1047
+ if (command.menu === "/ip service" && property === "disabled")
1048
+ return SERVICE_DISABLED[command.selector];
1049
+ if (command.menu === "/ip firewall service-port" && property === "disabled")
1050
+ return HELPER_DISABLED[command.selector] ?? "no";
1051
+ return DEFAULTS[command.menu]?.[property] ?? DEFAULTS["*"][property];
1052
+ }
1053
+ var CLEARED_WITH = {
1054
+ "/interface ethernet": { speed: "auto-negotiation" }
1055
+ };
1056
+ var SECRET_KEYS = new Set(["private-key", "password", "tcp-md5-key", "authentication-password", "encryption-password"]);
1057
+ var isSecretMarker = (value) => value.startsWith("<secret:");
1058
+ var canonicalRule = (rule) => rule.replace(/\s*([=&|;{}(),<>!]+)\s*/g, "$1").replace(/\s+/g, " ").trim();
1059
+ function normalValue(command, property, value) {
1060
+ if (property === "rule")
1061
+ return canonicalRule(value);
1062
+ if (property === "tagged" || property === "untagged" || property === "slaves")
1063
+ return value.split(",").sort().join(",");
1064
+ const onInterface = property === "address" && (command.menu === "/ip address" || command.menu === "/ipv6 address");
1065
+ const isAddress2 = /(^|-)address$/.test(property) && !property.endsWith("mac-address");
1066
+ if (onInterface && !value.includes("/"))
1067
+ return `${value}/${value.includes(":") ? 64 : 32}`;
1068
+ if (!onInterface && isAddress2 && /^[\d.]+\/32$/.test(value))
1069
+ return value.slice(0, -3);
1070
+ if (!onInterface && isAddress2 && value.includes(":") && !value.includes("/") && !value.includes(","))
1071
+ return `${value}/128`;
1072
+ return value;
1073
+ }
1074
+ function normal(command) {
1075
+ const result = {};
1076
+ for (const [property, raw] of Object.entries(command.attrs)) {
1077
+ if (SECRET_KEYS.has(property) || isSecretMarker(raw))
1078
+ continue;
1079
+ const value = normalValue(command, property, raw);
1080
+ if (value === defaultOf(command, property))
1081
+ continue;
1082
+ result[property] = value;
1083
+ }
1084
+ if (command.menu === "/ip address" && result.network && result.address && network(result.address).split("/")[0] === result.network)
1085
+ delete result.network;
1086
+ return result;
1087
+ }
1088
+ var IDENTITY = {
1089
+ "/interface wireguard peers": ["public-key"],
1090
+ "/interface list member": ["list", "interface"],
1091
+ "/ip firewall address-list": ["list", "address"],
1092
+ "/ipv6 firewall address-list": ["list", "address"],
1093
+ "/interface bridge port": ["interface"],
1094
+ "/interface bridge vlan": ["vlan-ids"],
1095
+ "/interface vxlan vteps": ["interface", "remote-ip"],
1096
+ "/routing bgp connection": ["remote.address"],
1097
+ "/routing rpki": ["address"],
1098
+ "/routing ospf interface-template": ["area", "interfaces"],
1099
+ "/routing bfd configuration": ["interfaces"],
1100
+ "/ip route": ["dst-address", "routing-table"],
1101
+ "/ipv6 route": ["dst-address", "routing-table"],
1102
+ "/ip address": ["address"],
1103
+ "/ipv6 address": ["address"],
1104
+ "/ip dhcp-server network": ["address"],
1105
+ "/ip dhcp-server lease": ["address"],
1106
+ "/ip traffic-flow target": ["dst-address", "port"],
1107
+ "/system ntp client servers": ["address"],
1108
+ "/system logging": ["action", "topics"],
1109
+ "/user ssh-keys": ["user", "key"]
1110
+ };
1111
+ function identity(command) {
1112
+ if (command.menu === "/interface bridge" && command.attrs["vlan-filtering"] === "yes")
1113
+ return { "vlan-filtering": "yes" };
1114
+ const normalized = normal(command);
1115
+ const properties = IDENTITY[command.menu] ?? ["name"];
1116
+ const result = {};
1117
+ for (const property of properties) {
1118
+ const value = normalized[property] ?? command.attrs[property] ?? defaultOf(command, property) ?? "";
1119
+ result[property] = value;
1120
+ }
1121
+ return result;
1122
+ }
1123
+ var ORDERED = new Set([
1124
+ "/routing filter rule",
1125
+ "/ip firewall filter",
1126
+ "/ipv6 firewall filter",
1127
+ "/ip firewall nat",
1128
+ "/ipv6 firewall nat",
1129
+ "/ip firewall mangle",
1130
+ "/ipv6 firewall mangle",
1131
+ "/ip firewall raw",
1132
+ "/ipv6 firewall raw",
1133
+ "/interface bridge filter",
1134
+ "/interface ethernet switch rule"
1135
+ ]);
1136
+ function canonical(command) {
1137
+ const properties = Object.entries(normal(command)).sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => `${name}=${value}`);
1138
+ const flags = [...command.flags].sort();
1139
+ return [command.verb, command.selector, ...flags, ...properties].filter(Boolean).join(" ");
1140
+ }
1141
+
1142
+ // src/adapters/devices/routeros/plan.ts
1143
+ var INSTALL_CERTIFICATE = "#install-certificate ";
1144
+ var ORDER = [
1145
+ "/certificate",
1146
+ "/interface bridge",
1147
+ "/interface ethernet",
1148
+ "/interface bonding",
1149
+ "/interface vlan",
1150
+ "/interface gre",
1151
+ "/interface vxlan",
1152
+ "/interface wireguard",
1153
+ "/interface vrrp",
1154
+ "/interface bridge port",
1155
+ "/interface bridge vlan",
1156
+ "/interface bridge filter",
1157
+ "/interface list",
1158
+ "/ip vrf",
1159
+ "/ip address",
1160
+ "/ipv6 address",
1161
+ "/ip pool",
1162
+ "/ip dhcp-server",
1163
+ "/ip firewall address-list",
1164
+ "/ipv6 firewall address-list",
1165
+ "/ip firewall",
1166
+ "/ipv6 firewall",
1167
+ "/routing rpki",
1168
+ "/routing filter rule",
1169
+ "/routing bgp instance",
1170
+ "/routing bgp connection",
1171
+ "/routing ospf instance",
1172
+ "/routing ospf area",
1173
+ "/routing ospf interface-template",
1174
+ "/ip",
1175
+ "/ipv6",
1176
+ "/routing",
1177
+ "/interface ethernet switch",
1178
+ "/snmp",
1179
+ "/tool",
1180
+ "/user",
1181
+ "/system"
1182
+ ];
1183
+ function rank(menu) {
1184
+ let best = ORDER.length;
1185
+ let bestLength = 0;
1186
+ ORDER.forEach((prefix, index) => {
1187
+ const matches = menu === prefix || menu.startsWith(prefix + " ");
1188
+ if (matches && prefix.length > bestLength) {
1189
+ best = index;
1190
+ bestLength = prefix.length;
1191
+ }
1192
+ });
1193
+ return best;
1194
+ }
1195
+ var DYNAMIC = new Set([
1196
+ "/ip route",
1197
+ "/ipv6 route",
1198
+ "/ip address",
1199
+ "/ipv6 address",
1200
+ "/ip firewall address-list",
1201
+ "/ipv6 firewall address-list",
1202
+ "/ip firewall filter",
1203
+ "/ipv6 firewall filter",
1204
+ "/ip firewall nat",
1205
+ "/ip dhcp-server lease"
1206
+ ]);
1207
+ var quote = (value) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$")}"`;
1208
+ var value = (raw) => /^[\w.:/@,!-]+$/.test(raw) ? raw : quote(raw);
1209
+ var findValue = (raw) => /^[\w.:-]+$/.test(raw) ? raw : quote(raw);
1210
+ function find(menu, id) {
1211
+ const conditions = Object.entries(id).map(([property, raw]) => `${property}=${findValue(raw)}`);
1212
+ if (DYNAMIC.has(menu))
1213
+ conditions.push("!dynamic");
1214
+ return `[ find ${conditions.join(" and ")} ]`;
1215
+ }
1216
+ function line(menu, verb, selector, flags, attrs) {
1217
+ const properties = Object.entries(attrs).map(([property, raw]) => `${property}=${value(raw)}`);
1218
+ return [menu, verb, selector, ...flags, ...properties].filter(Boolean).join(" ");
1219
+ }
1220
+ var describe = (attrs) => Object.entries(attrs).map(([property, raw]) => `${property}=${raw}`).join(" ");
1221
+ function byMenu(commands) {
1222
+ const groups = new Map;
1223
+ for (const command of commands) {
1224
+ const group = groups.get(command.menu) ?? [];
1225
+ group.push(command);
1226
+ groups.set(command.menu, group);
1227
+ }
1228
+ return groups;
1229
+ }
1230
+ function changes(wanted, existing, sendSecrets, problems, label) {
1231
+ const want = normal(wanted);
1232
+ const have = existing ? normal(existing) : {};
1233
+ const result = {};
1234
+ for (const [property, desired] of Object.entries(want)) {
1235
+ if (have[property] !== desired)
1236
+ result[property] = desired;
1237
+ }
1238
+ for (const property of Object.keys(have)) {
1239
+ if (property in want)
1240
+ continue;
1241
+ const clearedWith = CLEARED_WITH[wanted.menu]?.[property];
1242
+ if (clearedWith && !(clearedWith in want))
1243
+ continue;
1244
+ const fallback = defaultOf(wanted, property);
1245
+ if (fallback === undefined) {
1246
+ problems.push(`${label}: ${property}=${have[property]} is set on the device and its default is unknown; add it to DEFAULTS`);
1247
+ } else {
1248
+ result[property] = fallback;
1249
+ }
1250
+ }
1251
+ if (sendSecrets) {
1252
+ for (const [property, raw] of Object.entries(wanted.attrs)) {
1253
+ if (SECRET_KEYS.has(property) || isSecretMarker(raw))
1254
+ result[property] = raw;
1255
+ }
1256
+ }
1257
+ return result;
1258
+ }
1259
+ function planSettings(menu, wanted, existing, sendSecrets, problems) {
1260
+ const step = { title: menu, show: [], send: [] };
1261
+ const wantedBySelector = new Map(wanted.filter((item) => item.verb === "set").map((item) => [item.selector, item]));
1262
+ const existingBySelector = new Map(existing.filter((item) => item.verb === "set").map((item) => [item.selector, item]));
1263
+ for (const selector of new Set([...wantedBySelector.keys(), ...existingBySelector.keys()])) {
1264
+ const want = wantedBySelector.get(selector);
1265
+ const have = existingBySelector.get(selector);
1266
+ const target = want ?? { ...have, attrs: {} };
1267
+ const update = changes(target, have, sendSecrets, problems, `${menu} ${selector}`);
1268
+ if (!Object.keys(update).length)
1269
+ continue;
1270
+ step.show.push(`~ set ${selector} ${describe(update)}`.replace(/\s+/g, " "));
1271
+ step.send.push(line(menu, "set", selector, [], update));
1272
+ }
1273
+ return step.send.length ? step : undefined;
1274
+ }
1275
+ function planEntries(menu, wanted, existing, sendSecrets, problems) {
1276
+ const result = { first: [], last: [] };
1277
+ const changeStep = { title: menu, show: [], send: [] };
1278
+ const removeStep = { title: menu, show: [], send: [] };
1279
+ const key = (c) => JSON.stringify(identity(c));
1280
+ const wantedEntries = wanted.filter((item) => item.verb === "add");
1281
+ const existingEntries = existing.filter((item) => item.verb === "add");
1282
+ const existingByKey = new Map;
1283
+ for (const entry of existingEntries) {
1284
+ const entryKey = key(entry);
1285
+ if (existingByKey.has(entryKey))
1286
+ problems.push(`${menu}: two entries share ${entryKey}, so neither can be addressed alone`);
1287
+ existingByKey.set(entryKey, entry);
1288
+ }
1289
+ const kept = new Set;
1290
+ for (const entry of wantedEntries) {
1291
+ const entryKey = key(entry);
1292
+ kept.add(entryKey);
1293
+ const current = existingByKey.get(entryKey);
1294
+ if (!current) {
1295
+ changeStep.show.push(`+ ${canonical(entry)}`);
1296
+ changeStep.send.push(line(menu, "add", "", entry.flags, entry.attrs));
1297
+ continue;
1298
+ }
1299
+ const flagsDiffer = [...entry.flags].sort().join() !== [...current.flags].sort().join();
1300
+ if (flagsDiffer) {
1301
+ changeStep.show.push(`~ replace ${canonical(entry)}`);
1302
+ removeStep.send.push(`${menu} remove ${find(menu, identity(current))}`);
1303
+ changeStep.send.push(line(menu, "add", "", entry.flags, entry.attrs));
1304
+ continue;
1305
+ }
1306
+ const update = changes(entry, current, sendSecrets, problems, `${menu} ${entryKey}`);
1307
+ if (!Object.keys(update).length)
1308
+ continue;
1309
+ changeStep.show.push(`~ ${entryKey} ${describe(update)}`);
1310
+ changeStep.send.push(line(menu, "set", find(menu, identity(current)), [], update));
1311
+ }
1312
+ const removed = existingEntries.filter((entry) => !kept.has(key(entry)));
1313
+ const wholeLists = new Set;
1314
+ if (menu.endsWith("address-list")) {
1315
+ for (const entry of removed) {
1316
+ const list = entry.attrs.list;
1317
+ if (!wantedEntries.some((candidate) => candidate.attrs.list === list))
1318
+ wholeLists.add(list);
1319
+ }
1320
+ }
1321
+ for (const list of wholeLists) {
1322
+ const count = removed.filter((entry) => entry.attrs.list === list).length;
1323
+ removeStep.show.push(`- list ${list}, ${count} entr${count === 1 ? "y" : "ies"}`);
1324
+ removeStep.send.push(`${menu} remove [ find list=${quote(list)} and !dynamic ]`);
1325
+ }
1326
+ for (const entry of removed) {
1327
+ if (wholeLists.has(entry.attrs.list ?? ""))
1328
+ continue;
1329
+ removeStep.show.push(`- ${canonical(entry)}`);
1330
+ removeStep.send.push(`${menu} remove ${find(menu, identity(entry))}`);
1331
+ }
1332
+ if (changeStep.send.length)
1333
+ result.first.push(changeStep);
1334
+ if (removeStep.send.length)
1335
+ result.last.push(removeStep);
1336
+ return result;
1337
+ }
1338
+ function planCertificates(wanted, existing) {
1339
+ const menu = "/certificate";
1340
+ const result = { first: [], last: [] };
1341
+ const install = { title: menu, show: [], send: [] };
1342
+ const remove = { title: menu, show: [], send: [] };
1343
+ const existingByName = new Map(existing.map((item) => [item.attrs.name, item]));
1344
+ const wantedNames = new Set(wanted.map((item) => item.attrs.name));
1345
+ const renamed = new Set;
1346
+ for (const entry of wanted) {
1347
+ const name = entry.attrs.name;
1348
+ const current = existingByName.get(name);
1349
+ if (current && canonical(current) === canonical(entry))
1350
+ continue;
1351
+ const sameCertificate = existing.find((item) => !wantedNames.has(item.attrs.name) && !renamed.has(item.attrs.name) && item.attrs.fingerprint === entry.attrs.fingerprint && item.attrs["private-key"] === entry.attrs["private-key"]);
1352
+ if (!current && sameCertificate) {
1353
+ const oldName = sameCertificate.attrs.name;
1354
+ renamed.add(oldName);
1355
+ install.show.push(`~ rename ${oldName} to ${name}`);
1356
+ install.send.push(`${menu} set [ find name=${quote(oldName)} ] name=${quote(name)}`);
1357
+ continue;
1358
+ }
1359
+ install.show.push(`${current ? "~" : "+"} install ${name}`);
1360
+ if (current)
1361
+ install.send.push(`${menu} remove [ find name=${quote(name)} ]`);
1362
+ install.send.push(INSTALL_CERTIFICATE + name);
1363
+ }
1364
+ for (const entry of existing) {
1365
+ if (wantedNames.has(entry.attrs.name) || renamed.has(entry.attrs.name))
1366
+ continue;
1367
+ remove.show.push(`- ${entry.attrs.name}`);
1368
+ remove.send.push(`${menu} remove [ find name=${quote(entry.attrs.name)} ]`);
1369
+ }
1370
+ if (install.send.length)
1371
+ result.first.push(install);
1372
+ if (remove.send.length)
1373
+ result.last.push(remove);
1374
+ return result;
1375
+ }
1376
+ var OLD_RULES = "frameworkOldRules";
1377
+ function planOrdered(menu, wanted, existing) {
1378
+ const result = { first: [], last: [] };
1379
+ const same = wanted.map((item) => canonical(item)).join(`
1380
+ `) === existing.map((item) => canonical(item)).join(`
1381
+ `);
1382
+ if (same)
1383
+ return result;
1384
+ const where = DYNAMIC.has(menu) ? " where !dynamic" : "";
1385
+ const hasChains = [...wanted, ...existing].some((item) => item.attrs.chain !== undefined);
1386
+ const keptChains = new Set(wanted.map((item) => item.attrs.chain ?? ""));
1387
+ const droppedChains = [...new Set(existing.map((item) => item.attrs.chain ?? ""))].filter((chain) => !keptChains.has(chain));
1388
+ let removeOld = `${menu} remove $${OLD_RULES}`;
1389
+ if (hasChains) {
1390
+ const keep = [...keptChains].map((chain) => quote(chain)).join(";");
1391
+ const inKeptChain = `[:typeof [:find $keep [${menu} get $id chain]]] != "nil"`;
1392
+ removeOld = `:local keep {${keep}}; :foreach id in=$${OLD_RULES} do={ :if (${inKeptChain}) do={ ${menu} remove $id } }`;
1393
+ }
1394
+ const send2 = [
1395
+ `:global ${OLD_RULES} [${menu} find${where}]`,
1396
+ ...wanted.map((item) => line(menu, "add", "", item.flags, item.attrs)),
1397
+ `:global ${OLD_RULES}; ${removeOld}; /system script environment remove [find name="${OLD_RULES}"]`
1398
+ ];
1399
+ const show = [...existing.map((item) => `- ${canonical(item)}`), ...wanted.map((item) => `+ ${canonical(item)}`)];
1400
+ result.first.push({ title: `${menu}, in the order of the config`, show, send: send2 });
1401
+ if (hasChains) {
1402
+ for (const chain of droppedChains) {
1403
+ const selector = `[ find chain=${quote(chain)}${DYNAMIC.has(menu) ? " and !dynamic" : ""} ]`;
1404
+ result.last.push({ title: `${menu} chain ${chain}`, show: [`- chain ${chain}`], send: [`${menu} remove ${selector}`] });
1405
+ }
1406
+ }
1407
+ return result;
1408
+ }
1409
+ function plan2(desired, current, options) {
1410
+ const want = parse(desired);
1411
+ const have = parse(current);
1412
+ const owned = (command) => !(command.menu.endsWith("address-list") && want.generated.has(command.attrs.list ?? ""));
1413
+ const wantedByMenu = byMenu(want.commands.filter(owned));
1414
+ const existingByMenu = byMenu(have.commands.filter(owned));
1415
+ const menus = [...new Set([...wantedByMenu.keys(), ...existingByMenu.keys()])].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
1416
+ const first = [];
1417
+ const last = [];
1418
+ const problems = [];
1419
+ for (const menu of menus) {
1420
+ const wanted = wantedByMenu.get(menu) ?? [];
1421
+ const existing = existingByMenu.get(menu) ?? [];
1422
+ let menuPlan;
1423
+ if (menu === "/certificate")
1424
+ menuPlan = planCertificates(wanted, existing);
1425
+ else if (ORDERED.has(menu))
1426
+ menuPlan = planOrdered(menu, wanted, existing);
1427
+ else {
1428
+ menuPlan = planEntries(menu, wanted, existing, options.secrets, problems);
1429
+ const settings = planSettings(menu, wanted, existing, options.secrets, problems);
1430
+ if (settings)
1431
+ menuPlan.first.unshift(settings);
1432
+ }
1433
+ first.push(...menuPlan.first);
1434
+ last.unshift(...menuPlan.last);
1435
+ }
1436
+ return { steps: [...first, ...last], problems };
1437
+ }
1438
+
1439
+ // src/adapters/devices/routeros/render.ts
1440
+ import { X509Certificate } from "node:crypto";
1441
+ class Output {
1442
+ menus = new Map;
1443
+ add(menu, command) {
1444
+ const commands = this.menus.get(menu) ?? [];
1445
+ commands.push(command);
1446
+ this.menus.set(menu, commands);
1447
+ }
1448
+ text() {
1449
+ const blocks = [...this.menus].map(([menu, commands]) => [menu, ...commands].join(`
1450
+ `));
1451
+ return blocks.join(`
1452
+ `) + `
1453
+ `;
1454
+ }
1455
+ }
1456
+ function quote2(value2) {
1457
+ const plain = /^[\w.:/@-]+$/.test(value2);
1458
+ if (plain)
1459
+ return value2;
1460
+ const escaped = value2.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$");
1461
+ return `"${escaped}"`;
1462
+ }
1463
+ function command(verb, properties) {
1464
+ const words2 = [verb];
1465
+ for (const [name, value2] of Object.entries(properties)) {
1466
+ if (value2 === undefined || value2 === false)
1467
+ continue;
1468
+ const written = typeof value2 === "number" ? String(value2) : quote2(value2);
1469
+ words2.push(`${name}=${written}`);
1470
+ }
1471
+ return words2.join(" ");
1472
+ }
1473
+ var yesNo = (on) => on ? "yes" : "no";
1474
+ var familySuffix = (family) => family === "ipv4" ? "V4" : "V6";
1475
+ function portList(ports) {
1476
+ if (ports === undefined)
1477
+ return;
1478
+ return [ports].flat().join(",");
1479
+ }
1480
+ function duration(seconds) {
1481
+ const units = [
1482
+ ["w", 604800],
1483
+ ["d", 86400],
1484
+ ["h", 3600],
1485
+ ["m", 60],
1486
+ ["s", 1]
1487
+ ];
1488
+ let remaining = seconds;
1489
+ let text = "";
1490
+ for (const [unit, size] of units) {
1491
+ const count = Math.floor(remaining / size);
1492
+ if (count > 0)
1493
+ text += `${count}${unit}`;
1494
+ remaining %= size;
1495
+ }
1496
+ return text || "0s";
1497
+ }
1498
+ function bitrate(rate) {
1499
+ const match = /^([\d.]+)([kMG])$/.exec(rate);
1500
+ if (!match)
1501
+ return rate;
1502
+ const [, amount, unit] = match;
1503
+ const suffix = unit === "k" ? "kbps" : `${unit}bps`;
1504
+ return `${Number(amount).toFixed(1)}${suffix}`;
1505
+ }
1506
+ function dottedMask(cidr) {
1507
+ const { addr, len } = split(cidr);
1508
+ const mask = len === 0 ? 0 : 4294967295 << 32 - len >>> 0;
1509
+ const octets = [24, 16, 8, 0].map((shift) => mask >>> shift & 255);
1510
+ return `${addr}/${octets.join(".")}`;
1511
+ }
1512
+ function communityPattern(pattern) {
1513
+ const fields = pattern.split(":").map((field) => field === "*" ? "[0-9]+" : field);
1514
+ return `^${fields.join(":")}$`;
1515
+ }
1516
+ var BRIDGE = "bridge";
1517
+
1518
+ class Context {
1519
+ device;
1520
+ ports;
1521
+ interfaces;
1522
+ constructor(device) {
1523
+ this.device = device;
1524
+ this.ports = Object.fromEntries(Object.entries(device.ports ?? {}).filter((entry) => entry[1] !== undefined));
1525
+ this.interfaces = device.interfaces ?? {};
1526
+ }
1527
+ name(portOrInterface) {
1528
+ if (portOrInterface in this.interfaces)
1529
+ return portOrInterface;
1530
+ return spell(this.device.platform, this.device.model, portOrInterface);
1531
+ }
1532
+ vlanId(vlan) {
1533
+ const declared = this.device.vlans?.[vlan];
1534
+ if (!declared)
1535
+ throw new Error(`${this.device.name}: VLAN ${vlan} is not declared`);
1536
+ return declared.id;
1537
+ }
1538
+ switched() {
1539
+ const ports = Object.entries(this.ports).filter(([, port]) => !port.lag && (port.access_vlan || port.trunk_vlans));
1540
+ const lags = Object.entries(this.interfaces).filter((entry) => {
1541
+ const lag = entry[1];
1542
+ return lag.type === "lag" && Boolean(lag.access_vlan || lag.trunk_vlans);
1543
+ });
1544
+ return [...ports, ...lags];
1545
+ }
1546
+ vlanInterfaces() {
1547
+ return Object.entries(this.interfaces).filter((entry) => entry[1].type === "vlan");
1548
+ }
1549
+ routed() {
1550
+ const ports = Object.entries(this.ports).map(([port, settings]) => ({ name: this.name(port), settings }));
1551
+ const interfaces = Object.entries(this.interfaces).map(([name, settings]) => ({ name, settings }));
1552
+ return [...ports, ...interfaces];
1553
+ }
1554
+ }
1555
+ var BPDU_DESTINATION = "01:80:C2:00:00:00/FF:FF:FF:FF:FF:FF";
1556
+ function renderInterfaces(out, ctx) {
1557
+ hardwareOffload(out, ctx);
1558
+ bridge(out, ctx);
1559
+ ethernet(out, ctx);
1560
+ switchPorts(out, ctx);
1561
+ lags(out, ctx);
1562
+ vlanInterfaces(out, ctx);
1563
+ tunnels(out, ctx);
1564
+ wireguard(out, ctx);
1565
+ vrrp(out, ctx);
1566
+ bridgePorts(out, ctx);
1567
+ bridgeVlans(out, ctx);
1568
+ bridgeFilters(out, ctx);
1569
+ interfaceLists(out, ctx);
1570
+ vrfs(out, ctx);
1571
+ }
1572
+ function hardwareOffload(out, ctx) {
1573
+ const on = yesNo(ctx.device.hardware_offload);
1574
+ out.add("/interface ethernet switch", command("set switch1", { "l3-hw-offloading": on }));
1575
+ out.add("/interface ethernet switch l3hw-settings", command("set", { autorestart: on, "ipv6-hw": on }));
1576
+ }
1577
+ function bridge(out, ctx) {
1578
+ const { device } = ctx;
1579
+ const needsBridge = ctx.switched().length > 0 || ctx.vlanInterfaces().length > 0;
1580
+ if (needsBridge) {
1581
+ const stp = device.stp;
1582
+ const protocolMode = stp ? stp.mode ?? "rstp" : "none";
1583
+ const priority = stp?.priority === undefined ? undefined : `0x${stp.priority.toString(16)}`;
1584
+ out.add("/interface bridge", command("add", { name: BRIDGE, "protocol-mode": protocolMode, priority, "vlan-filtering": "yes" }));
1585
+ }
1586
+ for (const [name, iface] of Object.entries(ctx.interfaces)) {
1587
+ if (iface.type !== "loopback")
1588
+ continue;
1589
+ out.add("/interface bridge", command("add", { comment: iface.description, name }));
1590
+ }
1591
+ }
1592
+ var MEDIUM = {
1593
+ optical: {
1594
+ "1g": "1G-baseX",
1595
+ "10g": "10G-baseSR-LR",
1596
+ "25g": "25G-baseSR-LR",
1597
+ "40g": "40G-baseSR4-LR4",
1598
+ "50g": "50G-baseSR2-LR2",
1599
+ "100g": "100G-baseSR4-LR4"
1600
+ },
1601
+ copper: {
1602
+ "100m": "100M-baseT-full",
1603
+ "1g": "1G-baseT-full"
1604
+ }
1605
+ };
1606
+ function mediumOf(platformPortName, speed) {
1607
+ const medium = platformPortName.startsWith("ether") ? "copper" : "optical";
1608
+ return MEDIUM[medium][speed];
1609
+ }
1610
+ function ethernet(out, ctx) {
1611
+ const { device } = ctx;
1612
+ for (const port of portsOf(device.platform, device.model)) {
1613
+ const selector = `set [ find default-name=${spell(device.platform, device.model, port)} ]`;
1614
+ const settings = ctx.ports[port];
1615
+ if (!settings) {
1616
+ out.add("/interface ethernet", command(selector, { disabled: "yes" }));
1617
+ continue;
1618
+ }
1619
+ const fixedSpeed = settings.speed && mediumOf(spell(device.platform, device.model, port), settings.speed);
1620
+ out.add("/interface ethernet", command(selector, {
1621
+ "auto-negotiation": fixedSpeed ? "no" : undefined,
1622
+ comment: settings.description,
1623
+ mtu: settings.mtu,
1624
+ speed: fixedSpeed
1625
+ }));
1626
+ }
1627
+ }
1628
+ function switchPorts(out, ctx) {
1629
+ for (const [port, settings] of Object.entries(ctx.ports)) {
1630
+ const storm = settings.storm_control;
1631
+ if (!storm)
1632
+ continue;
1633
+ const thresholds = [storm.broadcast, storm.multicast, storm.unknown_unicast].filter((threshold) => threshold !== undefined);
1634
+ const percent = thresholds.find((threshold) => ("percent" in threshold));
1635
+ if (!percent || !("percent" in percent))
1636
+ continue;
1637
+ out.add("/interface ethernet switch port", command(`set ${ctx.name(port)}`, {
1638
+ "limit-broadcasts": yesNo(storm.broadcast !== undefined),
1639
+ "limit-unknown-multicasts": yesNo(storm.multicast !== undefined),
1640
+ "limit-unknown-unicasts": yesNo(storm.unknown_unicast !== undefined),
1641
+ "storm-rate": percent.percent
1642
+ }));
1643
+ }
1644
+ }
1645
+ function lags(out, ctx) {
1646
+ for (const [name, lag] of Object.entries(ctx.interfaces)) {
1647
+ if (lag.type !== "lag")
1648
+ continue;
1649
+ const members = Object.entries(ctx.ports).filter(([, port]) => port.lag === name).map(([port]) => ctx.name(port));
1650
+ const mode = lag.mode === "static" ? "balance-xor" : "802.3ad";
1651
+ out.add("/interface bonding", command("add", {
1652
+ comment: lag.description,
1653
+ mode,
1654
+ mtu: lag.mtu,
1655
+ name,
1656
+ slaves: members.join(","),
1657
+ "transmit-hash-policy": "layer-2-and-3"
1658
+ }));
1659
+ }
1660
+ }
1661
+ function vlanInterfaces(out, ctx) {
1662
+ for (const [name, iface] of ctx.vlanInterfaces()) {
1663
+ const softwareOnly = ctx.device.hardware_offload && iface.hardware_offload === false;
1664
+ out.add("/interface vlan", command("add", {
1665
+ comment: iface.description,
1666
+ interface: BRIDGE,
1667
+ "l3-hw-offloading": softwareOnly ? "no" : undefined,
1668
+ mtu: iface.mtu,
1669
+ name,
1670
+ "vlan-id": ctx.vlanId(iface.vlan)
1671
+ }));
1672
+ }
1673
+ }
1674
+ function tunnels(out, ctx) {
1675
+ for (const [name, iface] of Object.entries(ctx.interfaces)) {
1676
+ if (iface.type === "gre") {
1677
+ out.add("/interface gre", command("add", {
1678
+ comment: iface.description,
1679
+ "local-address": iface.local,
1680
+ mtu: iface.mtu,
1681
+ name,
1682
+ "remote-address": iface.remote
1683
+ }));
1684
+ }
1685
+ if (iface.type === "vxlan") {
1686
+ const port = iface.port && iface.port !== 4789 ? iface.port : undefined;
1687
+ out.add("/interface vxlan", command("add", {
1688
+ comment: iface.description,
1689
+ "local-address": iface.local,
1690
+ "mac-address": iface.mac,
1691
+ mtu: iface.mtu,
1692
+ name,
1693
+ port,
1694
+ vni: iface.vni
1695
+ }));
1696
+ out.add("/interface vxlan vteps", command("add", { interface: name, "remote-ip": iface.remote }));
1697
+ }
1698
+ }
1699
+ }
1700
+ function wireguard(out, ctx) {
1701
+ for (const [name, iface] of Object.entries(ctx.interfaces)) {
1702
+ if (iface.type !== "wireguard")
1703
+ continue;
1704
+ out.add("/interface wireguard", command("add", {
1705
+ comment: iface.description,
1706
+ "listen-port": iface.listen_port,
1707
+ mtu: iface.mtu,
1708
+ name,
1709
+ "private-key": marker(iface.private_key)
1710
+ }));
1711
+ for (const peer of iface.peers ?? []) {
1712
+ const endpoint = peer.endpoint ? splitEndpoint(peer.endpoint) : undefined;
1713
+ out.add("/interface wireguard peers", command("add", {
1714
+ "allowed-address": peer.allowed_addresses.join(","),
1715
+ "client-allowed-address": peer.client_allowed_addresses?.join(","),
1716
+ "endpoint-address": endpoint?.host,
1717
+ "endpoint-port": endpoint?.port,
1718
+ interface: name,
1719
+ name: `${name}-${peer.name}`,
1720
+ "persistent-keepalive": peer.keepalive ? duration(peer.keepalive) : undefined,
1721
+ "public-key": peer.public_key
1722
+ }));
1723
+ }
1724
+ }
1725
+ }
1726
+ function splitEndpoint(endpoint) {
1727
+ const colon = endpoint.lastIndexOf(":");
1728
+ return { host: endpoint.slice(0, colon), port: endpoint.slice(colon + 1) };
1729
+ }
1730
+ function vrrp(out, ctx) {
1731
+ for (const { name, settings } of ctx.routed()) {
1732
+ for (const group of settings.vrrp ?? []) {
1733
+ const ipv6 = familyOf(group.address) === "ipv6";
1734
+ out.add("/interface vrrp", command("add", {
1735
+ interface: name,
1736
+ interval: group.interval ? duration(group.interval) : undefined,
1737
+ name: vrrpName(name, group.id),
1738
+ "preemption-mode": yesNo(group.preempt),
1739
+ priority: group.priority,
1740
+ "v3-protocol": ipv6 ? "ipv6" : undefined,
1741
+ version: ipv6 ? 3 : undefined,
1742
+ vrid: group.id
1743
+ }));
1744
+ }
1745
+ }
1746
+ }
1747
+ var vrrpName = (iface, id) => `${iface}-vrrp${id}`;
1748
+ function bridgePorts(out, ctx) {
1749
+ for (const [port, settings] of ctx.switched()) {
1750
+ const isTrunk = Boolean(settings.trunk_vlans);
1751
+ const untagged = isTrunk ? settings.native_vlan : settings.access_vlan;
1752
+ const pvid = untagged ? ctx.vlanId(untagged) : undefined;
1753
+ let frameTypes;
1754
+ if (!isTrunk)
1755
+ frameTypes = "admit-only-untagged-and-priority-tagged";
1756
+ else if (pvid === undefined)
1757
+ frameTypes = "admit-only-vlan-tagged";
1758
+ const stp = typeof settings.stp === "object" ? settings.stp : undefined;
1759
+ out.add("/interface bridge port", command("add", {
1760
+ bridge: BRIDGE,
1761
+ edge: stp?.edge ? "yes" : undefined,
1762
+ "frame-types": frameTypes,
1763
+ interface: ctx.name(port),
1764
+ "path-cost": stp?.cost,
1765
+ priority: stp?.priority === undefined ? undefined : `0x${stp.priority.toString(16)}`,
1766
+ pvid
1767
+ }));
1768
+ }
1769
+ }
1770
+ function bridgeVlans(out, ctx) {
1771
+ for (const [vlanName, vlan] of Object.entries(ctx.device.vlans ?? {})) {
1772
+ const routedHere = ctx.vlanInterfaces().some(([, iface]) => iface.vlan === vlanName);
1773
+ const trunks = ctx.switched().filter(([, settings]) => settings.trunk_vlans?.includes(vlanName)).map(([port]) => ctx.name(port));
1774
+ const tagged = [...routedHere ? [BRIDGE] : [], ...trunks];
1775
+ if (!tagged.length)
1776
+ continue;
1777
+ out.add("/interface bridge vlan", command("add", {
1778
+ bridge: BRIDGE,
1779
+ comment: vlan.description,
1780
+ tagged: tagged.join(","),
1781
+ "vlan-ids": vlan.id
1782
+ }));
1783
+ }
1784
+ }
1785
+ function bridgeFilters(out, ctx) {
1786
+ for (const [port, settings] of ctx.switched()) {
1787
+ if (settings.stp !== false)
1788
+ continue;
1789
+ const name = ctx.name(port);
1790
+ const comment = `${name} out of spanning tree`;
1791
+ out.add("/interface bridge filter", command("add", {
1792
+ action: "drop",
1793
+ chain: "input",
1794
+ comment,
1795
+ "dst-mac-address": BPDU_DESTINATION,
1796
+ "in-interface": name
1797
+ }));
1798
+ out.add("/interface bridge filter", command("add", {
1799
+ action: "drop",
1800
+ chain: "output",
1801
+ comment,
1802
+ "dst-mac-address": BPDU_DESTINATION,
1803
+ "out-interface": name
1804
+ }));
1805
+ }
1806
+ }
1807
+ function interfaceLists(out, ctx) {
1808
+ const lists = new Map;
1809
+ const lldp = lldpMembers(ctx);
1810
+ if (lldp)
1811
+ lists.set("lldp", lldp);
1812
+ const native = ctx.device.management?.native?.interfaces;
1813
+ if (native)
1814
+ lists.set("native", native.map((member) => ctx.name(member)));
1815
+ for (const list of lists.keys())
1816
+ out.add("/interface list", command("add", { name: list }));
1817
+ for (const [list, members] of lists)
1818
+ for (const member of members)
1819
+ out.add("/interface list member", command("add", {
1820
+ interface: member,
1821
+ list
1822
+ }));
1823
+ }
1824
+ function lldpMembers(ctx) {
1825
+ const lldp = ctx.device.lldp;
1826
+ if (!lldp)
1827
+ return;
1828
+ const overridden = Object.entries(ctx.ports).filter(([, port]) => port.lldp !== undefined);
1829
+ if (lldp === true && !overridden.length)
1830
+ return;
1831
+ const base = lldp === true ? Object.keys(ctx.ports) : [...lldp.interfaces];
1832
+ const members = new Set(base.map((member) => ctx.name(member)));
1833
+ for (const [port, settings] of overridden) {
1834
+ if (settings.lldp)
1835
+ members.add(ctx.name(port));
1836
+ else
1837
+ members.delete(ctx.name(port));
1838
+ }
1839
+ return [...members];
1840
+ }
1841
+ function vrfs(out, ctx) {
1842
+ for (const [vrf, settings] of Object.entries(ctx.device.vrfs ?? {})) {
1843
+ const members = ctx.routed().filter(({ settings: iface }) => iface.vrf === vrf).map(({ name }) => name);
1844
+ out.add("/ip vrf", command("add", { comment: settings.description, interfaces: members.join(","), name: vrf }));
1845
+ }
1846
+ }
1847
+ var NETWORKS = { ipv4: "bgp-networks-v4", ipv6: "bgp-networks-v6" };
1848
+ function renderAddressing(out, ctx) {
1849
+ addresses(out, ctx);
1850
+ dhcp(out, ctx);
1851
+ dns(out, ctx);
1852
+ addressLists(out, ctx);
1853
+ }
1854
+ function addresses(out, ctx) {
1855
+ for (const { name, settings } of ctx.routed()) {
1856
+ for (const entry of settings.addresses ?? []) {
1857
+ const address = typeof entry === "string" ? entry : entry.address;
1858
+ const peer = typeof entry === "string" ? undefined : entry.peer;
1859
+ if (familyOf(address) === "ipv4") {
1860
+ out.add("/ip address", command("add", { address, comment: settings.description, interface: name, network: peer }));
1861
+ } else {
1862
+ out.add("/ipv6 address", command("add", { address, advertise: yesNo(settings.ipv6_ra), comment: settings.description, interface: name }));
1863
+ }
1864
+ }
1865
+ for (const group of settings.vrrp ?? []) {
1866
+ const vrrpInterface = vrrpName(name, group.id);
1867
+ const comment = `VRRP ${group.id} on ${name}`;
1868
+ if (familyOf(group.address) === "ipv4") {
1869
+ out.add("/ip address", command("add", { address: `${group.address}/32`, comment, interface: vrrpInterface }));
1870
+ } else {
1871
+ out.add("/ipv6 address", command("add", {
1872
+ address: `${group.address}/128`,
1873
+ advertise: "no",
1874
+ comment,
1875
+ interface: vrrpInterface
1876
+ }));
1877
+ }
1878
+ }
1879
+ }
1880
+ }
1881
+ function dhcp(out, ctx) {
1882
+ for (const [name, server] of Object.entries(ctx.device.dhcp ?? {})) {
1883
+ const [first, last] = server.pool;
1884
+ out.add("/ip pool", command("add", { name, ranges: `${first}-${last}` }));
1885
+ out.add("/ip dhcp-server", command("add", {
1886
+ "address-pool": name,
1887
+ interface: ctx.name(server.interface),
1888
+ "lease-time": server.lease_time ? duration(server.lease_time) : undefined,
1889
+ name
1890
+ }));
1891
+ out.add("/ip dhcp-server network", command("add", {
1892
+ address: server.network,
1893
+ "dns-server": server.dns?.join(","),
1894
+ gateway: server.gateway
1895
+ }));
1896
+ for (const reservation of server.reservations ?? []) {
1897
+ out.add("/ip dhcp-server lease", command("add", {
1898
+ address: reservation.address,
1899
+ comment: reservation.description,
1900
+ "mac-address": reservation.mac,
1901
+ server: name
1902
+ }));
1903
+ }
1904
+ }
1905
+ for (const [name, relay] of Object.entries(ctx.device.dhcp_relay ?? {})) {
1906
+ out.add("/ip dhcp-relay", command("add", { "dhcp-server": relay.servers.join(","), interface: ctx.name(relay.interface), name }));
1907
+ }
1908
+ }
1909
+ function dns(out, ctx) {
1910
+ const dns2 = ctx.device.system?.dns;
1911
+ out.add("/ip dns", command("set", { "allow-remote-requests": yesNo(dns2?.serve), servers: dns2?.servers.join(",") ?? "" }));
1912
+ }
1913
+ function addressLists(out, ctx) {
1914
+ const { device } = ctx;
1915
+ const entries2 = [];
1916
+ for (const [list, members] of Object.entries(device.firewall?.address_sets ?? {})) {
1917
+ for (const address of members)
1918
+ entries2.push({ list, address });
1919
+ }
1920
+ for (const [list, set] of Object.entries(device.prefix_sets ?? {})) {
1921
+ if ("prefixes" in set)
1922
+ for (const address of set.prefixes)
1923
+ entries2.push({ list, address });
1924
+ else
1925
+ out.add("/ip firewall address-list", `# generated ${list}`);
1926
+ }
1927
+ for (const prefix of device.routing?.bgp?.networks ?? []) {
1928
+ entries2.push({ list: NETWORKS[familyOf(host(prefix))], address: prefix });
1929
+ }
1930
+ for (const family of ["ipv4", "ipv6"]) {
1931
+ const menu = family === "ipv4" ? "/ip firewall address-list" : "/ipv6 firewall address-list";
1932
+ for (const { list, address } of entries2) {
1933
+ if (familyOf(host(address)) !== family)
1934
+ continue;
1935
+ out.add(menu, command("add", { address, list }));
1936
+ }
1937
+ }
1938
+ }
1939
+ var HELPERS = ["ftp", "tftp", "irc", "h323", "sip", "pptp", "rtsp"];
1940
+ var CHAINS = ["input", "forward", "output"];
1941
+ function renderFirewall(out, ctx) {
1942
+ for (const family of ["ipv4", "ipv6"]) {
1943
+ filter(out, ctx, family);
1944
+ nat(out, ctx, family);
1945
+ }
1946
+ helpers(out, ctx);
1947
+ switchAcls(out, ctx);
1948
+ }
1949
+ function families(ctx, match) {
1950
+ const sets = ctx.device.firewall?.address_sets ?? {};
1951
+ const address = match?.src ?? match?.dst;
1952
+ let candidates = ["ipv4", "ipv6"];
1953
+ if (match?.family)
1954
+ candidates = [match.family];
1955
+ else if (address)
1956
+ candidates = [familyOf(address)];
1957
+ const setHas = (set, family) => !set || (sets[set] ?? []).some((member) => familyOf(member) === family);
1958
+ return candidates.filter((family) => setHas(match?.src_set, family) && setHas(match?.dst_set, family));
1959
+ }
1960
+ function matchProperties(ctx, match, family) {
1961
+ const protocol = match?.protocol === "icmp" && family === "ipv6" ? "icmpv6" : match?.protocol;
1962
+ const setFlags = match?.tcp_flags?.set ?? [];
1963
+ const unsetFlags = (match?.tcp_flags?.unset ?? []).map((flag) => `!${flag}`);
1964
+ const flags = match?.tcp_flags ? [...setFlags, ...unsetFlags].join(",") : undefined;
1965
+ return {
1966
+ "connection-state": match?.state?.join(","),
1967
+ "dst-address": match?.dst,
1968
+ "dst-address-list": match?.dst_set,
1969
+ "dst-port": portList(match?.dst_port),
1970
+ "in-interface": match?.in_interface && ctx.name(match.in_interface),
1971
+ "out-interface": match?.out_interface && ctx.name(match.out_interface),
1972
+ protocol,
1973
+ "src-address": match?.src,
1974
+ "src-address-list": match?.src_set,
1975
+ "src-port": portList(match?.src_port),
1976
+ "tcp-flags": flags
1977
+ };
1978
+ }
1979
+ function filter(out, ctx, family) {
1980
+ const menu = family === "ipv4" ? "/ip firewall filter" : "/ipv6 firewall filter";
1981
+ for (const chain of CHAINS) {
1982
+ const settings = ctx.device.firewall?.filter?.[chain];
1983
+ if (!settings)
1984
+ continue;
1985
+ for (const rule of settings.rules ?? []) {
1986
+ if (!families(ctx, rule.match).includes(family))
1987
+ continue;
1988
+ const match = matchProperties(ctx, rule.match, family);
1989
+ if (rule.offload && family === "ipv4") {
1990
+ out.add(menu, command("add", { action: "fasttrack-connection", chain, comment: rule.description, ...match }));
1991
+ }
1992
+ out.add(menu, command("add", { action: rule.action, chain, comment: rule.description, ...match }));
1993
+ }
1994
+ if (settings.default === "drop")
1995
+ out.add(menu, command("add", { action: "drop", chain, comment: "Default" }));
1996
+ }
1997
+ }
1998
+ function nat(out, ctx, family) {
1999
+ const menu = family === "ipv4" ? "/ip firewall nat" : "/ipv6 firewall nat";
2000
+ const chains = [
2001
+ ["srcnat", ctx.device.firewall?.nat?.source],
2002
+ ["dstnat", ctx.device.firewall?.nat?.destination]
2003
+ ];
2004
+ for (const [chain, rules] of chains) {
2005
+ for (const rule of rules ?? []) {
2006
+ if (!families(ctx, rule.match).includes(family))
2007
+ continue;
2008
+ if (rule.action !== "masquerade" && familyOf(rule.to) !== family)
2009
+ continue;
2010
+ const action = rule.action === "masquerade" ? {
2011
+ action: "masquerade"
2012
+ } : { action: rule.action === "snat" ? "src-nat" : "dst-nat", "to-addresses": rule.to, "to-ports": rule.to_port };
2013
+ out.add(menu, command("add", { ...action, chain, comment: rule.description, ...matchProperties(ctx, rule.match, family) }));
2014
+ }
2015
+ }
2016
+ }
2017
+ function helpers(out, ctx) {
2018
+ const enabled = ctx.device.firewall?.helpers ?? [];
2019
+ for (const helper of HELPERS) {
2020
+ out.add("/ip firewall service-port", command(`set ${helper}`, { disabled: yesNo(!enabled.includes(helper)) }));
2021
+ }
2022
+ }
2023
+ function switchAcls(out, ctx) {
2024
+ for (const [port, settings] of Object.entries(ctx.ports)) {
2025
+ if (!settings.acl)
2026
+ continue;
2027
+ for (const rule of ctx.device.acls?.[settings.acl] ?? []) {
2028
+ const match = rule.match ?? {};
2029
+ const ipv6 = match.family === "ipv6" || [match.src, match.dst].some((address2) => address2 && familyOf(address2) === "ipv6");
2030
+ const address = (prefix) => prefix && !ipv6 ? dottedMask(prefix) : prefix;
2031
+ out.add("/interface ethernet switch rule", command("add", {
2032
+ comment: rule.description,
2033
+ [ipv6 ? "dst-address6" : "dst-address"]: address(match.dst),
2034
+ "dst-port": portList(match.dst_port),
2035
+ "mac-protocol": ipv6 ? "ipv6" : "ip",
2036
+ "new-dst-ports": rule.action === "drop" ? "" : undefined,
2037
+ ports: ctx.name(port),
2038
+ protocol: match.protocol,
2039
+ rate: rule.rate && bitrate(rule.rate),
2040
+ [ipv6 ? "src-address6" : "src-address"]: address(match.src),
2041
+ "src-port": portList(match.src_port),
2042
+ switch: "switch1",
2043
+ "vlan-id": match.vlan && ctx.vlanId(match.vlan)
2044
+ }));
2045
+ }
2046
+ }
2047
+ }
2048
+ var RPKI_GROUP = "rpki";
2049
+ var REJECT_ALL = "REJECT-ALL";
2050
+ var ROLES = {
2051
+ provider: "ebgp-customer",
2052
+ customer: "ebgp-provider",
2053
+ peer: "ebgp-peer",
2054
+ rs: "ebgp-rs-client",
2055
+ "rs-client": "ebgp-rs"
2056
+ };
2057
+ function renderRouting(out, ctx) {
2058
+ staticRoutes(out, ctx);
2059
+ rpki(out, ctx);
2060
+ if (ctx.device.routing?.bgp) {
2061
+ bgpInstance(out, ctx);
2062
+ policies(out, ctx);
2063
+ bgpConnections(out, ctx);
2064
+ }
2065
+ ospf(out, ctx);
2066
+ bfd(out, ctx);
2067
+ }
2068
+ function staticRoutes(out, ctx) {
2069
+ for (const route of ctx.device.routing?.static ?? []) {
2070
+ const menu = familyOf(host(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
2071
+ const verb = route.blackhole ? "add blackhole" : "add";
2072
+ const gateway = route.via ?? (route.interface ? ctx.name(route.interface) : undefined);
2073
+ out.add(menu, command(verb, {
2074
+ comment: route.description,
2075
+ distance: route.distance,
2076
+ "dst-address": route.prefix,
2077
+ gateway,
2078
+ "routing-table": route.vrf
2079
+ }));
2080
+ }
2081
+ }
2082
+ function rpki(out, ctx) {
2083
+ for (const server of ctx.device.routing?.rpki ?? []) {
2084
+ out.add("/routing rpki", command("add", {
2085
+ address: server.address,
2086
+ comment: server.description,
2087
+ group: RPKI_GROUP,
2088
+ port: server.port
2089
+ }));
2090
+ }
2091
+ }
2092
+ function bgpInstance(out, ctx) {
2093
+ const bgp = ctx.device.routing.bgp;
2094
+ out.add("/routing bgp instance", command("add", { as: bgp.asn, name: "default", "router-id": ctx.device.routing?.router_id }));
2095
+ }
2096
+ function settingsOf(ctx, neighbor) {
2097
+ const group = neighbor.group ? ctx.device.routing?.bgp?.groups?.[neighbor.group] : undefined;
2098
+ return { ...group, ...neighbor };
2099
+ }
2100
+ function familiesOf(ctx, neighbor) {
2101
+ return settingsOf(ctx, neighbor).families ?? [familyOf(neighbor.address)];
2102
+ }
2103
+ var chainName = (policy, family) => `${policy}-${familySuffix(family)}`;
2104
+ function appliesTo(ctx, rule, family) {
2105
+ const match = rule.match;
2106
+ if (match?.family && match.family !== family)
2107
+ return false;
2108
+ if (match?.prefix && familyOf(host(match.prefix)) !== family)
2109
+ return false;
2110
+ if (match?.prefix_set) {
2111
+ const set = ctx.device.prefix_sets?.[match.prefix_set];
2112
+ if (!set)
2113
+ return false;
2114
+ const firstPrefix = "source" in set ? undefined : set.prefixes[0];
2115
+ const setFamily = "source" in set ? set.source.family : firstPrefix && familyOf(host(firstPrefix));
2116
+ if (setFamily !== family)
2117
+ return false;
2118
+ }
2119
+ return true;
2120
+ }
2121
+ function conditions(rule) {
2122
+ const match = rule.match ?? {};
2123
+ const tests = [];
2124
+ if (match.prefix)
2125
+ tests.push(`dst==${match.prefix}`);
2126
+ if (match.prefix_set)
2127
+ tests.push(`dst in ${match.prefix_set}`);
2128
+ if (match.prefix_length?.min !== undefined)
2129
+ tests.push(`dst-len>${match.prefix_length.min - 1}`);
2130
+ if (match.prefix_length?.max !== undefined)
2131
+ tests.push(`dst-len<${match.prefix_length.max + 1}`);
2132
+ if (match.as_path_length?.min !== undefined)
2133
+ tests.push(`bgp-path-len>${match.as_path_length.min - 1}`);
2134
+ if (match.as_path_length?.max !== undefined)
2135
+ tests.push(`bgp-path-len<${match.as_path_length.max + 1}`);
2136
+ if (match.rpki)
2137
+ tests.push(`rpki ${match.rpki}`);
2138
+ if (match.community)
2139
+ tests.push(`bgp-communities includes ${match.community}`);
2140
+ if (match.large_community)
2141
+ tests.push(`bgp-large-communities includes ${match.large_community}`);
2142
+ return tests;
2143
+ }
2144
+ function actions(rule, family) {
2145
+ const set = rule.set ?? {};
2146
+ const steps2 = [];
2147
+ if (set.local_pref !== undefined)
2148
+ steps2.push(`set bgp-local-pref ${set.local_pref}`);
2149
+ if (set.med !== undefined)
2150
+ steps2.push(`set bgp-med ${set.med}`);
2151
+ if (set.prepend !== undefined)
2152
+ steps2.push(`set bgp-path-prepend ${set.prepend}`);
2153
+ for (const pattern of set.remove_communities ?? [])
2154
+ steps2.push(`delete bgp-communities regexp "${communityPattern(pattern)}"`);
2155
+ if (set.communities)
2156
+ steps2.push(`set bgp-communities ${set.communities.join(",")}`);
2157
+ if (set.add_communities)
2158
+ steps2.push(`append bgp-communities ${set.add_communities.join(",")}`);
2159
+ for (const pattern of set.remove_large_communities ?? []) {
2160
+ steps2.push(`delete bgp-large-communities regexp "${communityPattern(pattern)}"`);
2161
+ }
2162
+ if (set.large_communities)
2163
+ steps2.push(`set bgp-large-communities ${set.large_communities.join(",")}`);
2164
+ if (set.add_large_communities)
2165
+ steps2.push(`append bgp-large-communities ${set.add_large_communities.join(",")}`);
2166
+ if (set.next_hop && familyOf(set.next_hop) === family)
2167
+ steps2.push(`set gw ${set.next_hop}`);
2168
+ if (set.preferred_source && familyOf(set.preferred_source) === family)
2169
+ steps2.push(`set pref-src ${set.preferred_source}`);
2170
+ if (set.blackhole)
2171
+ steps2.push("set blackhole yes");
2172
+ if (rule.action)
2173
+ steps2.push(rule.action);
2174
+ return steps2;
2175
+ }
2176
+ function ruleText(rule, family) {
2177
+ const parts = [];
2178
+ if (rule.call)
2179
+ parts.push(`jump ${chainName(rule.call, family)}`);
2180
+ if (rule.match?.rpki)
2181
+ parts.push(`rpki-verify ${RPKI_GROUP}`);
2182
+ const tests = conditions(rule);
2183
+ const steps2 = actions(rule, family);
2184
+ if (tests.length)
2185
+ parts.push(`if (${tests.join(" && ")})${steps2.length ? ` { ${steps2.join("; ")} }` : ""}`);
2186
+ else
2187
+ parts.push(...steps2);
2188
+ return parts.join("; ");
2189
+ }
2190
+ function policies(out, ctx) {
2191
+ const bgp = ctx.device.routing.bgp;
2192
+ const policies2 = ctx.device.policies ?? {};
2193
+ const wanted = new Map;
2194
+ const want = (policy, family) => {
2195
+ const families2 = wanted.get(policy) ?? new Set;
2196
+ if (families2.has(family))
2197
+ return;
2198
+ families2.add(family);
2199
+ wanted.set(policy, families2);
2200
+ for (const rule of policies2[policy] ?? [])
2201
+ if (rule.call && appliesTo(ctx, rule, family))
2202
+ want(rule.call, family);
2203
+ };
2204
+ let rejectAllNeeded = false;
2205
+ for (const neighbor of Object.values(bgp.neighbors ?? {})) {
2206
+ const settings = settingsOf(ctx, neighbor);
2207
+ for (const family of familiesOf(ctx, neighbor)) {
2208
+ for (const policy of [settings.import, settings.export]) {
2209
+ if (policy)
2210
+ want(policy, family);
2211
+ else
2212
+ rejectAllNeeded = true;
2213
+ }
2214
+ }
2215
+ }
2216
+ const called = new Set(Object.values(policies2).flatMap((rules) => rules.flatMap((rule) => rule.call ? [rule.call] : [])));
2217
+ for (const policy of [...wanted.keys()].sort()) {
2218
+ for (const family of ["ipv4", "ipv6"]) {
2219
+ if (!wanted.get(policy).has(family))
2220
+ continue;
2221
+ const chain = chainName(policy, family);
2222
+ const rules = (policies2[policy] ?? []).filter((rule) => appliesTo(ctx, rule, family));
2223
+ for (const rule of rules) {
2224
+ const text = ruleText(rule, family);
2225
+ if (text)
2226
+ out.add("/routing filter rule", command("add", { chain, comment: rule.description, rule: text }));
2227
+ }
2228
+ const last = rules.at(-1);
2229
+ const decides = last !== undefined && !last.match && !last.call && last.action !== undefined;
2230
+ if (!called.has(policy) && !decides)
2231
+ out.add("/routing filter rule", command("add", { chain, rule: "reject" }));
2232
+ }
2233
+ }
2234
+ if (rejectAllNeeded) {
2235
+ for (const family of ["ipv4", "ipv6"])
2236
+ out.add("/routing filter rule", command("add", {
2237
+ chain: chainName(REJECT_ALL, family),
2238
+ rule: "reject"
2239
+ }));
2240
+ }
2241
+ }
2242
+ function bgpConnections(out, ctx) {
2243
+ const bgp = ctx.device.routing.bgp;
2244
+ for (const [name, neighbor] of Object.entries(bgp.neighbors ?? {})) {
2245
+ const settings = settingsOf(ctx, neighbor);
2246
+ const [family] = familiesOf(ctx, neighbor);
2247
+ const addressFamily = familyOf(neighbor.address);
2248
+ const internal = settings.remote_as === bgp.asn;
2249
+ const originates = bgp.networks?.some((prefix) => familyOf(host(prefix)) === family);
2250
+ const role = settings.role ? ROLES[settings.role] : internal ? "ibgp" : "ebgp";
2251
+ out.add("/routing bgp connection", command("add", {
2252
+ afi: addressFamily === family ? undefined : family === "ipv6" ? "ipv6" : "ip",
2253
+ as: bgp.asn,
2254
+ connect: settings.passive ? "no" : undefined,
2255
+ "hold-time": settings.hold_time ? duration(settings.hold_time) : undefined,
2256
+ "input.filter": chainName(settings.import ?? REJECT_ALL, family),
2257
+ [`input.limit-process-routes-${family}`]: settings.max_prefixes,
2258
+ "keepalive-time": settings.keepalive ? duration(settings.keepalive) : undefined,
2259
+ listen: settings.passive ? "yes" : undefined,
2260
+ "local.address": settings.local_address,
2261
+ "local.role": role,
2262
+ multihop: settings.multihop ? "yes" : undefined,
2263
+ name,
2264
+ "output.filter-chain": chainName(settings.export ?? REJECT_ALL, family),
2265
+ "output.network": originates ? NETWORKS[family] : undefined,
2266
+ "remote.address": `${neighbor.address}/${addressFamily === "ipv4" ? 32 : 128}`,
2267
+ "remote.as": settings.remote_as,
2268
+ "tcp-md5-key": settings.password && marker(settings.password),
2269
+ "use-bfd": settings.bfd ? "yes" : undefined
2270
+ }));
2271
+ }
2272
+ }
2273
+ function ospf(out, ctx) {
2274
+ const ospf2 = ctx.device.routing?.ospf;
2275
+ if (!ospf2)
2276
+ return;
2277
+ for (const family of ospf2.families ?? ["ipv4"]) {
2278
+ const version = family === "ipv4" ? 2 : 3;
2279
+ const instance = `ospf-v${version}`;
2280
+ out.add("/routing ospf instance", command("add", {
2281
+ name: instance,
2282
+ redistribute: ospf2.redistribute?.join(","),
2283
+ "router-id": ctx.device.routing?.router_id,
2284
+ version
2285
+ }));
2286
+ for (const [areaId, area] of Object.entries(ospf2.areas)) {
2287
+ const areaName = `${instance}-${areaId}`;
2288
+ out.add("/routing ospf area", command("add", { "area-id": areaId, instance, name: areaName }));
2289
+ for (const [iface, settings] of Object.entries(area.interfaces)) {
2290
+ const verb = settings?.passive ? "add passive" : "add";
2291
+ out.add("/routing ospf interface-template", command(verb, {
2292
+ area: areaName,
2293
+ cost: settings?.cost,
2294
+ interfaces: ctx.name(iface),
2295
+ type: settings?.network === "point-to-point" ? "ptp" : undefined,
2296
+ "use-bfd": settings?.bfd ? "yes" : undefined
2297
+ }));
2298
+ }
2299
+ }
2300
+ }
2301
+ }
2302
+ function bfd(out, ctx) {
2303
+ const routing = ctx.device.routing;
2304
+ const bgpWants = Object.values(routing?.bgp?.neighbors ?? {}).some((neighbor) => settingsOf(ctx, neighbor).bfd);
2305
+ const ospfWants = Object.values(routing?.ospf?.areas ?? {}).some((area) => Object.values(area.interfaces).some((iface) => iface?.bfd));
2306
+ if (bgpWants || ospfWants)
2307
+ out.add("/routing bfd configuration", command("add", { interfaces: "all" }));
2308
+ }
2309
+ var SERVICES = {
2310
+ ftp: "ftp",
2311
+ ssh: "ssh",
2312
+ telnet: "telnet",
2313
+ http: "www",
2314
+ https: "www-ssl",
2315
+ api: "api",
2316
+ api_tls: "api-ssl",
2317
+ native: "winbox"
2318
+ };
2319
+ var USER_GROUPS = { admin: "full", operator: "write", "read-only": "read" };
2320
+ var SNMP_AUTH2 = { sha1: "SHA1", sha256: "SHA1" };
2321
+ var SNMP_PRIVACY = { aes128: "AES", des: "DES" };
2322
+ function renderManagement(out, ctx) {
2323
+ services(out, ctx);
2324
+ layerTwo(out, ctx);
2325
+ snmp(out, ctx);
2326
+ users(out, ctx);
2327
+ certificates(out, ctx);
2328
+ }
2329
+ function services(out, ctx) {
2330
+ const management = ctx.device.management ?? {};
2331
+ for (const [neutral, platform] of Object.entries(SERVICES)) {
2332
+ const service = management[neutral];
2333
+ if (!service) {
2334
+ out.add("/ip service", command(`set ${platform}`, { disabled: "yes" }));
2335
+ continue;
2336
+ }
2337
+ const allow = service.allow ?? management.allow ?? [];
2338
+ out.add("/ip service", command(`set ${platform}`, {
2339
+ address: allow.join(","),
2340
+ certificate: service.certificate,
2341
+ disabled: "no",
2342
+ port: service.port
2343
+ }));
2344
+ }
2345
+ out.add("/ip service", command("set reverse-proxy", { disabled: "yes" }));
2346
+ out.add("/ip ssh", command("set", { "strong-crypto": yesNo(!management.ssh?.weak_crypto) }));
2347
+ }
2348
+ function layerTwo(out, ctx) {
2349
+ const nativeList = ctx.device.management?.native?.interfaces ? "native" : "none";
2350
+ out.add("/tool mac-server", command("set", { "allowed-interface-list": nativeList }));
2351
+ out.add("/tool mac-server mac-winbox", command("set", { "allowed-interface-list": nativeList }));
2352
+ out.add("/tool mac-server ping", command("set", { enabled: "no" }));
2353
+ out.add("/tool bandwidth-server", command("set", { enabled: "no" }));
2354
+ let discovery = "none";
2355
+ if (lldpMembers(ctx))
2356
+ discovery = "lldp";
2357
+ else if (ctx.device.lldp)
2358
+ discovery = "all";
2359
+ out.add("/ip neighbor discovery-settings", command("set", { "discover-interface-list": discovery, protocol: "lldp" }));
2360
+ }
2361
+ function snmp(out, ctx) {
2362
+ const management = ctx.device.management ?? {};
2363
+ const snmp2 = management.snmp;
2364
+ if (!snmp2) {
2365
+ out.add("/snmp", command("set", { enabled: "no" }));
2366
+ out.add("/snmp community", command("set [ find default=yes ]", { addresses: "::/0", name: "public" }));
2367
+ return;
2368
+ }
2369
+ const allow = (snmp2.allow ?? management.allow ?? []).join(",");
2370
+ out.add("/snmp", command("set", { contact: snmp2.contact ?? "", enabled: "yes", location: snmp2.location ?? "" }));
2371
+ if (snmp2.community)
2372
+ out.add("/snmp community", command("set [ find default=yes ]", { addresses: allow, name: marker(snmp2.community) }));
2373
+ else
2374
+ out.add("/snmp community", command("set [ find default=yes ]", { addresses: "::/0", disabled: "yes", name: "public" }));
2375
+ for (const [name, user] of Object.entries(snmp2.users ?? {})) {
2376
+ out.add("/snmp community", command("add", {
2377
+ addresses: allow,
2378
+ "authentication-password": marker(user.auth_password),
2379
+ "authentication-protocol": SNMP_AUTH2[user.auth],
2380
+ "encryption-password": marker(user.privacy_password),
2381
+ "encryption-protocol": SNMP_PRIVACY[user.privacy],
2382
+ name,
2383
+ security: "private"
2384
+ }));
2385
+ }
2386
+ }
2387
+ function users(out, ctx) {
2388
+ for (const [name, user] of Object.entries(ctx.device.users ?? {})) {
2389
+ out.add("/user", command("add", {
2390
+ comment: user.description,
2391
+ group: USER_GROUPS[user.role],
2392
+ name,
2393
+ password: marker(user.password)
2394
+ }));
2395
+ for (const key of user.ssh_keys ?? [])
2396
+ out.add("/user ssh-keys", command("add", { key, user: name }));
2397
+ }
2398
+ }
2399
+ function certificates(out, ctx) {
2400
+ for (const [name, certificate] of Object.entries(ctx.device.certificates ?? {})) {
2401
+ out.add("/certificate", command("add", {
2402
+ fingerprint: fingerprint(certificate.certificate),
2403
+ name,
2404
+ "private-key": yesNo(Boolean(certificate.private_key))
2405
+ }));
2406
+ }
2407
+ }
2408
+ function fingerprint(pem) {
2409
+ return new X509Certificate(pem).fingerprint256.replace(/:/g, "").toLowerCase();
2410
+ }
2411
+ var TOPICS = [
2412
+ ["critical", "critical"],
2413
+ ["error", "error"],
2414
+ ["warning", "warning"],
2415
+ ["info", "info"],
2416
+ ["debug", "debug"]
2417
+ ];
2418
+ var SEVERITY_ORDER = ["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"];
2419
+ function topicsFrom(level) {
2420
+ const threshold = Math.max(SEVERITY_ORDER.indexOf(level), SEVERITY_ORDER.indexOf("critical"));
2421
+ return TOPICS.filter(([severity]) => SEVERITY_ORDER.indexOf(severity) <= threshold).map(([, topic]) => topic);
2422
+ }
2423
+ function renderSystem(out, ctx) {
2424
+ const system = ctx.device.system ?? {};
2425
+ out.add("/ip settings", command("set", {
2426
+ "send-redirects": yesNo(system.ip?.icmp_redirects),
2427
+ "tcp-syncookies": yesNo(system.ip?.syn_cookies)
2428
+ }));
2429
+ out.add("/system identity", command("set", { name: ctx.device.name }));
2430
+ out.add("/system clock", command("set", { "time-zone-autodetect": "no", "time-zone-name": system.timezone ?? "UTC" }));
2431
+ out.add("/system note", command("set", { note: system.banner ?? "", "show-at-login": "yes" }));
2432
+ out.add("/system ntp client", command("set", { enabled: yesNo(Boolean(system.ntp?.servers.length)) }));
2433
+ out.add("/system ntp server", command("set", { enabled: yesNo(system.ntp?.serve) }));
2434
+ for (const server of system.ntp?.servers ?? [])
2435
+ out.add("/system ntp client servers", command("add", { address: server }));
2436
+ logging(out, ctx);
2437
+ out.add("/system package update", command("set", { channel: system.release_channel ?? "stable" }));
2438
+ out.add("/system routerboard settings", command("set", { "enter-setup-on": "delete-key" }));
2439
+ flowExport(out, ctx);
2440
+ }
2441
+ function logging(out, ctx) {
2442
+ const logging2 = ctx.device.system?.logging;
2443
+ const local = logging2?.local;
2444
+ out.add("/system logging action", command("set 1", {
2445
+ "disk-file-count": local?.files ?? 2,
2446
+ "disk-lines-per-file": local?.lines_per_file ?? 1000
2447
+ }));
2448
+ for (const topic of local ? topicsFrom(local.level) : [])
2449
+ out.add("/system logging", command("add", { action: "disk", topics: topic }));
2450
+ (logging2?.remote ?? []).forEach((collector, index) => {
2451
+ const action = `remote${index + 1}`;
2452
+ out.add("/system logging action", command("add", {
2453
+ name: action,
2454
+ remote: collector.address,
2455
+ "remote-port": collector.port,
2456
+ target: "remote"
2457
+ }));
2458
+ for (const topic of topicsFrom(collector.level))
2459
+ out.add("/system logging", command("add", { action, topics: topic }));
2460
+ });
2461
+ }
2462
+ function flowExport(out, ctx) {
2463
+ const flow = ctx.device.flow_export;
2464
+ if (!flow) {
2465
+ out.add("/ip traffic-flow", command("set", { enabled: "no" }));
2466
+ return;
2467
+ }
2468
+ const sampled = flow.sampling !== undefined && flow.sampling > 1;
2469
+ const interfaces = flow.interfaces?.map((iface) => ctx.name(iface)).join(",") ?? "all";
2470
+ out.add("/ip traffic-flow", command("set", {
2471
+ enabled: "yes",
2472
+ interfaces,
2473
+ "packet-sampling": yesNo(sampled),
2474
+ "sampling-interval": sampled ? flow.sampling : undefined,
2475
+ "sampling-space": sampled ? 1 : undefined
2476
+ }));
2477
+ const version = flow.protocol === "ipfix" ? "ipfix" : "9";
2478
+ for (const collector of flow.collectors) {
2479
+ out.add("/ip traffic-flow target", command("add", { "dst-address": collector.address, port: collector.port ?? 2055, version }));
2480
+ }
2481
+ }
2482
+ function render2(device) {
2483
+ const out = new Output;
2484
+ const ctx = new Context(device);
2485
+ renderInterfaces(out, ctx);
2486
+ renderAddressing(out, ctx);
2487
+ renderFirewall(out, ctx);
2488
+ renderRouting(out, ctx);
2489
+ renderManagement(out, ctx);
2490
+ renderSystem(out, ctx);
2491
+ return out.text();
2492
+ }
2493
+
2494
+ // src/adapters/devices/routeros/index.ts
2495
+ var SUPPORTED_MAJOR = 7;
2496
+ async function login(device) {
2497
+ const user = device.users?.[device.connection.user];
2498
+ if (!user)
2499
+ throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
2500
+ return connect({
2501
+ host: device.connection.host,
2502
+ port: device.connection.port,
2503
+ user: device.connection.user,
2504
+ password: resolve(user.password)
2505
+ });
2506
+ }
2507
+ async function checkVersion(client, device) {
2508
+ const version = (await exec(client, ":put [/system resource get version]")).trim();
2509
+ const major = Number(version.split(".")[0]);
2510
+ if (major !== SUPPORTED_MAJOR) {
2511
+ throw new Error(`${device.name} runs RouterOS ${version}; this adapter renders for version ${SUPPORTED_MAJOR}`);
2512
+ }
2513
+ }
2514
+ var EXPORT_TIMESTAMP = /^# \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} by RouterOS/m;
2515
+ var withoutTimestamp = (text) => text.replace(/\r/g, "").replace(EXPORT_TIMESTAMP, "# by RouterOS");
2516
+ var LIST_CERTIFICATES = [
2517
+ ":foreach c in=[/certificate find] do={",
2518
+ ':local key "no";',
2519
+ ':if ([/certificate get $c private-key]) do={ :set key "yes" };',
2520
+ ':put ("add fingerprint=" . [/certificate get $c fingerprint] . " name=" . [/certificate get $c name] . " private-key=" . $key)',
2521
+ "}"
2522
+ ].join(" ");
2523
+ async function read2(device) {
2524
+ const client = await login(device);
2525
+ try {
2526
+ await checkVersion(client, device);
2527
+ const config = await exec(client, "/export", 300000);
2528
+ const users2 = await exec(client, "/user export");
2529
+ const certificates2 = (await exec(client, LIST_CERTIFICATES)).trim();
2530
+ const listed = certificates2.split(`
2531
+ `).every((line2) => line2.trim().startsWith("add fingerprint="));
2532
+ if (certificates2 && !listed) {
2533
+ throw new Error(`${device.name}: certificates could not be listed: ${certificates2.slice(0, 120)}`);
2534
+ }
2535
+ const userMenu = users2.slice(users2.indexOf(`
2536
+ /`)).trim();
2537
+ const certificateMenu = certificates2 ? `/certificate
2538
+ ${certificates2}` : "";
2539
+ const text = [config.trim(), userMenu, certificateMenu].filter(Boolean).join(`
2540
+ `);
2541
+ return withoutTimestamp(text) + `
2542
+ `;
2543
+ } finally {
2544
+ client.end();
2545
+ }
2546
+ }
2547
+ function unsupported2(device) {
2548
+ const found = [];
2549
+ for (const [name, port] of Object.entries(device.ports ?? {})) {
2550
+ if (!port)
2551
+ continue;
2552
+ if (port.speed && !mediumOf(spell(device.platform, device.model, name), port.speed)) {
2553
+ found.push(`ports.${name}.speed ${port.speed}, the port has no fixed speed by that name`);
2554
+ }
2555
+ const thresholds = Object.values(port.storm_control ?? {});
2556
+ const percents = new Set(thresholds.map((threshold) => ("percent" in threshold) ? threshold.percent : undefined));
2557
+ if (thresholds.some((threshold) => ("pps" in threshold))) {
2558
+ found.push(`ports.${name}.storm_control in pps, the platform takes percent`);
2559
+ } else if (percents.size > 1) {
2560
+ found.push(`ports.${name}.storm_control with a different percent per kind, the platform takes one`);
2561
+ }
2562
+ }
2563
+ const bgp = device.routing?.bgp;
2564
+ for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
2565
+ const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
2566
+ const families2 = neighbor.families ?? group?.families ?? [familyOf(neighbor.address)];
2567
+ if (families2.length > 1)
2568
+ found.push(`routing.bgp.neighbors.${name}.families, one family per session`);
2569
+ }
2570
+ const management = device.management;
2571
+ if (management?.console)
2572
+ found.push("management.console, the console uses the device's users");
2573
+ if (management?.privilege_password)
2574
+ found.push("management.privilege_password, the platform has no privileged mode");
2575
+ if (management?.ssh?.auth_timeout || management?.ssh?.auth_retries) {
2576
+ found.push("management.ssh.auth_timeout and auth_retries");
2577
+ }
2578
+ for (const [name, user] of Object.entries(management?.snmp?.users ?? {})) {
2579
+ if (user.auth !== "sha1") {
2580
+ found.push(`management.snmp.users.${name}.auth ${user.auth}, the platform authenticates with SHA1 or MD5`);
2581
+ }
2582
+ }
2583
+ if (device.system?.transceiver_monitoring === false)
2584
+ found.push("system.transceiver_monitoring false, it is always on");
2585
+ if (device.flow_export?.protocol === "sflow")
2586
+ found.push("flow_export.protocol sflow, the platform exports NetFlow and IPFIX");
2587
+ return found;
2588
+ }
2589
+ var ROLLBACK_SCHEDULER = "apply-rollback";
2590
+ var escapeForString = (text) => text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$");
2591
+ async function refusal(client, command2) {
2592
+ const compiled = (await exec(client, `:put [:parse "${escapeForString(command2)}"]`)).trim();
2593
+ const refused = !compiled.startsWith("(") || /syntax error|bad parameter|no such|expected/.test(compiled);
2594
+ return refused ? compiled.slice(0, 200) : undefined;
2595
+ }
2596
+ async function installCertificate(client, device, name) {
2597
+ const certificate = device.certificates?.[name];
2598
+ if (!certificate)
2599
+ throw new Error(`certificate ${name} is not declared`);
2600
+ await upload(client, `${name}.crt`, certificate.certificate);
2601
+ await exec(client, `/certificate import file-name=${name}.crt name=${name} passphrase=""`);
2602
+ if (certificate.private_key) {
2603
+ const value2 = resolve(certificate.private_key);
2604
+ const privateKey = value2.includes("-----BEGIN") ? `${value2}
2605
+ ` : Buffer.from(value2, "base64").toString("utf8");
2606
+ await upload(client, `${name}.key`, privateKey);
2607
+ await exec(client, `/certificate import file-name=${name}.key name=${name} passphrase=""`);
2608
+ }
2609
+ await exec(client, `/file remove [find name="${name}.crt" or name="${name}.key"]`);
2610
+ }
2611
+ async function apply2(device, _network, changes2, options) {
2612
+ const client = await login(device);
2613
+ await checkVersion(client, device);
2614
+ const commands = changes2.steps.flatMap((step) => step.send.map((command2) => ({ title: step.title, command: command2 })));
2615
+ for (const { title, command: command2 } of commands) {
2616
+ if (command2.startsWith(INSTALL_CERTIFICATE))
2617
+ continue;
2618
+ const reason = await refusal(client, substitute(command2));
2619
+ if (reason) {
2620
+ client.end();
2621
+ throw new Error(`${title}: the device will not parse ${command2.slice(0, 200)}
2622
+ ${reason}`);
2623
+ }
2624
+ }
2625
+ const stamp = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 12);
2626
+ const backup = `pre-apply-${stamp}`;
2627
+ const armRollback = [
2628
+ `/system scheduler add name="${ROLLBACK_SCHEDULER}"`,
2629
+ `interval=${options.rollback}m`,
2630
+ `on-event="/system backup load name=${backup} password=\\"\\""`,
2631
+ "policy=read,write,policy,test,reboot,ftp"
2632
+ ].join(" ");
2633
+ try {
2634
+ console.log(` saving ${backup}, arming a ${options.rollback} minute rollback`);
2635
+ await exec(client, `/system backup save name=${backup} dont-encrypt=yes`, 120000);
2636
+ await exec(client, `/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
2637
+ await exec(client, armRollback);
2638
+ for (const { title, command: command2 } of commands) {
2639
+ if (command2.startsWith(INSTALL_CERTIFICATE)) {
2640
+ const certificateName = command2.slice(INSTALL_CERTIFICATE.length).trim();
2641
+ await installCertificate(client, device, certificateName);
2642
+ continue;
2643
+ }
2644
+ const answer = (await exec(client, substitute(command2), 120000)).trim();
2645
+ if (answer)
2646
+ throw new Error(`${title}: ${command2.slice(0, 160)}
2647
+ device said: ${answer.slice(0, 300)}`);
2648
+ }
2649
+ } catch (error) {
2650
+ console.log(` FAILED: ${error.message}`);
2651
+ console.log(` restoring ${backup} now; the rollback stays armed in case this does not get through`);
2652
+ await exec(client, `/system backup load name=${backup} password=""`, 30000).catch(() => {
2653
+ return;
2654
+ });
2655
+ client.end();
2656
+ throw new Error("apply failed and the backup is being restored");
2657
+ }
2658
+ client.end();
2659
+ const proof = await login(device).catch(() => {
2660
+ return;
2661
+ });
2662
+ if (!proof)
2663
+ throw new Error(`could not log in again; the device restores ${backup} within ${options.rollback} minutes`);
2664
+ await exec(proof, `/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
2665
+ await exec(proof, `/file remove [find name="${backup}.backup"]`);
2666
+ proof.end();
2667
+ console.log(" logged in again, rollback disarmed");
2668
+ }
2669
+ async function fill(device, _network, set, family, prefixes) {
2670
+ const menu = family === "ipv4" ? "/ip firewall address-list" : "/ipv6 firewall address-list";
2671
+ const batchSize = 60;
2672
+ const client = await login(device);
2673
+ try {
2674
+ await exec(client, `${menu} remove [find list="${set}" and !dynamic]`);
2675
+ for (let start = 0;start < prefixes.length; start += batchSize) {
2676
+ const batch = prefixes.slice(start, start + batchSize).map((prefix) => `${menu} add list="${set}" address=${prefix}`);
2677
+ const answer = (await exec(client, batch.join("; "), 90000)).trim();
2678
+ if (answer)
2679
+ throw new Error(`${set}: ${answer.slice(0, 200)}`);
2680
+ }
2681
+ const count = (await exec(client, `:put [:len [${menu} find list="${set}"]]`)).trim();
2682
+ console.log(` ${set}: ${count} entries on the device`);
2683
+ } finally {
2684
+ client.end();
2685
+ }
2686
+ }
2687
+ var routeros = {
2688
+ extension: ".rsc",
2689
+ unsupported: unsupported2,
2690
+ render: render2,
2691
+ read: read2,
2692
+ plan: plan2,
2693
+ apply: apply2,
2694
+ fill
2695
+ };
2696
+
2697
+ // src/adapters/devices/vrp/index.ts
2698
+ var DEVICE_FACTS2 = [
2699
+ "!",
2700
+ "device ",
2701
+ "system resource",
2702
+ "drop-profile",
2703
+ "dcb ",
2704
+ "diffserv domain",
2705
+ "vlan reserved",
2706
+ "interface MEth",
2707
+ "interface NULL",
2708
+ "stack",
2709
+ "vm-manager",
2710
+ "return",
2711
+ "authentication-scheme default",
2712
+ "authorization-scheme default",
2713
+ "accounting-scheme default",
2714
+ "domain default",
2715
+ "Info:"
2716
+ ];
2717
+ var UNDO_BY_KEYWORD2 = /^(description|name|port default vlan|port link-type|port trunk pvid vlan|set authentication password|eth-trunk|ip binding vpn-instance)\b/;
2718
+ function undo2(line2) {
2719
+ if (line2.startsWith("undo "))
2720
+ return line2.slice("undo ".length);
2721
+ if (/^rule \d+/.test(line2))
2722
+ return `undo ${line2.split(" ").slice(0, 2).join(" ")}`;
2723
+ const keyword = UNDO_BY_KEYWORD2.exec(line2);
2724
+ if (keyword)
2725
+ return `undo ${keyword[1]}`;
2726
+ if (line2.startsWith("storm control "))
2727
+ return `undo ${line2.replace(/ (min-rate|max-rate).*$/, "")}`;
2728
+ return `undo ${line2}`;
2729
+ }
2730
+ function canon(line2) {
2731
+ return line2.replace(/(irreversible-cipher|cipher) \S+/g, "cipher <secret>").replace(/<secret:\w+>/g, "<secret>");
2732
+ }
2733
+ function order(path) {
2734
+ const header = path[0] ?? "";
2735
+ if (!header)
2736
+ return 1;
2737
+ if (/^(ip vpn-instance|vlan |acl number|aaa|bfd)/.test(header))
2738
+ return 0;
2739
+ if (header.startsWith("interface Eth-Trunk"))
2740
+ return 2;
2741
+ if (header.startsWith("interface Vlanif"))
2742
+ return 4;
2743
+ if (header.startsWith("interface "))
2744
+ return 3;
2745
+ if (header.startsWith("ospf "))
2746
+ return 5;
2747
+ return 6;
2748
+ }
2749
+ var dialect2 = {
2750
+ block: /^(interface |vlan \d+$|acl number |aaa$|user-interface |ospf |ip vpn-instance |bfd$)/,
2751
+ indented: true,
2752
+ terminator: "#",
2753
+ ignore: (line2) => DEVICE_FACTS2.some((fact) => line2.startsWith(fact)),
2754
+ undo: undo2,
2755
+ leave: "quit",
2756
+ first: /^authentication-mode /,
2757
+ canon,
2758
+ order
2759
+ };
2760
+ var MANAGEMENT_ACL2 = 2000;
2761
+ var OSPF_PROCESS2 = 1;
2762
+ var FIRST_COLLECTOR = 1;
2763
+ var SNMP_GROUP2 = "circuit";
2764
+ var USER_LEVEL = { admin: 3, operator: 2, "read-only": 1 };
2765
+ var SNMP_AUTH3 = { sha1: "sha", sha256: "sha2-256" };
2766
+ var SNMP_PRIVACY2 = { aes128: "aes128", des: "des56" };
2767
+ function batchIds(line2) {
2768
+ const words2 = line2.replace(/^vlan batch /, "").split(" ");
2769
+ const ids = [];
2770
+ for (let index = 0;index < words2.length; index++) {
2771
+ const first = Number(words2[index]);
2772
+ if (words2[index + 1] === "to") {
2773
+ const last = Number(words2[index + 2]);
2774
+ for (let id = first;id <= last; id++)
2775
+ ids.push(id);
2776
+ index += 2;
2777
+ } else {
2778
+ ids.push(first);
2779
+ }
2780
+ }
2781
+ return ids;
2782
+ }
2783
+ function vlanBatch(ids) {
2784
+ const sorted = [...new Set(ids)].sort((a, b) => a - b);
2785
+ const ranges = [];
2786
+ for (let i = 0;i < sorted.length; i++) {
2787
+ let end = i;
2788
+ while (sorted[end + 1] === sorted[end] + 1)
2789
+ end++;
2790
+ ranges.push(i === end ? `${sorted[i]}` : `${sorted[i]} to ${sorted[end]}`);
2791
+ i = end;
2792
+ }
2793
+ return ranges.join(" ");
2794
+ }
2795
+
2796
+ class Config {
2797
+ lines = [];
2798
+ line(text) {
2799
+ this.lines.push(text);
2800
+ }
2801
+ block(header, body) {
2802
+ this.lines.push(header, ...body.map((text) => ` ${text}`), "#");
2803
+ }
2804
+ separator() {
2805
+ this.lines.push("#");
2806
+ }
2807
+ text() {
2808
+ return this.lines.join(`
2809
+ `) + `
2810
+ `;
2811
+ }
2812
+ }
2813
+ function render3(device) {
2814
+ const config = new Config;
2815
+ const vlans = device.vlans ?? {};
2816
+ const vlanId = (name) => vlans[name].id;
2817
+ const interfaces = device.interfaces ?? {};
2818
+ const lagNumbers = lagIds(device);
2819
+ const nameOf = (portOrInterface) => {
2820
+ const iface = interfaces[portOrInterface];
2821
+ if (iface?.type === "lag")
2822
+ return `Eth-Trunk${lagNumbers.get(portOrInterface)}`;
2823
+ if (iface?.type === "vlan")
2824
+ return `Vlanif${vlanId(iface.vlan)}`;
2825
+ return spell(device.platform, device.model, portOrInterface);
2826
+ };
2827
+ system(config, device);
2828
+ vlanBlocks(config, device);
2829
+ managementAcl(config, device);
2830
+ users2(config, device);
2831
+ vrfs2(config, device);
2832
+ for (const [name, lag] of Object.entries(interfaces)) {
2833
+ if (lag.type === "lag")
2834
+ config.block(`interface ${nameOf(name)}`, [...lagMode(lag), ...switching2(lag, vlanId)]);
2835
+ }
2836
+ for (const [name, iface] of Object.entries(interfaces)) {
2837
+ if (iface.type === "vlan")
2838
+ config.block(`interface ${nameOf(name)}`, vlanInterface(device, name, iface));
2839
+ }
2840
+ for (const port of portsOf(device.platform, device.model)) {
2841
+ const settings = device.ports?.[port];
2842
+ config.block(`interface ${nameOf(port)}`, physicalPort2(device, port, settings, vlanId, lagNumbers));
2843
+ }
2844
+ staticRoutes2(config, device, nameOf);
2845
+ ospf2(config, device, nameOf);
2846
+ if (wantsBfd(device))
2847
+ config.block("bfd", []);
2848
+ if (device.dhcp_relay && Object.keys(device.dhcp_relay).length) {
2849
+ config.line("dhcp enable");
2850
+ config.separator();
2851
+ }
2852
+ flowExport2(config, device);
2853
+ snmp2(config, device);
2854
+ ssh(config, device);
2855
+ userInterfaces(config, device);
2856
+ return config.text();
2857
+ }
2858
+ function system(config, device) {
2859
+ config.line(`sysname ${device.name}`);
2860
+ config.separator();
2861
+ const timezone = device.system?.timezone;
2862
+ if (timezone) {
2863
+ const minutes = offset(timezone);
2864
+ const absolute = Math.abs(minutes);
2865
+ const hours = String(Math.floor(absolute / 60)).padStart(2, "0");
2866
+ const remainder = String(absolute % 60).padStart(2, "0");
2867
+ config.line(`clock timezone ${timezone.split("/").at(-1)} ${minutes < 0 ? "minus" : "add"} ${hours}:${remainder}:00`);
2868
+ config.separator();
2869
+ }
2870
+ const stp = device.stp;
2871
+ if (!stp)
2872
+ config.line("stp disable");
2873
+ else {
2874
+ if (stp.mode && stp.mode !== "mstp")
2875
+ config.line(`stp mode ${stp.mode}`);
2876
+ if (stp.priority !== undefined)
2877
+ config.line(`stp priority ${stp.priority}`);
2878
+ }
2879
+ if (device.lldp)
2880
+ config.line("lldp enable");
2881
+ config.separator();
2882
+ const ntp = device.system?.ntp;
2883
+ if (!ntp?.serve) {
2884
+ config.line("ntp server disable");
2885
+ config.line("ntp ipv6 server disable");
2886
+ }
2887
+ for (const server of ntp?.servers ?? [])
2888
+ config.line(`ntp unicast-server ${server}`);
2889
+ config.separator();
2890
+ const ids = Object.values(device.vlans ?? {}).map((vlan) => vlan.id);
2891
+ if (ids.length) {
2892
+ config.line(`vlan batch ${vlanBatch(ids)}`);
2893
+ config.separator();
2894
+ }
2895
+ if (!device.management?.telnet) {
2896
+ config.line("telnet server disable");
2897
+ config.line("telnet ipv6 server disable");
2898
+ config.separator();
2899
+ }
2900
+ }
2901
+ function vlanBlocks(config, device) {
2902
+ for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
2903
+ config.block(`vlan ${vlan.id}`, [`name ${name}`, ...vlan.description ? [`description ${vlan.description}`] : []]);
2904
+ }
2905
+ }
2906
+ function managementAcl(config, device) {
2907
+ const allow = device.management?.allow ?? [];
2908
+ if (!allow.length)
2909
+ return;
2910
+ const rules = allow.map((cidr, index) => `rule ${(index + 1) * 5} permit source ${host(cidr)} ${wildcard(split(cidr).len)}`);
2911
+ config.block(`acl number ${MANAGEMENT_ACL2}`, ["description Management access", ...rules, `rule ${(allow.length + 1) * 5} deny`]);
2912
+ }
2913
+ function users2(config, device) {
2914
+ const lines = [];
2915
+ for (const [name, user] of Object.entries(device.users ?? {})) {
2916
+ lines.push(`local-user ${name} password irreversible-cipher ${marker(user.password)}`);
2917
+ lines.push(`local-user ${name} service-type ssh`);
2918
+ lines.push(`local-user ${name} level ${USER_LEVEL[user.role]}`);
2919
+ }
2920
+ config.block("aaa", lines);
2921
+ }
2922
+ function vrfs2(config, device) {
2923
+ for (const name of Object.keys(device.vrfs ?? {}))
2924
+ config.block(`ip vpn-instance ${name}`, ["ipv4-family"]);
2925
+ }
2926
+ function lagMode(lag) {
2927
+ const lines = [lag.mode === "static" ? "mode manual load-balance" : "mode lacp-static"];
2928
+ if (lag.description)
2929
+ lines.unshift(`description ${lag.description}`);
2930
+ return lines;
2931
+ }
2932
+ function switching2(settings, vlanId) {
2933
+ const lines = [];
2934
+ if (settings.access_vlan)
2935
+ lines.push(`port default vlan ${vlanId(settings.access_vlan)}`);
2936
+ if (settings.trunk_vlans) {
2937
+ lines.push("port link-type trunk");
2938
+ if (settings.native_vlan)
2939
+ lines.push(`port trunk pvid vlan ${vlanId(settings.native_vlan)}`);
2940
+ const allowed = [...settings.trunk_vlans, ...settings.native_vlan ? [settings.native_vlan] : []].map(vlanId);
2941
+ lines.push("undo port trunk allow-pass vlan 1");
2942
+ lines.push(`port trunk allow-pass vlan ${vlanBatch(allowed)}`);
2943
+ }
2944
+ if (settings.stp === false)
2945
+ lines.push("stp disable");
2946
+ else if (typeof settings.stp === "object") {
2947
+ if (settings.stp.edge)
2948
+ lines.push("stp edged-port enable");
2949
+ if (settings.stp.cost !== undefined)
2950
+ lines.push(`stp cost ${settings.stp.cost}`);
2951
+ if (settings.stp.priority !== undefined)
2952
+ lines.push(`stp port priority ${settings.stp.priority}`);
2953
+ }
2954
+ const storm = settings.storm_control;
2955
+ if (storm) {
2956
+ const kinds = [
2957
+ ["broadcast", storm.broadcast],
2958
+ ["multicast", storm.multicast],
2959
+ ["unknown-unicast", storm.unknown_unicast]
2960
+ ];
2961
+ for (const [kind, threshold] of kinds) {
2962
+ if (!threshold || !("percent" in threshold))
2963
+ continue;
2964
+ const percent = threshold.percent;
2965
+ lines.push(`storm control ${kind} min-rate percent ${percent} max-rate percent ${percent}`);
2966
+ }
2967
+ lines.push("storm control action suppress", "storm control enable trap", "storm control enable log");
2968
+ }
2969
+ return lines;
2970
+ }
2971
+ function vrrpLines2(group) {
2972
+ const lines = [`vrrp vrid ${group.id} virtual-ip ${group.address}`];
2973
+ if (group.priority !== undefined)
2974
+ lines.push(`vrrp vrid ${group.id} priority ${group.priority}`);
2975
+ if (!group.preempt)
2976
+ lines.push(`vrrp vrid ${group.id} preempt disable`);
2977
+ if (group.interval !== undefined && group.interval !== 1)
2978
+ lines.push(`vrrp vrid ${group.id} timer advertise ${group.interval}`);
2979
+ return lines;
2980
+ }
2981
+ function vlanInterface(device, name, iface) {
2982
+ const lines = [];
2983
+ if (iface.description)
2984
+ lines.push(`description ${iface.description}`);
2985
+ if (iface.vrf)
2986
+ lines.push(`ip binding vpn-instance ${iface.vrf}`);
2987
+ for (const entry of iface.addresses ?? []) {
2988
+ const address = typeof entry === "string" ? entry : entry.address;
2989
+ if (familyOf(address) === "ipv4")
2990
+ lines.push(`ip address ${host(address)} ${netmask(split(address).len)}`);
2991
+ else
2992
+ lines.push(`ipv6 address ${address}`);
2993
+ }
2994
+ for (const group of iface.vrrp ?? [])
2995
+ lines.push(...vrrpLines2(group));
2996
+ const relays = Object.values(device.dhcp_relay ?? {}).filter((relay) => relay.interface === name);
2997
+ if (relays.length) {
2998
+ lines.push("dhcp select relay");
2999
+ for (const server of relays.flatMap((relay) => relay.servers))
3000
+ lines.push(`dhcp relay binding server ip ${server}`);
3001
+ }
3002
+ for (const area of Object.values(device.routing?.ospf?.areas ?? {})) {
3003
+ const settings = area.interfaces[name];
3004
+ if (!settings)
3005
+ continue;
3006
+ if (settings.cost !== undefined)
3007
+ lines.push(`ospf cost ${settings.cost}`);
3008
+ if (settings.network === "point-to-point")
3009
+ lines.push("ospf network-type p2p");
3010
+ if (settings.bfd)
3011
+ lines.push("ospf bfd enable");
3012
+ }
3013
+ return lines;
3014
+ }
3015
+ function physicalPort2(device, port, settings, vlanId, lagNumbers) {
3016
+ if (!settings)
3017
+ return ["shutdown"];
3018
+ if (settings.lag) {
3019
+ const membership = `eth-trunk ${lagNumbers.get(settings.lag)}`;
3020
+ return settings.description ? [membership, `description ${settings.description}`] : [membership];
3021
+ }
3022
+ const lines = [];
3023
+ if (settings.description)
3024
+ lines.push(`description ${settings.description}`);
3025
+ lines.push(...switching2(settings, vlanId));
3026
+ if (settings.lldp === false)
3027
+ lines.push("lldp disable");
3028
+ const flow = device.flow_export;
3029
+ const observed = flow?.protocol === "sflow" && (!flow.interfaces || flow.interfaces.includes(port));
3030
+ if (observed) {
3031
+ for (let index = 0;index < flow.collectors.length; index++)
3032
+ lines.push(`sflow sampling collector ${FIRST_COLLECTOR + index}`);
3033
+ lines.push(`sflow sampling rate ${flow.sampling}`);
3034
+ lines.push("sflow sampling inbound");
3035
+ }
3036
+ return lines;
3037
+ }
3038
+ function staticRoutes2(config, device, nameOf) {
3039
+ for (const route of device.routing?.static ?? []) {
3040
+ const { addr, len } = split(route.prefix);
3041
+ const nextHop = route.blackhole ? "NULL0" : route.via ?? nameOf(route.interface);
3042
+ const vrf = route.vrf ? ` vpn-instance ${route.vrf}` : "";
3043
+ const preference = route.distance ? ` preference ${route.distance}` : "";
3044
+ const description = route.description ? ` description ${route.description}` : "";
3045
+ if (familyOf(addr) === "ipv4")
3046
+ config.line(`ip route-static${vrf} ${addr} ${netmask(len)} ${nextHop}${preference}${description}`);
3047
+ else
3048
+ config.line(`ipv6 route-static${vrf} ${addr} ${len} ${nextHop}${preference}`);
3049
+ }
3050
+ config.separator();
3051
+ }
3052
+ function ospf2(config, device, nameOf) {
3053
+ const ospf3 = device.routing?.ospf;
3054
+ if (!ospf3)
3055
+ return;
3056
+ const routerId = device.routing?.router_id;
3057
+ const lines = [];
3058
+ for (const source of ospf3.redistribute ?? [])
3059
+ lines.push(`import-route ${source === "connected" ? "direct" : "static"}`);
3060
+ const areaLines = [];
3061
+ for (const [areaId, area] of Object.entries(ospf3.areas)) {
3062
+ areaLines.push(`area ${areaId}`);
3063
+ for (const [iface, settings] of Object.entries(area.interfaces)) {
3064
+ if (settings?.passive)
3065
+ lines.push(`silent-interface ${nameOf(iface)}`);
3066
+ for (const prefix of ipv4Networks(device, iface))
3067
+ areaLines.push(` network ${host(prefix)} ${wildcard(split(prefix).len)}`);
3068
+ }
3069
+ }
3070
+ config.block(`ospf ${OSPF_PROCESS2}${routerId ? ` router-id ${routerId}` : ""}`, [...lines, ...areaLines]);
3071
+ }
3072
+ function wantsBfd(device) {
3073
+ const areas = Object.values(device.routing?.ospf?.areas ?? {});
3074
+ return areas.some((area) => Object.values(area.interfaces).some((settings) => settings?.bfd));
3075
+ }
3076
+ function flowExport2(config, device) {
3077
+ const flow = device.flow_export;
3078
+ if (flow?.protocol !== "sflow")
3079
+ return;
3080
+ config.line(`sflow agent ip ${device.connection.host}`);
3081
+ flow.collectors.forEach((collector, index) => {
3082
+ const number = FIRST_COLLECTOR + index;
3083
+ config.line(`sflow collector ${number} ip ${collector.address} udp-port ${collector.port ?? 6343}`);
3084
+ });
3085
+ config.separator();
3086
+ }
3087
+ function snmp2(config, device) {
3088
+ const snmp3 = device.management?.snmp;
3089
+ if (!snmp3)
3090
+ return;
3091
+ const versions = [snmp3.community ? "v2c" : "", snmp3.users ? "v3" : ""].filter(Boolean).join(" ");
3092
+ const guarded = (device.management?.allow?.length ?? 0) > 0;
3093
+ config.line("snmp-agent");
3094
+ config.line(`snmp-agent sys-info version ${versions}`);
3095
+ if (snmp3.contact)
3096
+ config.line(`snmp-agent sys-info contact ${snmp3.contact}`);
3097
+ if (snmp3.location)
3098
+ config.line(`snmp-agent sys-info location ${snmp3.location}`);
3099
+ if (snmp3.community)
3100
+ config.line(`snmp-agent community read cipher ${marker(snmp3.community)}${guarded ? ` acl ${MANAGEMENT_ACL2}` : ""}`);
3101
+ if (snmp3.users) {
3102
+ config.line("snmp-agent mib-view included all iso");
3103
+ config.line(`snmp-agent group v3 ${SNMP_GROUP2} privacy read-view all${guarded ? ` acl ${MANAGEMENT_ACL2}` : ""}`);
3104
+ for (const [name, user] of Object.entries(snmp3.users)) {
3105
+ config.line(`snmp-agent usm-user v3 ${name} group ${SNMP_GROUP2}`);
3106
+ config.line(`snmp-agent usm-user v3 ${name} authentication-mode ${SNMP_AUTH3[user.auth]} cipher ${marker(user.auth_password)}`);
3107
+ config.line(`snmp-agent usm-user v3 ${name} privacy-mode ${SNMP_PRIVACY2[user.privacy]} cipher ${marker(user.privacy_password)}`);
3108
+ }
3109
+ }
3110
+ config.separator();
3111
+ }
3112
+ function ssh(config, device) {
3113
+ const ssh2 = device.management?.ssh;
3114
+ if (!ssh2)
3115
+ return;
3116
+ const guarded = (device.management?.allow?.length ?? 0) > 0;
3117
+ config.line("stelnet server enable");
3118
+ for (const user of Object.keys(device.users ?? {})) {
3119
+ config.line(`ssh user ${user}`);
3120
+ config.line(`ssh user ${user} authentication-type password`);
3121
+ config.line(`ssh user ${user} service-type stelnet`);
3122
+ }
3123
+ if (guarded)
3124
+ config.line(`ssh server acl ${MANAGEMENT_ACL2}`);
3125
+ config.line("ssh authorization-type default aaa");
3126
+ config.separator();
3127
+ if (!ssh2.weak_crypto) {
3128
+ config.line("ssh server cipher aes256_ctr aes128_ctr");
3129
+ config.line("ssh server hmac sha2_256_96 sha2_256");
3130
+ config.line("ssh server key-exchange dh_group_exchange_sha256 ecdh_sha2_nistp256 ecdh_sha2_nistp384 ecdh_sha2_nistp521");
3131
+ config.separator();
3132
+ }
3133
+ }
3134
+ function userInterfaces(config, device) {
3135
+ const console2 = device.management?.console;
3136
+ const consoleLogin = console2 ? ["authentication-mode password", `set authentication password cipher ${marker(console2.password)}`] : ["authentication-mode aaa"];
3137
+ config.block("user-interface con 0", consoleLogin);
3138
+ config.block("user-interface vty 0 4", ["authentication-mode aaa", `protocol inbound ${device.management?.telnet ? "all" : "ssh"}`]);
3139
+ }
3140
+ var HOST_KEYS2 = ["ssh-rsa"];
3141
+ async function login2(device) {
3142
+ const user = device.users?.[device.connection.user];
3143
+ if (!user)
3144
+ throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
3145
+ return connect({
3146
+ host: device.connection.host,
3147
+ port: device.connection.port,
3148
+ user: device.connection.user,
3149
+ password: resolve(user.password),
3150
+ hostKeys: HOST_KEYS2
3151
+ });
3152
+ }
3153
+ async function openShell(client) {
3154
+ const channel = await shell(client);
3155
+ const banner = await readUntil(channel, (text) => /Change now\? ?\[Y\/N\]/i.test(text) || /<[^>]+>\s*$/.test(text), 15000, 1500);
3156
+ if (/Change now\?/i.test(banner))
3157
+ await send(channel, "N", 1e4, 1500);
3158
+ await send(channel, "screen-length 0 temporary", 1e4, 1500);
3159
+ return channel;
3160
+ }
3161
+ async function read3(device) {
3162
+ const client = await login2(device);
3163
+ try {
3164
+ const channel = await openShell(client);
3165
+ const dump = await send(channel, "display current-configuration", 120000, 3000);
3166
+ channel.end();
3167
+ const lines = dump.replace(/\r/g, "").split(`
3168
+ `);
3169
+ const start = lines.findIndex((line2) => line2.startsWith("!Software Version"));
3170
+ if (start < 0)
3171
+ throw new Error(`${device.name} returned no configuration`);
3172
+ const body = lines.slice(start).filter((line2) => !/^<.*>$/.test(line2.trim()) && !line2.startsWith("!Last configuration"));
3173
+ return scrub(body.join(`
3174
+ `).trim() + `
3175
+ `);
3176
+ } finally {
3177
+ client.end();
3178
+ }
3179
+ }
3180
+ function unsupported3(device) {
3181
+ const found = [];
3182
+ for (const [name, port] of Object.entries(device.ports ?? {})) {
3183
+ if (!port)
3184
+ continue;
3185
+ for (const field of ["speed", "acl", "mtu", "addresses", "vrrp", "vrf"]) {
3186
+ if (port[field] !== undefined)
3187
+ found.push(`ports.${name}.${field}, ports are switched; use a VLAN interface`);
3188
+ }
3189
+ if (port.lldp === true && !device.lldp)
3190
+ found.push(`ports.${name}.lldp true while LLDP is off on the device`);
3191
+ const storm = Object.values(port.storm_control ?? {});
3192
+ if (storm.some((threshold) => ("pps" in threshold)))
3193
+ found.push(`ports.${name}.storm_control in pps, the platform takes percent`);
3194
+ }
3195
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
3196
+ if (iface.type !== "vlan" && iface.type !== "lag")
3197
+ found.push(`interfaces.${name}, a ${iface.type}`);
3198
+ if (iface.type === "lag" && iface.addresses?.length)
3199
+ found.push(`interfaces.${name}.addresses, LAGs are switched`);
3200
+ for (const group of iface.vrrp ?? [])
3201
+ if (familyOf(group.address) === "ipv6")
3202
+ found.push(`interfaces.${name}.vrrp for IPv6`);
3203
+ }
3204
+ const routing = device.routing;
3205
+ if (routing?.bgp)
3206
+ found.push("routing.bgp");
3207
+ if (routing?.rpki)
3208
+ found.push("routing.rpki");
3209
+ if (routing?.ospf?.families?.includes("ipv6"))
3210
+ found.push("routing.ospf for IPv6");
3211
+ for (const field of ["firewall", "dhcp", "acls", "policies", "prefix_sets", "hardware_offload", "certificates"]) {
3212
+ if (device[field] !== undefined)
3213
+ found.push(field);
3214
+ }
3215
+ for (const [name, relay] of Object.entries(device.dhcp_relay ?? {})) {
3216
+ if (device.interfaces?.[relay.interface]?.type !== "vlan")
3217
+ found.push(`dhcp_relay.${name}.interface, relays run on VLAN interfaces`);
3218
+ }
3219
+ if (device.users && "admin" in device.users)
3220
+ found.push("users.admin, the platform reserves the name");
3221
+ const management = device.management ?? {};
3222
+ for (const field of ["http", "https", "api", "api_tls", "ftp", "native", "privilege_password"]) {
3223
+ if (management[field])
3224
+ found.push(`management.${field}`);
3225
+ }
3226
+ if (management.ssh?.auth_timeout || management.ssh?.auth_retries)
3227
+ found.push("management.ssh.auth_timeout and auth_retries");
3228
+ if (device.lldp && device.lldp !== true)
3229
+ found.push("lldp.interfaces, use lldp on the ports instead");
3230
+ const system2 = device.system ?? {};
3231
+ if (system2.dns || system2.logging || system2.release_channel || system2.banner) {
3232
+ found.push("system.dns, logging, release_channel and banner");
3233
+ }
3234
+ const flow = device.flow_export;
3235
+ if (flow && flow.protocol !== "sflow")
3236
+ found.push(`flow_export.protocol ${flow.protocol}, the platform exports sFlow`);
3237
+ const sampling = flow?.protocol === "sflow" ? flow.sampling : undefined;
3238
+ const samplingInRange = sampling !== undefined && Number.isInteger(sampling) && sampling >= 4096 && sampling <= 4294967295;
3239
+ if (flow?.protocol === "sflow" && !samplingInRange) {
3240
+ found.push("flow_export.sampling, required and between 4096 and 4294967295: the platform samples, one packet in 4096 at most");
3241
+ }
3242
+ return found;
3243
+ }
3244
+ var REFUSED2 = /^\s*Error|Unrecognized command|Incomplete command|Wrong parameter|Too many parameters/m;
3245
+ var INTERACTIVE = /^(snmp-agent usm-user v3 \S+ (?:authentication|privacy)-mode \S+) cipher <secret:([A-Za-z0-9_./:-]+)>$/;
3246
+ async function sendLine(channel, line2) {
3247
+ const interactive = INTERACTIVE.exec(line2.trim());
3248
+ if (interactive) {
3249
+ const [, command2, reference] = interactive;
3250
+ const password = valueOfReference(reference);
3251
+ let answer2 = await send(channel, command2, 15000, 900);
3252
+ answer2 += await send(channel, password, 15000, 900);
3253
+ answer2 += await send(channel, password, 15000, 900);
3254
+ return answer2;
3255
+ }
3256
+ let answer = await send(channel, substitute(line2), 20000, 800);
3257
+ if (/\[Y\/N\]/i.test(answer))
3258
+ answer += await send(channel, "Y", 20000, 800);
3259
+ return answer;
3260
+ }
3261
+ async function apply3(device, _network, plan3) {
3262
+ const client = await login2(device);
3263
+ const channel = await openShell(client);
3264
+ try {
3265
+ await send(channel, "system-view", 1e4, 1500);
3266
+ for (const step of plan3.steps) {
3267
+ for (const line2 of step.send) {
3268
+ const answer = await sendLine(channel, line2);
3269
+ if (REFUSED2.test(answer))
3270
+ throw new Error(`${step.title}: ${line2}
3271
+ device said: ${answer.trim().slice(0, 300)}`);
3272
+ }
3273
+ }
3274
+ const committed = await send(channel, "commit", 60000, 3000);
3275
+ if (REFUSED2.test(committed))
3276
+ throw new Error(`commit refused: ${committed.trim().slice(0, 300)}`);
3277
+ await send(channel, "quit", 1e4);
3278
+ } catch (error) {
3279
+ await send(channel, "clear configuration candidate", 1e4).catch(() => {
3280
+ return;
3281
+ });
3282
+ channel.end();
3283
+ client.end();
3284
+ throw error;
3285
+ }
3286
+ const proof = await login2(device).catch(() => {
3287
+ return;
3288
+ });
3289
+ if (!proof) {
3290
+ channel.end();
3291
+ client.end();
3292
+ throw new Error("committed, but a fresh login failed. Not saved: a reboot restores the previous configuration. Recover over the open session or the console.");
3293
+ }
3294
+ proof.end();
3295
+ const saving = await send(channel, "save", 20000, 2000);
3296
+ if (/\[Y\/N\]/i.test(saving))
3297
+ await send(channel, "Y", 60000, 5000);
3298
+ channel.end();
3299
+ client.end();
3300
+ console.log(" committed, login proved, saved");
3301
+ }
3302
+ function plan3(desired, current) {
3303
+ const groups = compare(desired, current, dialect2);
3304
+ vlanListChange(groups, "vlan batch ", batchIds, (ids) => [`vlan batch ${vlanBatch(ids)}`], (ids) => [`vlan batch ${vlanBatch(ids)}`]);
3305
+ const top = groups.find((group) => group.path.length === 0);
3306
+ const removedBatch = top?.remove.find((line2) => line2.startsWith("vlan batch ")) ?? "";
3307
+ const removedIds = removedBatch ? batchIds(removedBatch) : [];
3308
+ const firstRemovals = [
3309
+ ...removedIds.map((id) => `interface Vlanif${id}`).filter((line2) => top?.remove.includes(line2)),
3310
+ ...removedIds.map((id) => `vlan ${id}`).filter((line2) => top?.remove.includes(line2)),
3311
+ ...removedBatch ? [removedBatch] : []
3312
+ ];
3313
+ if (top)
3314
+ top.remove = top.remove.filter((line2) => !firstRemovals.includes(line2));
3315
+ const planned = steps(groups.filter((group) => group.add.length || group.remove.length), dialect2);
3316
+ if (firstRemovals.length) {
3317
+ const sent = firstRemovals.filter((line2) => !/^vlan \d+$/.test(line2)).map(undo2);
3318
+ planned.unshift({ title: "VLANs no longer used", show: firstRemovals.map((line2) => `- ${line2}`), send: sent });
3319
+ }
3320
+ return { steps: planned, problems: [] };
3321
+ }
3322
+ var vrp = {
3323
+ extension: ".cfg",
3324
+ unsupported: unsupported3,
3325
+ render: render3,
3326
+ read: read3,
3327
+ plan: plan3,
3328
+ apply: apply3
3329
+ };
3330
+
3331
+ // src/adapters/devices/index.ts
3332
+ var adapters = { routeros, vrp, "raisecom-ros": raisecom };
3333
+
3334
+ // src/adapters/registries/index.ts
3335
+ async function ripeStat(query, family) {
3336
+ if (!/^AS\d+$/i.test(query))
3337
+ throw new Error(`ripe-stat takes an AS number, got '${query}'`);
3338
+ const res = await fetch(`https://stat.ripe.net/data/announced-prefixes/data.json?resource=${query.toUpperCase()}`, {
3339
+ signal: AbortSignal.timeout(90000)
3340
+ });
3341
+ if (!res.ok)
3342
+ throw new Error(`ripe-stat answered ${res.status}`);
3343
+ const data = await res.json();
3344
+ return [
3345
+ ...new Set(data.data.prefixes.map((entry) => entry.prefix).filter((entry) => entry.includes(":") === (family === "ipv6")))
3346
+ ].sort();
3347
+ }
3348
+ var registries = { "ripe-stat": ripeStat };
3349
+
3350
+ // src/adapters/registries/peeringdb.ts
3351
+ var API = "https://www.peeringdb.com/api";
3352
+ var addressesOf = (iface) => (iface.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
3353
+ function peersWithRouteServer(device, iface) {
3354
+ const bgp = device.routing?.bgp;
3355
+ const subnets = addressesOf(iface);
3356
+ return Object.values(bgp?.neighbors ?? {}).some((neighbor) => {
3357
+ const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
3358
+ const role = neighbor.role ?? group?.role;
3359
+ return role === "rs" && subnets.some((subnet) => contains(subnet, neighbor.address));
3360
+ });
3361
+ }
3362
+ function wanted(network2) {
3363
+ const records = [];
3364
+ for (const device of network2.devices) {
3365
+ for (const iface of Object.values(device.interfaces ?? {})) {
3366
+ const exchange = iface.exchange;
3367
+ if (!exchange?.peeringdb_ixlan)
3368
+ continue;
3369
+ const published = addressesOf(iface).map(host).filter(isGlobal);
3370
+ records.push({
3371
+ name: exchange.name,
3372
+ ixlan_id: exchange.peeringdb_ixlan,
3373
+ speed: exchange.speed,
3374
+ ipaddr4: published.find((address) => familyOf(address) === "ipv4") ?? null,
3375
+ ipaddr6: published.find((address) => familyOf(address) === "ipv6") ?? null,
3376
+ is_rs_peer: peersWithRouteServer(device, iface)
3377
+ });
3378
+ }
3379
+ }
3380
+ return records;
3381
+ }
3382
+ async function planPeeringDb(network2) {
3383
+ if (!network2.asn)
3384
+ throw new Error("the network declares no asn");
3385
+ const response = await fetch(`${API}/netixlan?asn=${network2.asn}`, {
3386
+ headers: { Accept: "application/json" },
3387
+ signal: AbortSignal.timeout(30000)
3388
+ });
3389
+ if (!response.ok)
3390
+ throw new Error(`PeeringDB answered ${response.status}`);
3391
+ const existing = (await response.json()).data;
3392
+ const desired = wanted(network2);
3393
+ const netId = existing[0]?.net_id;
3394
+ const changes2 = [];
3395
+ for (const record of desired) {
3396
+ const current = existing.find((candidate) => candidate.ixlan_id === record.ixlan_id);
3397
+ const body = {
3398
+ net_id: netId,
3399
+ ixlan_id: record.ixlan_id,
3400
+ asn: network2.asn,
3401
+ speed: record.speed,
3402
+ ipaddr4: record.ipaddr4,
3403
+ ipaddr6: record.ipaddr6,
3404
+ is_rs_peer: record.is_rs_peer,
3405
+ operational: current?.operational ?? true
3406
+ };
3407
+ if (!current) {
3408
+ changes2.push({ summary: `+ ${record.name}: ${record.ipaddr4}, ${record.ipaddr6}, ${record.speed} Mbit/s`, body });
3409
+ continue;
3410
+ }
3411
+ const differs = current.ipaddr4 !== record.ipaddr4 || current.ipaddr6 !== record.ipaddr6 || current.speed !== record.speed || current.is_rs_peer !== record.is_rs_peer;
3412
+ if (differs) {
3413
+ const before = `${current.ipaddr4}, ${current.ipaddr6}, ${current.speed} Mbit/s, route server ${current.is_rs_peer}`;
3414
+ const after = `${record.ipaddr4}, ${record.ipaddr6}, ${record.speed} Mbit/s, route server ${record.is_rs_peer}`;
3415
+ changes2.push({ summary: `~ ${record.name}: ${before} to ${after}`, id: current.id, body });
3416
+ }
3417
+ }
3418
+ const unknown = existing.filter((record) => !desired.some((candidate) => candidate.ixlan_id === record.ixlan_id)).map((record) => record.name);
3419
+ if (changes2.some((change) => !change.id) && !netId) {
3420
+ throw new Error("PeeringDB holds no presence for this network yet, so its record id is unknown; add the first one by hand");
3421
+ }
3422
+ return { changes: changes2, unknown };
3423
+ }
3424
+ async function sendPeeringDb(network2, changes2) {
3425
+ if (!network2.peeringdb)
3426
+ throw new Error("the network declares no peeringdb api_key");
3427
+ const apiKey = resolve(network2.peeringdb.api_key);
3428
+ for (const change of changes2) {
3429
+ const response = await fetch(`${API}/netixlan${change.id ? `/${change.id}` : ""}`, {
3430
+ method: change.id ? "PUT" : "POST",
3431
+ headers: { Authorization: `Api-Key ${apiKey}`, "Content-Type": "application/json" },
3432
+ body: JSON.stringify(change.body),
3433
+ signal: AbortSignal.timeout(30000)
3434
+ });
3435
+ if (!response.ok) {
3436
+ const reason = (await response.text()).slice(0, 300);
3437
+ throw new Error(`${change.summary}: PeeringDB answered ${response.status} ${reason}`);
3438
+ }
3439
+ console.log(` sent ${change.summary}`);
3440
+ }
3441
+ }
3442
+
3443
+ // src/core/validate.ts
3444
+ import { X509Certificate as X509Certificate2 } from "node:crypto";
3445
+ function validate(network2) {
3446
+ const findings = [];
3447
+ const seen = new Set;
3448
+ for (const device of network2.devices) {
3449
+ if (seen.has(device.name))
3450
+ findings.push({ level: "error", device: device.name, message: "declared twice" });
3451
+ seen.add(device.name);
3452
+ }
3453
+ for (const device of network2.devices) {
3454
+ const report = (message) => findings.push({ level: "error", device: device.name, message });
3455
+ checkDevice(device, report);
3456
+ const adapter = adapters[device.platform];
3457
+ if (!adapter)
3458
+ report(`no adapter for platform ${device.platform}`);
3459
+ else
3460
+ for (const field of adapter.unsupported(device))
3461
+ report(`${device.platform} cannot express ${field}`);
3462
+ }
3463
+ findings.push(...checkLinks(network2.devices));
3464
+ for (const check of network2.checks ?? [])
3465
+ findings.push(...check(network2.devices));
3466
+ return findings;
3467
+ }
3468
+
3469
+ class Names {
3470
+ device;
3471
+ report;
3472
+ ports;
3473
+ constructor(device, report) {
3474
+ this.device = device;
3475
+ this.report = report;
3476
+ let ports = [];
3477
+ try {
3478
+ ports = portsOf(device.platform, device.model);
3479
+ } catch (error) {
3480
+ report(error.message);
3481
+ }
3482
+ this.ports = ports;
3483
+ }
3484
+ isInterface(name) {
3485
+ return name in (this.device.interfaces ?? {}) || this.ports.includes(name);
3486
+ }
3487
+ expect(what, name, declared) {
3488
+ if (name !== undefined && !declared)
3489
+ this.report(`${what} '${name}' is not declared`);
3490
+ }
3491
+ expectInterface(what, name) {
3492
+ this.expect(what, name, name !== undefined && this.isInterface(name));
3493
+ }
3494
+ }
3495
+ function checkDevice(device, report) {
3496
+ const names = new Names(device, report);
3497
+ const cidr = (where, value2) => {
3498
+ if (!isCidr(value2))
3499
+ report(`${where}: '${value2}' is not an address with a length`);
3500
+ };
3501
+ const ip = (where, value2) => {
3502
+ if (!isAddress(value2))
3503
+ report(`${where}: '${value2}' is not an address`);
3504
+ };
3505
+ if (!device.users?.[device.connection.user])
3506
+ report(`connection user '${device.connection.user}' is not one of its users`);
3507
+ checkVlans(device, report);
3508
+ checkAddresses(device, report, cidr, ip);
3509
+ checkPorts(device, names, report);
3510
+ checkInterfaces(device, names, report, ip);
3511
+ checkRouting(device, names, report, cidr, ip);
3512
+ checkPolicies(device, names, report, cidr);
3513
+ checkFirewall(device, names, cidr);
3514
+ checkServices(device, names, report, cidr);
3515
+ checkCertificates(device, names, report);
3516
+ }
3517
+ function checkVlans(device, report) {
3518
+ const ids = new Map;
3519
+ for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
3520
+ if (!Number.isInteger(vlan.id) || vlan.id < 1 || vlan.id > 4094)
3521
+ report(`VLAN ${name} id ${vlan.id} is outside 1-4094`);
3522
+ if (ids.has(vlan.id))
3523
+ report(`VLANs ${ids.get(vlan.id)} and ${name} share id ${vlan.id}`);
3524
+ ids.set(vlan.id, name);
3525
+ }
3526
+ }
3527
+ function checkAddresses(device, report, cidr, ip) {
3528
+ const holderOf = new Map;
3529
+ const holders2 = [
3530
+ ...Object.entries(device.ports ?? {}).map(([name, settings]) => ({ where: `port ${name}`, settings })),
3531
+ ...Object.entries(device.interfaces ?? {}).map(([name, settings]) => ({ where: `interface ${name}`, settings }))
3532
+ ];
3533
+ for (const { where, settings } of holders2) {
3534
+ for (const entry of settings?.addresses ?? []) {
3535
+ const address = typeof entry === "string" ? entry : entry.address;
3536
+ cidr(where, address);
3537
+ if (typeof entry !== "string")
3538
+ ip(where, entry.peer);
3539
+ const bare = address.split("/")[0];
3540
+ if (holderOf.has(bare))
3541
+ report(`${bare} is on both ${holderOf.get(bare)} and ${where}`);
3542
+ holderOf.set(bare, where);
3543
+ }
3544
+ for (const group of settings?.vrrp ?? []) {
3545
+ ip(`${where} VRRP ${group.id}`, group.address);
3546
+ if (group.id < 1 || group.id > 255)
3547
+ report(`${where} VRRP id ${group.id} is outside 1-255`);
3548
+ }
3549
+ }
3550
+ }
3551
+ function checkPorts(device, names, report) {
3552
+ const vlans = device.vlans ?? {};
3553
+ for (const [name, port] of Object.entries(device.ports ?? {})) {
3554
+ if (!port)
3555
+ continue;
3556
+ const switched = Boolean(port.access_vlan || port.trunk_vlans);
3557
+ if (!names.ports.includes(name))
3558
+ report(`port ${name} does not exist on ${device.model}`);
3559
+ if (port.access_vlan && port.trunk_vlans)
3560
+ report(`port ${name} is both access and trunk`);
3561
+ if (port.native_vlan && !port.trunk_vlans)
3562
+ report(`port ${name} has a native VLAN but is not a trunk`);
3563
+ if (switched && port.addresses?.length)
3564
+ report(`port ${name} is switched and routed at once`);
3565
+ if (port.lag && (switched || port.addresses)) {
3566
+ report(`port ${name} is in a LAG and states its own switching or addresses; the LAG holds them`);
3567
+ }
3568
+ for (const vlan of [port.access_vlan, port.native_vlan, ...port.trunk_vlans ?? []]) {
3569
+ names.expect(`port ${name} VLAN`, vlan, vlan === undefined || vlan in vlans);
3570
+ }
3571
+ names.expect(`port ${name} ACL`, port.acl, port.acl !== undefined && port.acl in (device.acls ?? {}));
3572
+ names.expect(`port ${name} LAG`, port.lag, port.lag !== undefined && device.interfaces?.[port.lag]?.type === "lag");
3573
+ names.expect(`port ${name} VRF`, port.vrf, port.vrf !== undefined && port.vrf in (device.vrfs ?? {}));
3574
+ }
3575
+ }
3576
+ function checkInterfaces(device, names, report, ip) {
3577
+ const vlans = device.vlans ?? {};
3578
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
3579
+ if (names.ports.includes(name))
3580
+ report(`interface ${name} has the name of a port`);
3581
+ names.expect(`interface ${name} VRF`, iface.vrf, iface.vrf !== undefined && iface.vrf in (device.vrfs ?? {}));
3582
+ if (iface.type === "vlan")
3583
+ names.expect(`interface ${name} VLAN`, iface.vlan, iface.vlan in vlans);
3584
+ if (iface.type === "gre" || iface.type === "vxlan") {
3585
+ ip(`interface ${name}`, iface.local);
3586
+ ip(`interface ${name}`, iface.remote);
3587
+ }
3588
+ if (iface.type === "lag") {
3589
+ const members = Object.values(device.ports ?? {}).filter((port) => port?.lag === name);
3590
+ if (!members.length)
3591
+ report(`LAG ${name} has no member ports`);
3592
+ }
3593
+ if (iface.type === "wireguard") {
3594
+ for (const peer of iface.peers ?? []) {
3595
+ for (const address of peer.allowed_addresses) {
3596
+ if (!isCidr(address))
3597
+ report(`interface ${name} peer ${peer.name}: '${address}' is not an address with a length`);
3598
+ }
3599
+ }
3600
+ }
3601
+ }
3602
+ }
3603
+ function checkRouting(device, names, report, cidr, ip) {
3604
+ const routing = device.routing;
3605
+ const policies2 = device.policies ?? {};
3606
+ for (const route of routing?.static ?? []) {
3607
+ cidr("static route", route.prefix);
3608
+ if (!route.via && !route.interface && !route.blackhole)
3609
+ report(`static route ${route.prefix} has no next hop`);
3610
+ if (route.via)
3611
+ ip(`static route ${route.prefix}`, route.via);
3612
+ names.expectInterface(`static route ${route.prefix} interface`, route.interface);
3613
+ names.expect(`static route ${route.prefix} VRF`, route.vrf, route.vrf !== undefined && route.vrf in (device.vrfs ?? {}));
3614
+ }
3615
+ const bgp = routing?.bgp;
3616
+ const isPolicy = (policy) => policy !== undefined && (policy in policies2);
3617
+ for (const prefix of bgp?.networks ?? [])
3618
+ cidr("BGP network", prefix);
3619
+ for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
3620
+ for (const policy of [group.import, group.export]) {
3621
+ names.expect(`BGP group ${name} policy`, policy, isPolicy(policy));
3622
+ }
3623
+ }
3624
+ for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
3625
+ ip(`neighbor ${name}`, neighbor.address);
3626
+ const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
3627
+ names.expect(`neighbor ${name} group`, neighbor.group, group !== undefined);
3628
+ if (!(neighbor.remote_as ?? group?.remote_as))
3629
+ report(`neighbor ${name} has no remote_as`);
3630
+ for (const policy of [neighbor.import, neighbor.export]) {
3631
+ names.expect(`neighbor ${name} policy`, policy, isPolicy(policy));
3632
+ }
3633
+ const local = neighbor.local_address ?? group?.local_address;
3634
+ if (local && familyOf(local) !== familyOf(neighbor.address))
3635
+ report(`neighbor ${name} local_address and address differ in family`);
3636
+ }
3637
+ for (const [areaId, area] of Object.entries(routing?.ospf?.areas ?? {})) {
3638
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(areaId))
3639
+ report(`OSPF area '${areaId}' is not written as a dotted area ID`);
3640
+ for (const iface of Object.keys(area.interfaces))
3641
+ names.expectInterface(`OSPF area ${areaId} interface`, iface);
3642
+ }
3643
+ }
3644
+ function checkPolicies(device, names, report, cidr) {
3645
+ const policies2 = device.policies ?? {};
3646
+ const sets = device.prefix_sets ?? {};
3647
+ for (const [name, rules] of Object.entries(policies2)) {
3648
+ for (const rule of rules) {
3649
+ names.expect(`policy ${name} call`, rule.call, rule.call !== undefined && rule.call in policies2);
3650
+ const set = rule.match?.prefix_set;
3651
+ names.expect(`policy ${name} prefix set`, set, set !== undefined && set in sets);
3652
+ if (rule.match?.prefix)
3653
+ cidr(`policy ${name}`, rule.match.prefix);
3654
+ }
3655
+ }
3656
+ const visit = (policy, path) => {
3657
+ if (path.includes(policy))
3658
+ return report(`policy ${[...path, policy].join(" -> ")} calls itself`);
3659
+ for (const rule of policies2[policy] ?? [])
3660
+ if (rule.call)
3661
+ visit(rule.call, [...path, policy]);
3662
+ };
3663
+ for (const policy of Object.keys(policies2))
3664
+ visit(policy, []);
3665
+ }
3666
+ function checkFirewall(device, names, cidr) {
3667
+ const firewall = device.firewall;
3668
+ const sets = firewall?.address_sets ?? {};
3669
+ for (const [name, members] of Object.entries(sets)) {
3670
+ for (const member of members) {
3671
+ if (!isAddress(member))
3672
+ cidr(`address set ${name}`, member);
3673
+ }
3674
+ }
3675
+ const filterChains = [firewall?.filter?.input, firewall?.filter?.forward, firewall?.filter?.output];
3676
+ const filterRules = filterChains.flatMap((chain) => chain?.rules ?? []);
3677
+ const natRules = [...firewall?.nat?.source ?? [], ...firewall?.nat?.destination ?? []];
3678
+ for (const rule of [...filterRules, ...natRules]) {
3679
+ const match = rule.match;
3680
+ if (!match)
3681
+ continue;
3682
+ for (const set of [match.src_set, match.dst_set]) {
3683
+ names.expect("address set", set, set !== undefined && set in sets);
3684
+ }
3685
+ for (const iface of [match.in_interface, match.out_interface])
3686
+ names.expectInterface("interface", iface);
3687
+ for (const address of [match.src, match.dst]) {
3688
+ if (address)
3689
+ cidr("firewall rule", address);
3690
+ }
3691
+ }
3692
+ }
3693
+ function checkServices(device, names, report, cidr) {
3694
+ for (const [name, server] of Object.entries(device.dhcp ?? {})) {
3695
+ names.expectInterface(`DHCP ${name} interface`, server.interface);
3696
+ cidr(`DHCP ${name}`, server.network);
3697
+ if (!isCidr(server.network))
3698
+ continue;
3699
+ const reserved = (server.reservations ?? []).map((reservation) => reservation.address);
3700
+ const addresses2 = [server.gateway, ...server.pool, ...reserved];
3701
+ for (const address of addresses2) {
3702
+ if (!contains(server.network, address))
3703
+ report(`DHCP ${name}: ${address} is outside ${server.network}`);
3704
+ }
3705
+ }
3706
+ for (const [name, relay] of Object.entries(device.dhcp_relay ?? {})) {
3707
+ names.expectInterface(`DHCP relay ${name} interface`, relay.interface);
3708
+ }
3709
+ for (const iface of device.flow_export?.interfaces ?? [])
3710
+ names.expectInterface("flow export interface", iface);
3711
+ const lldp = device.lldp;
3712
+ if (lldp && lldp !== true)
3713
+ for (const iface of lldp.interfaces)
3714
+ names.expectInterface("LLDP interface", iface);
3715
+ const management = device.management;
3716
+ for (const iface of management?.native?.interfaces ?? [])
3717
+ names.expectInterface("native management interface", iface);
3718
+ for (const prefix of management?.allow ?? [])
3719
+ cidr("management allow", prefix);
3720
+ if (management?.snmp && !management.snmp.community && !management.snmp.users)
3721
+ report("SNMP is on with neither a community nor a user");
3722
+ }
3723
+ function checkCertificates(device, names, report) {
3724
+ const certificates2 = device.certificates ?? {};
3725
+ for (const [name, certificate] of Object.entries(certificates2)) {
3726
+ try {
3727
+ new X509Certificate2(certificate.certificate);
3728
+ } catch {
3729
+ report(`certificate ${name} is not a PEM certificate`);
3730
+ }
3731
+ }
3732
+ for (const service of ["https", "api_tls"]) {
3733
+ const name = device.management?.[service]?.certificate;
3734
+ names.expect(`${service} certificate`, name, name !== undefined && name in certificates2);
3735
+ if (name && certificates2[name] && !certificates2[name].private_key) {
3736
+ report(`${service} certificate ${name} has no private key to serve with`);
3737
+ }
3738
+ }
3739
+ }
3740
+ function carried(device, port) {
3741
+ const id = (vlan) => vlan ? device.vlans?.[vlan]?.id : undefined;
3742
+ return {
3743
+ access: id(port.access_vlan),
3744
+ native: id(port.native_vlan),
3745
+ trunk: (port.trunk_vlans ?? []).map(id).sort((a, b) => (a ?? 0) - (b ?? 0))
3746
+ };
3747
+ }
3748
+ function checkLinks(devices) {
3749
+ const findings = [];
3750
+ const byName = new Map(devices.map((device) => [device.name, device]));
3751
+ for (const device of devices) {
3752
+ for (const [portName, port] of Object.entries(device.ports ?? {})) {
3753
+ const link = port?.link;
3754
+ if (!port || !link)
3755
+ continue;
3756
+ const report = (message) => findings.push({
3757
+ level: "error",
3758
+ device: device.name,
3759
+ message: `port ${portName} link: ${message}`
3760
+ });
3761
+ const far = byName.get(link.device);
3762
+ if (!far) {
3763
+ report(`device ${link.device} is not in the network`);
3764
+ continue;
3765
+ }
3766
+ const farPort = far.ports?.[link.port];
3767
+ if (!farPort) {
3768
+ report(`${link.device} does not declare port ${link.port}`);
3769
+ continue;
3770
+ }
3771
+ const linksBack = farPort.link?.device === device.name && farPort.link.port === portName;
3772
+ if (!linksBack)
3773
+ report(`${link.device} ${link.port} does not link back to it`);
3774
+ const here = JSON.stringify(carried(device, port));
3775
+ const there = JSON.stringify(carried(far, farPort));
3776
+ if (here !== there)
3777
+ report(`carries ${here}, and ${link.device} ${link.port} carries ${there}`);
3778
+ }
3779
+ }
3780
+ return findings;
3781
+ }
3782
+
3783
+ // src/cli/index.ts
3784
+ var HELP = `usage: circuit <command> [device...] [options]
3785
+
3786
+ new [directory] start a project from a pattern, asking what it needs
3787
+ validate check the config, then every rule in checks
3788
+ build <device> print the configuration the device should run, without contacting it
3789
+ diff [device...] what apply would send, planned against the last snapshot, offline
3790
+ snapshot [device...] read what each device runs into the state directory
3791
+ apply <device> read the device, plan against what it runs, and send with --confirm
3792
+ --secrets also send every secret, to rotate one
3793
+ --rollback=N minutes before a device that can restore itself does, default 10
3794
+ refresh <device> fetch prefix sets that come from a registry onto the device, with --confirm
3795
+ wireguard <device> <interface> <peer>
3796
+ a client config for one peer, its private key left for the peer to fill
3797
+ communities the network's BGP communities, one per line, for a looking glass or bgp.tools
3798
+ peeringdb bring PeeringDB's exchange records in line with the config, with --confirm
3799
+ secrets every secret the config uses, and whether each can be read
3800
+
3801
+ --config <path> the network's config file, circuit.config.ts in the working directory by default`;
3802
+ var DEFAULT_CONFIG = "circuit.config.ts";
3803
+ var [command2, ...args] = process.argv.slice(2);
3804
+ if (command2 === "new") {
3805
+ const { create } = await import("../chunks/new-7n8qvtjd.js");
3806
+ await create(args);
3807
+ process.exit(0);
3808
+ }
3809
+ if (!command2 || command2 === "help" || command2 === "--help" || command2 === "-h") {
3810
+ console.log(HELP);
3811
+ process.exit(0);
3812
+ }
3813
+ var configAt = args.indexOf("--config");
3814
+ var configArgument = configAt >= 0 ? args[configAt + 1] : args.find((arg) => arg.startsWith("--config="))?.slice("--config=".length);
3815
+ var rest = args.filter((arg, index) => index !== configAt && (configAt < 0 || index !== configAt + 1) && !arg.startsWith("--config="));
3816
+ var configPath = resolve2(process.cwd(), configArgument ?? DEFAULT_CONFIG);
3817
+ if (!existsSync2(configPath)) {
3818
+ console.error(configArgument ? `no config at ${configPath}` : `no ${DEFAULT_CONFIG} in ${process.cwd()}; name another with --config <path>`);
3819
+ process.exit(1);
3820
+ }
3821
+ var root2 = dirname(configPath);
3822
+ useRoot(root2);
3823
+ async function load(path) {
3824
+ if (process.versions.bun)
3825
+ return (await import(path)).default;
3826
+ const { createJiti } = await import("jiti");
3827
+ return createJiti(import.meta.url).import(path, { default: true });
3828
+ }
3829
+ var network2 = await load(configPath);
3830
+ var flags = rest.filter((arg) => arg.startsWith("--"));
3831
+ var names = rest.filter((arg) => !arg.startsWith("--"));
3832
+ var confirmed = flags.includes("--confirm");
3833
+ var options = {
3834
+ secrets: flags.includes("--secrets"),
3835
+ rollback: Number(flags.find((flag) => flag.startsWith("--rollback="))?.split("=")[1] ?? 10)
3836
+ };
3837
+ function fail(message) {
3838
+ console.error(message);
3839
+ process.exit(1);
3840
+ }
3841
+ function selected(allByDefault) {
3842
+ if (!names.length && allByDefault)
3843
+ return [...network2.devices];
3844
+ const known = network2.devices.map((device) => device.name).join(", ");
3845
+ const byName = (name) => network2.devices.find((device) => device.name === name);
3846
+ return names.map((name) => byName(name) ?? fail(`no device ${name}; known: ${known}`));
3847
+ }
3848
+ function one() {
3849
+ const [device] = selected(false);
3850
+ return device ?? fail("name a device");
3851
+ }
3852
+ var adapterOf = (device) => adapters[device.platform] ?? fail(`no adapter for ${device.platform}`);
3853
+ var generated = join2(root2, ".circuit");
3854
+ var stateDirectory = join2(generated, "state");
3855
+ var statePath = (device) => join2(stateDirectory, device.name + adapterOf(device).extension);
3856
+ var GENERATED_README = `# Written by Circuit
3857
+
3858
+ Do not edit anything here: an edit changes nothing on a device, and Circuit overwrites it. Change the config instead.
3859
+
3860
+ \`state/\` holds what each device ran when Circuit last read it, with secrets removed. \`circuit snapshot\` and \`circuit apply\` write it, and \`circuit diff\` plans against it. Commit it, so the history of this directory is the history of the network.
3861
+ `;
3862
+ function writeState(device, text) {
3863
+ mkdirSync(stateDirectory, { recursive: true });
3864
+ writeFileSync(join2(generated, "README.md"), GENERATED_README);
3865
+ writeFileSync(statePath(device), text);
3866
+ }
3867
+ function guard(devices) {
3868
+ const relevant = new Set(devices.map((device) => device.name));
3869
+ const findings = validate(network2).filter((finding) => !finding.device || relevant.has(finding.device));
3870
+ for (const finding of findings) {
3871
+ const where = finding.device ? `${finding.device}: ` : "";
3872
+ const level = finding.level === "error" ? "error" : "warn ";
3873
+ console.log(` ${level} ${where}${finding.message}`);
3874
+ }
3875
+ const errors = findings.filter((finding) => finding.level === "error").length;
3876
+ if (errors)
3877
+ fail(`
3878
+ ${errors} error(s).`);
3879
+ }
3880
+ function show(device, plan4) {
3881
+ const commands = plan4.steps.reduce((count, step) => count + step.send.length, 0);
3882
+ const summary = commands ? `${commands} command(s) in ${plan4.steps.length} step(s)` : "matches";
3883
+ console.log(`${device.name}: ${summary}`);
3884
+ for (const step of plan4.steps) {
3885
+ console.log(` ${step.title}`);
3886
+ for (const line2 of step.show)
3887
+ console.log(` ${line2.slice(0, 220)}`);
3888
+ }
3889
+ for (const problem of plan4.problems)
3890
+ console.log(` PROBLEM ${problem}`);
3891
+ return commands + plan4.problems.length;
3892
+ }
3893
+ function validateCommand() {
3894
+ guard([...network2.devices]);
3895
+ console.log(`${network2.devices.length} device(s), no errors.`);
3896
+ }
3897
+ function buildCommand() {
3898
+ const device = one();
3899
+ process.stdout.write(adapterOf(device).render(device));
3900
+ }
3901
+ function diffCommand() {
3902
+ const devices = selected(true);
3903
+ guard(devices);
3904
+ let differences = 0;
3905
+ for (const device of devices) {
3906
+ const path = statePath(device);
3907
+ if (!existsSync2(path)) {
3908
+ console.log(`${device.name}: no snapshot, run snapshot first`);
3909
+ continue;
3910
+ }
3911
+ const adapter = adapterOf(device);
3912
+ const plan4 = adapter.plan(adapter.render(device), readFileSync2(path, "utf8"), options);
3913
+ differences += show(device, plan4);
3914
+ }
3915
+ process.exit(differences ? 1 : 0);
3916
+ }
3917
+ async function snapshotCommand() {
3918
+ for (const device of selected(true)) {
3919
+ const text = await adapterOf(device).read(device, network2.devices);
3920
+ writeState(device, text);
3921
+ console.log(`${device.name}: ${text.split(`
3922
+ `).length} lines`);
3923
+ }
3924
+ if (names.length || !existsSync2(stateDirectory))
3925
+ return;
3926
+ const current = new Set(network2.devices.map((device) => basename(statePath(device))));
3927
+ for (const file of readdirSync(stateDirectory)) {
3928
+ if (current.has(file))
3929
+ continue;
3930
+ rmSync(join2(stateDirectory, file));
3931
+ console.log(`${file}: removed, no such device in the config`);
3932
+ }
3933
+ }
3934
+ async function applyCommand() {
3935
+ const device = one();
3936
+ guard([device]);
3937
+ const adapter = adapterOf(device);
3938
+ const live = await adapter.read(device, network2.devices);
3939
+ writeState(device, live);
3940
+ const plan4 = adapter.plan(adapter.render(device), live, options);
3941
+ if (!show(device, plan4))
3942
+ return;
3943
+ if (plan4.problems.length)
3944
+ fail(`
3945
+ Not sent: resolve the problems first.`);
3946
+ if (!confirmed) {
3947
+ console.log(`
3948
+ Nothing sent. Re-run with --confirm.`);
3949
+ return;
3950
+ }
3951
+ await adapter.apply(device, network2.devices, plan4, options);
3952
+ writeState(device, await adapter.read(device, network2.devices));
3953
+ console.log("snapshot updated");
3954
+ }
3955
+ async function refreshCommand() {
3956
+ const device = one();
3957
+ const fill2 = adapterOf(device).fill ?? fail(`${device.platform} cannot hold a fetched prefix set`);
3958
+ for (const [name, set] of Object.entries(device.prefix_sets ?? {})) {
3959
+ if (!("source" in set))
3960
+ continue;
3961
+ const { registry, query, family } = set.source;
3962
+ const prefixes = await registries[registry](query, family);
3963
+ if (prefixes.length < 1 || prefixes.length > 1e5) {
3964
+ fail(`${name}: ${prefixes.length} prefixes from ${registry}, refusing`);
3965
+ }
3966
+ console.log(`${name}: ${prefixes.length} prefixes from ${registry} ${query}`);
3967
+ if (confirmed)
3968
+ await fill2(device, network2.devices, name, family, prefixes);
3969
+ }
3970
+ if (!confirmed)
3971
+ console.log(`
3972
+ Nothing sent. Re-run with --confirm.`);
3973
+ }
3974
+ function publicKeyOf(privateKey) {
3975
+ const pkcs8Prefix = Buffer.from("302e020100300506032b656e04220420", "hex");
3976
+ const der = Buffer.concat([pkcs8Prefix, Buffer.from(privateKey, "base64")]);
3977
+ const key = createPrivateKey({ key: der, format: "der", type: "pkcs8" });
3978
+ const publicDer = createPublicKey(key).export({ format: "der", type: "spki" });
3979
+ return publicDer.subarray(-32).toString("base64");
3980
+ }
3981
+ function wireguardCommand() {
3982
+ const [deviceName, interfaceName, peerName] = names;
3983
+ const device = network2.devices.find((candidate) => candidate.name === deviceName) ?? fail(HELP);
3984
+ const iface = device.interfaces?.[interfaceName ?? ""];
3985
+ if (iface?.type !== "wireguard")
3986
+ fail(`${deviceName} has no WireGuard interface ${interfaceName}`);
3987
+ const peers = iface.peers ?? [];
3988
+ const peerNames = peers.map((candidate) => candidate.name).join(", ");
3989
+ const peer = peers.find((candidate) => candidate.name === peerName) ?? fail(`no peer ${peerName} on ${interfaceName}: ${peerNames}`);
3990
+ const tunnelNetworks = (iface.addresses ?? []).map((entry) => network(typeof entry === "string" ? entry : entry.address));
3991
+ const lines = [
3992
+ "[Interface]",
3993
+ `# ${peerName}'s own private key. It never leaves their machine.`,
3994
+ "PrivateKey = <FILL IN>",
3995
+ `Address = ${peer.allowed_addresses.join(", ")}`,
3996
+ "",
3997
+ "[Peer]",
3998
+ `PublicKey = ${publicKeyOf(resolve(iface.private_key))}`,
3999
+ `Endpoint = ${iface.endpoint ?? "<FILL IN>"}:${iface.listen_port}`,
4000
+ `AllowedIPs = ${(peer.client_allowed_addresses ?? tunnelNetworks).join(", ")}`,
4001
+ "PersistentKeepalive = 25",
4002
+ ""
4003
+ ];
4004
+ process.stdout.write(lines.join(`
4005
+ `));
4006
+ }
4007
+ function communitiesCommand() {
4008
+ const communities = network2.communities ?? fail("the network declares no communities");
4009
+ for (const { community, description } of communities) {
4010
+ if (community.includes(","))
4011
+ fail(`community ${community} holds a comma`);
4012
+ console.log(`${community},${description}`);
4013
+ }
4014
+ }
4015
+ async function peeringdbCommand() {
4016
+ const plan4 = await planPeeringDb(network2);
4017
+ for (const name of plan4.unknown) {
4018
+ console.log(` PeeringDB lists ${name}, which the config does not. Remove it by hand if the network left.`);
4019
+ }
4020
+ for (const change of plan4.changes)
4021
+ console.log(` ${change.summary}`);
4022
+ if (!plan4.changes.length) {
4023
+ console.log("PeeringDB matches the config.");
4024
+ return;
4025
+ }
4026
+ if (!confirmed) {
4027
+ console.log(`
4028
+ Nothing sent. Re-run with --confirm.`);
4029
+ return;
4030
+ }
4031
+ await sendPeeringDb(network2, plan4.changes);
4032
+ }
4033
+ function secretsIn(value2, found = new Set) {
4034
+ if (isSecret(value2))
4035
+ found.add(referenceOf(value2));
4036
+ else if (Array.isArray(value2))
4037
+ for (const item of value2)
4038
+ secretsIn(item, found);
4039
+ else if (typeof value2 === "object" && value2 !== null)
4040
+ for (const item of Object.values(value2))
4041
+ secretsIn(item, found);
4042
+ return found;
4043
+ }
4044
+ function secretsCommand() {
4045
+ const references = [...secretsIn([network2.devices, network2.peeringdb])].sort();
4046
+ let missing = 0;
4047
+ for (const reference of references) {
4048
+ const name = reference.replace(/^file:/, "");
4049
+ try {
4050
+ resolve(reference);
4051
+ console.log(` ok ${name}, from ${sourceOf(reference)}`);
4052
+ } catch (error) {
4053
+ missing++;
4054
+ console.log(` missing ${name}: ${error.message}`);
4055
+ }
4056
+ }
4057
+ console.log(`
4058
+ ${references.length} secret(s), ${missing} missing.`);
4059
+ process.exit(missing ? 1 : 0);
4060
+ }
4061
+ var commands = {
4062
+ validate: validateCommand,
4063
+ build: buildCommand,
4064
+ diff: diffCommand,
4065
+ snapshot: snapshotCommand,
4066
+ apply: applyCommand,
4067
+ refresh: refreshCommand,
4068
+ wireguard: wireguardCommand,
4069
+ communities: communitiesCommand,
4070
+ peeringdb: peeringdbCommand,
4071
+ secrets: secretsCommand
4072
+ };
4073
+ var run = commands[command2];
4074
+ if (run)
4075
+ await run();
4076
+ else
4077
+ fail(`no command ${command2}
4078
+
4079
+ ${HELP}`);