@takodotid/circuit 0.25.0 → 0.26.1
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.
- package/README.md +7 -7
- package/dist/chunks/{index-wm9ypkjq.js → index-22sbskdx.js} +13 -5
- package/dist/chunks/{server-n2q4tzy4.js → server-1ftkqkx5.js} +1 -1
- package/dist/cli/index.js +2990 -264
- package/dist/types/adapters/devices/catalog.d.ts +14 -0
- package/dist/types/adapters/devices/fortios/firewall.d.ts +21 -0
- package/dist/types/adapters/devices/fortios/index.d.ts +55 -0
- package/dist/types/adapters/devices/fortios/models.d.ts +14 -0
- package/dist/types/adapters/devices/fortios/names.d.ts +32 -0
- package/dist/types/adapters/devices/fortios/plan.d.ts +26 -0
- package/dist/types/adapters/devices/fortios/render.d.ts +36 -0
- package/dist/types/adapters/devices/fortios/routing.d.ts +32 -0
- package/dist/types/adapters/devices/fortios/tree.d.ts +33 -0
- package/dist/types/schema/common.d.ts +1 -1
- package/dist/types/schema/device.d.ts +1 -1
- package/dist/types/schema/firewall.d.ts +3 -3
- package/dist/types/schema/interfaces.d.ts +13 -13
- package/dist/types/schema/policy.d.ts +3 -3
- package/dist/types/schema/routing.d.ts +7 -7
- package/dist/types/schema/system.d.ts +7 -7
- package/package.json +5 -2
- package/src/adapters/devices/catalog.ts +2 -1
- package/src/adapters/devices/fortios/TRAPS.md +40 -0
- package/src/adapters/devices/fortios/firewall.ts +352 -0
- package/src/adapters/devices/fortios/index.ts +800 -0
- package/src/adapters/devices/fortios/models.ts +7 -0
- package/src/adapters/devices/fortios/names.ts +78 -0
- package/src/adapters/devices/fortios/plan.ts +840 -0
- package/src/adapters/devices/fortios/render.ts +734 -0
- package/src/adapters/devices/fortios/routing.ts +444 -0
- package/src/adapters/devices/fortios/tree.ts +121 -0
- package/src/adapters/devices/index.ts +2 -1
- package/src/adapters/devices/junos/TRAPS.md +2 -2
- package/src/adapters/devices/raisecom-ros/TRAPS.md +2 -2
- package/src/adapters/devices/routeros/TRAPS.md +5 -5
- package/src/adapters/devices/vrp/TRAPS.md +4 -4
- package/src/schema/common.ts +1 -1
- package/src/schema/device.ts +1 -1
- package/src/schema/firewall.ts +3 -3
- package/src/schema/interfaces.ts +13 -13
- package/src/schema/policy.ts +3 -3
- package/src/schema/routing.ts +7 -7
- package/src/schema/system.ts +7 -7
package/dist/cli/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
addressesOf,
|
|
12
12
|
freeOfLength,
|
|
13
13
|
studioData
|
|
14
|
-
} from "../chunks/index-
|
|
14
|
+
} from "../chunks/index-22sbskdx.js";
|
|
15
15
|
import {
|
|
16
16
|
familyOf2,
|
|
17
17
|
split,
|
|
@@ -202,6 +202,2732 @@ function upload(client, path, content) {
|
|
|
202
202
|
});
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// src/adapters/devices/fortios/names.ts
|
|
206
|
+
function nameOf(device, name) {
|
|
207
|
+
if (device.interfaces && name in device.interfaces)
|
|
208
|
+
return name;
|
|
209
|
+
return spell(device.platform, device.model, name);
|
|
210
|
+
}
|
|
211
|
+
function settingsOf(device, name) {
|
|
212
|
+
return device.interfaces?.[name] ?? device.ports?.[name];
|
|
213
|
+
}
|
|
214
|
+
function addressesOf2(settings) {
|
|
215
|
+
return (settings?.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
|
|
216
|
+
}
|
|
217
|
+
var withLength = (value) => value.includes("/") ? value : `${value}/${familyOf2(value) === "ipv4" ? 32 : 128}`;
|
|
218
|
+
var familyOfPrefix = (value) => familyOf2(split(withLength(value)).addr);
|
|
219
|
+
var subnet = (prefix) => `${host2(prefix)} ${netmask(split(prefix).len)}`;
|
|
220
|
+
var network2 = (prefix) => network(withLength(prefix));
|
|
221
|
+
function vrfNumber(device, vrf) {
|
|
222
|
+
return vrf ? Object.keys(device.vrfs ?? {}).indexOf(vrf) + 1 : 0;
|
|
223
|
+
}
|
|
224
|
+
function routedHolders(device) {
|
|
225
|
+
const holders = [...Object.entries(device.ports ?? {}), ...Object.entries(device.interfaces ?? {})];
|
|
226
|
+
return holders.filter((holder) => holder[1] !== undefined && !holder[1].lag);
|
|
227
|
+
}
|
|
228
|
+
function interfaceFor(device, address, vrf) {
|
|
229
|
+
let best;
|
|
230
|
+
for (const [name, settings] of routedHolders(device)) {
|
|
231
|
+
if ((settings.vrf ?? undefined) !== vrf)
|
|
232
|
+
continue;
|
|
233
|
+
for (const own of addressesOf2(settings)) {
|
|
234
|
+
const length = split(own).len;
|
|
235
|
+
const holds = familyOf2(host2(own)) === familyOf2(address) && contains2(network2(own), address);
|
|
236
|
+
if (holds && length > (best?.length ?? -1))
|
|
237
|
+
best = { name, length };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return best?.name;
|
|
241
|
+
}
|
|
242
|
+
function holderOf(device, address) {
|
|
243
|
+
return routedHolders(device).find(([, settings]) => addressesOf2(settings).some((own) => host2(own) === address))?.[0];
|
|
244
|
+
}
|
|
245
|
+
function addressed(device) {
|
|
246
|
+
return routedHolders(device).filter(([, settings]) => addressesOf2(settings).length > 0).map(([name]) => name);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/adapters/devices/fortios/tree.ts
|
|
250
|
+
var UNSET = "\x00unset";
|
|
251
|
+
|
|
252
|
+
class Block {
|
|
253
|
+
settings = new Map;
|
|
254
|
+
blocks = new Map;
|
|
255
|
+
entries = new Map;
|
|
256
|
+
set(key, value) {
|
|
257
|
+
this.settings.set(key, String(value));
|
|
258
|
+
return this;
|
|
259
|
+
}
|
|
260
|
+
block(header) {
|
|
261
|
+
if (!this.blocks.has(header))
|
|
262
|
+
this.blocks.set(header, new Block);
|
|
263
|
+
return this.blocks.get(header);
|
|
264
|
+
}
|
|
265
|
+
entry(id, plain = false) {
|
|
266
|
+
const written = typeof id === "number" || plain ? String(id) : quote(id);
|
|
267
|
+
const key = bare(written);
|
|
268
|
+
if (!this.entries.has(key))
|
|
269
|
+
this.entries.set(key, { id: written, block: new Block });
|
|
270
|
+
return this.entries.get(key).block;
|
|
271
|
+
}
|
|
272
|
+
isEmpty() {
|
|
273
|
+
return !this.settings.size && !this.blocks.size && !this.entries.size;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function quote(text) {
|
|
277
|
+
return `"${text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
278
|
+
}
|
|
279
|
+
var quoteAll = (texts) => texts.map(quote).join(" ");
|
|
280
|
+
function wordsOf(value) {
|
|
281
|
+
return value.match(/"(?:[^"\\]|\\.)*"|'[^']*'|\S+/g) ?? [];
|
|
282
|
+
}
|
|
283
|
+
function bare(word) {
|
|
284
|
+
if (word.startsWith('"'))
|
|
285
|
+
return word.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
286
|
+
if (word.startsWith("'"))
|
|
287
|
+
return word.slice(1, -1);
|
|
288
|
+
return word;
|
|
289
|
+
}
|
|
290
|
+
function linesOf(block, indent = "") {
|
|
291
|
+
const lines = [];
|
|
292
|
+
for (const [key, value] of block.settings)
|
|
293
|
+
lines.push(value === UNSET ? `${indent}unset ${key}` : `${indent}set ${key} ${value}`);
|
|
294
|
+
for (const [header, child] of block.blocks)
|
|
295
|
+
lines.push(`${indent}config ${header}`, ...linesOf(child, indent + " "), `${indent}end`);
|
|
296
|
+
for (const { id, block: entry } of block.entries.values()) {
|
|
297
|
+
lines.push(`${indent}edit ${id}`, ...linesOf(entry, indent + " "), `${indent}next`);
|
|
298
|
+
}
|
|
299
|
+
return lines;
|
|
300
|
+
}
|
|
301
|
+
function print(root) {
|
|
302
|
+
const lines = [];
|
|
303
|
+
for (const [section, block] of root.blocks)
|
|
304
|
+
lines.push(`config ${section}`, ...linesOf(block, " "), "end");
|
|
305
|
+
return lines.join(`
|
|
306
|
+
`) + `
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
var unclosed = (line) => (line.replace(/\\./g, "").match(/"/g)?.length ?? 0) % 2 === 1;
|
|
310
|
+
function parse(text) {
|
|
311
|
+
const root = new Block;
|
|
312
|
+
const stack = [{ block: root, isEntry: false }];
|
|
313
|
+
const lines = text.replace(/\r/g, "").split(`
|
|
314
|
+
`);
|
|
315
|
+
for (let index = 0;index < lines.length; index++) {
|
|
316
|
+
let line = lines[index].trim();
|
|
317
|
+
if (!line || line.startsWith("#"))
|
|
318
|
+
continue;
|
|
319
|
+
while (unclosed(line) && index + 1 < lines.length)
|
|
320
|
+
line += `
|
|
321
|
+
` + lines[++index];
|
|
322
|
+
const current = stack.at(-1).block;
|
|
323
|
+
const [verb] = line.split(/\s+/, 1);
|
|
324
|
+
const rest = line.slice(verb.length).trim();
|
|
325
|
+
if (verb === "config") {
|
|
326
|
+
const child = current.block(rest);
|
|
327
|
+
stack.push({ block: child, isEntry: false });
|
|
328
|
+
} else if (verb === "edit") {
|
|
329
|
+
const key = bare(rest);
|
|
330
|
+
if (!current.entries.has(key))
|
|
331
|
+
current.entries.set(key, { id: rest, block: new Block });
|
|
332
|
+
stack.push({ block: current.entries.get(key).block, isEntry: true });
|
|
333
|
+
} else if (verb === "next") {
|
|
334
|
+
if (stack.at(-1).isEntry)
|
|
335
|
+
stack.pop();
|
|
336
|
+
} else if (verb === "end") {
|
|
337
|
+
if (stack.at(-1).isEntry)
|
|
338
|
+
stack.pop();
|
|
339
|
+
if (stack.length > 1)
|
|
340
|
+
stack.pop();
|
|
341
|
+
} else if (verb === "set") {
|
|
342
|
+
const [key] = rest.split(/\s+/, 1);
|
|
343
|
+
current.settings.set(key, rest.slice(key.length).trim());
|
|
344
|
+
} else if (verb === "unset") {
|
|
345
|
+
current.settings.set(rest, UNSET);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return root;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/adapters/devices/fortios/firewall.ts
|
|
352
|
+
var PROTOCOL_NUMBER = { tcp: 6, udp: 17, icmp: 1, gre: 47, esp: 50, ah: 51, ospf: 89, vrrp: 112 };
|
|
353
|
+
var SERVICES = {
|
|
354
|
+
ssh: { protocol: "tcp", port: 22 },
|
|
355
|
+
https: { protocol: "tcp", port: 443 },
|
|
356
|
+
http: { protocol: "tcp", port: 80 },
|
|
357
|
+
telnet: { protocol: "tcp", port: 23 },
|
|
358
|
+
snmp: { protocol: "udp", port: 161 },
|
|
359
|
+
native: { protocol: "tcp", port: 541 }
|
|
360
|
+
};
|
|
361
|
+
function natRules(device) {
|
|
362
|
+
const nat = device.firewall?.nat;
|
|
363
|
+
return [...nat?.source ?? [], ...nat?.destination ?? []];
|
|
364
|
+
}
|
|
365
|
+
function familiesOf(device, match) {
|
|
366
|
+
if (match?.family)
|
|
367
|
+
return [match.family];
|
|
368
|
+
const prefixes = [match?.src, match?.dst].filter((prefix) => prefix !== undefined);
|
|
369
|
+
if (prefixes.length)
|
|
370
|
+
return [...new Set(prefixes.map(familyOfPrefix))];
|
|
371
|
+
let families = ["ipv4", "ipv6"];
|
|
372
|
+
for (const set of [match?.src_set, match?.dst_set]) {
|
|
373
|
+
if (!set)
|
|
374
|
+
continue;
|
|
375
|
+
const present = new Set((device.firewall?.address_sets?.[set] ?? []).map(familyOfPrefix));
|
|
376
|
+
families = families.filter((family) => present.has(family));
|
|
377
|
+
}
|
|
378
|
+
return families;
|
|
379
|
+
}
|
|
380
|
+
function portList(ports) {
|
|
381
|
+
if (ports === undefined)
|
|
382
|
+
return [];
|
|
383
|
+
const list = Array.isArray(ports) ? ports : [ports];
|
|
384
|
+
return list.map((port) => typeof port === "number" ? String(port) : `${port.min}-${port.max}`);
|
|
385
|
+
}
|
|
386
|
+
function serviceOf(match, family) {
|
|
387
|
+
const protocol = match?.protocol;
|
|
388
|
+
const destinations = portList(match?.dst_port);
|
|
389
|
+
const sources = portList(match?.src_port);
|
|
390
|
+
if (!protocol && !destinations.length && !sources.length)
|
|
391
|
+
return { name: "ALL" };
|
|
392
|
+
if (!protocol || protocol === "tcp" || protocol === "udp") {
|
|
393
|
+
const ranges = (destinations.length ? destinations : ["1-65535"]).flatMap((destination) => sources.length ? sources.map((source) => `${destination}:${source}`) : [destination]);
|
|
394
|
+
const protocols = protocol ? [protocol] : ["tcp", "udp"];
|
|
395
|
+
const name = `${protocols.join("+")}/${destinations.join(",") || "any"}${sources.length ? ` from ${sources.join(",")}` : ""}`;
|
|
396
|
+
const block = new Block;
|
|
397
|
+
for (const each of protocols)
|
|
398
|
+
block.set(`${each}-portrange`, ranges.join(" "));
|
|
399
|
+
return { name, block };
|
|
400
|
+
}
|
|
401
|
+
if (protocol === "icmp") {
|
|
402
|
+
const kind = family === "ipv4" ? "ICMP" : "ICMP6";
|
|
403
|
+
return { name: kind.toLowerCase(), block: new Block().set("protocol", kind).set("icmptype", UNSET) };
|
|
404
|
+
}
|
|
405
|
+
const number = PROTOCOL_NUMBER[protocol];
|
|
406
|
+
return { name: `protocol/${number}`, block: new Block().set("protocol", "IP").set("protocol-number", number) };
|
|
407
|
+
}
|
|
408
|
+
function isImplied(rule) {
|
|
409
|
+
const state = rule.match?.state;
|
|
410
|
+
return rule.action === "accept" && state !== undefined && state.length > 0 && state.every((one) => one === "established" || one === "related");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
class Objects {
|
|
414
|
+
root;
|
|
415
|
+
device;
|
|
416
|
+
constructor(root, device) {
|
|
417
|
+
this.root = root;
|
|
418
|
+
this.device = device;
|
|
419
|
+
}
|
|
420
|
+
address(prefix, set, family) {
|
|
421
|
+
if (set)
|
|
422
|
+
return this.group(set, this.device.firewall?.address_sets?.[set] ?? [], family);
|
|
423
|
+
if (!prefix)
|
|
424
|
+
return "all";
|
|
425
|
+
return this.prefix(prefix, family);
|
|
426
|
+
}
|
|
427
|
+
prefix(prefix, family) {
|
|
428
|
+
const name = network2(prefix);
|
|
429
|
+
if (family === "ipv4")
|
|
430
|
+
this.root.block("firewall address").entry(name).set("subnet", subnet(name));
|
|
431
|
+
else
|
|
432
|
+
this.root.block("firewall address6").entry(name).set("ip6", name);
|
|
433
|
+
return name;
|
|
434
|
+
}
|
|
435
|
+
group(name, members, family) {
|
|
436
|
+
const names = members.filter((member) => familyOfPrefix(member) === family).map((member) => this.prefix(member, family));
|
|
437
|
+
const table = family === "ipv4" ? "firewall addrgrp" : "firewall addrgrp6";
|
|
438
|
+
this.root.block(table).entry(name).set("member", quoteAll([...new Set(names)]));
|
|
439
|
+
return name;
|
|
440
|
+
}
|
|
441
|
+
pool(address) {
|
|
442
|
+
this.root.block("firewall ippool").entry(address).set("startip", address).set("endip", address);
|
|
443
|
+
}
|
|
444
|
+
service(match, family) {
|
|
445
|
+
const { name, block } = serviceOf(match, family);
|
|
446
|
+
if (!block)
|
|
447
|
+
return name;
|
|
448
|
+
const entry = this.root.block("firewall service custom").entry(name);
|
|
449
|
+
for (const [key, value] of block.settings)
|
|
450
|
+
entry.set(key, value);
|
|
451
|
+
return name;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function firewall(root, device) {
|
|
455
|
+
const fw = device.firewall;
|
|
456
|
+
const objects = new Objects(root, device);
|
|
457
|
+
const forward = fw?.filter?.forward;
|
|
458
|
+
const policies = { ipv4: 0, ipv6: 0 };
|
|
459
|
+
for (const rule of forward?.rules ?? []) {
|
|
460
|
+
if (isImplied(rule))
|
|
461
|
+
continue;
|
|
462
|
+
for (const family of familiesOf(device, rule.match)) {
|
|
463
|
+
const table = root.block(family === "ipv4" ? "firewall policy" : "firewall policy6");
|
|
464
|
+
policy(table.entry(++policies[family]), objects, device, rule, family);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (forward?.default !== "drop") {
|
|
468
|
+
for (const family of ["ipv4", "ipv6"]) {
|
|
469
|
+
const table = root.block(family === "ipv4" ? "firewall policy" : "firewall policy6");
|
|
470
|
+
policy(table.entry(++policies[family]), objects, device, { action: "accept" }, family);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const localIns = { ipv4: 0, ipv6: 0 };
|
|
474
|
+
const localInEntry = (family) => root.block(family === "ipv4" ? "firewall local-in-policy" : "firewall local-in-policy6").entry(++localIns[family]);
|
|
475
|
+
management(localInEntry, objects, device);
|
|
476
|
+
const input = fw?.filter?.input;
|
|
477
|
+
for (const rule of input?.rules ?? []) {
|
|
478
|
+
if (isImplied(rule))
|
|
479
|
+
continue;
|
|
480
|
+
for (const family of familiesOf(device, rule.match))
|
|
481
|
+
localIn(localInEntry(family), objects, device, rule, family);
|
|
482
|
+
}
|
|
483
|
+
if (input?.default === "drop") {
|
|
484
|
+
for (const family of ["ipv4", "ipv6"])
|
|
485
|
+
localIn(localInEntry(family), objects, device, { action: "drop" }, family);
|
|
486
|
+
}
|
|
487
|
+
(fw?.nat?.source ?? []).forEach((rule, index) => sourceNat(root.block("firewall central-snat-map").entry(index + 1), objects, device, rule));
|
|
488
|
+
(fw?.nat?.destination ?? []).forEach((rule, index) => destinationNat(root.block("firewall vip").entry(`dnat-${index + 1}`), device, rule));
|
|
489
|
+
acls(root, objects, device);
|
|
490
|
+
}
|
|
491
|
+
function management(entry, objects, device) {
|
|
492
|
+
const config = device.management ?? {};
|
|
493
|
+
const guards = new Map;
|
|
494
|
+
for (const [service, listener] of Object.entries(SERVICES)) {
|
|
495
|
+
const settings = config[service];
|
|
496
|
+
const sources = settings?.allow ?? config.allow;
|
|
497
|
+
if (!settings || !sources?.length)
|
|
498
|
+
continue;
|
|
499
|
+
const name = settings.allow ? `circuit-${service}` : "circuit-management";
|
|
500
|
+
const guard = guards.get(name) ?? { sources, services: [] };
|
|
501
|
+
guard.services.push(objects.service({ protocol: listener.protocol, dst_port: settings.port ?? listener.port }, "ipv4"));
|
|
502
|
+
guards.set(name, guard);
|
|
503
|
+
}
|
|
504
|
+
for (const [name, guard] of guards) {
|
|
505
|
+
for (const family of ["ipv4", "ipv6"]) {
|
|
506
|
+
const services = quoteAll(guard.services);
|
|
507
|
+
if (guard.sources.some((source) => familyOfPrefix(source) === family)) {
|
|
508
|
+
const accept = entry(family);
|
|
509
|
+
accept.set("intf", quote("any"));
|
|
510
|
+
accept.set("srcaddr", quote(objects.group(name, guard.sources, family)));
|
|
511
|
+
accept.set("dstaddr", quote("all"));
|
|
512
|
+
accept.set("action", "accept");
|
|
513
|
+
accept.set("service", services);
|
|
514
|
+
accept.set("schedule", quote("always"));
|
|
515
|
+
accept.set("comments", quote("Management from allowed sources"));
|
|
516
|
+
}
|
|
517
|
+
const deny = entry(family);
|
|
518
|
+
deny.set("intf", quote("any"));
|
|
519
|
+
deny.set("srcaddr", quote("all"));
|
|
520
|
+
deny.set("dstaddr", quote("all"));
|
|
521
|
+
deny.set("action", "deny");
|
|
522
|
+
deny.set("service", services);
|
|
523
|
+
deny.set("schedule", quote("always"));
|
|
524
|
+
deny.set("comments", quote("Management from anywhere else"));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function policy(entry, objects, device, rule, family) {
|
|
529
|
+
const match = rule.match;
|
|
530
|
+
entry.set("srcintf", quote(match?.in_interface ? nameOf(device, match.in_interface) : "any"));
|
|
531
|
+
entry.set("dstintf", quote(match?.out_interface ? nameOf(device, match.out_interface) : "any"));
|
|
532
|
+
entry.set("srcaddr", quote(objects.address(match?.src, match?.src_set, family)));
|
|
533
|
+
entry.set("dstaddr", quote(objects.address(match?.dst, match?.dst_set, family)));
|
|
534
|
+
entry.set("action", rule.action === "accept" ? "accept" : "deny");
|
|
535
|
+
entry.set("schedule", quote("always"));
|
|
536
|
+
entry.set("service", quote(objects.service(match, family)));
|
|
537
|
+
if (rule.action !== "accept")
|
|
538
|
+
entry.set("logtraffic", "disable");
|
|
539
|
+
if (rule.action === "accept" && !device.hardware_offload && !rule.offload)
|
|
540
|
+
entry.set("auto-asic-offload", "disable");
|
|
541
|
+
if (rule.description)
|
|
542
|
+
entry.set("comments", quote(rule.description));
|
|
543
|
+
if (rule.action === "reject")
|
|
544
|
+
entry.set("send-deny-packet", "enable");
|
|
545
|
+
}
|
|
546
|
+
function localIn(entry, objects, device, rule, family) {
|
|
547
|
+
const match = rule.match;
|
|
548
|
+
entry.set("intf", quote(match?.in_interface ? nameOf(device, match.in_interface) : "any"));
|
|
549
|
+
entry.set("srcaddr", quote(objects.address(match?.src, match?.src_set, family)));
|
|
550
|
+
entry.set("dstaddr", quote(objects.address(match?.dst, match?.dst_set, family)));
|
|
551
|
+
entry.set("action", rule.action === "accept" ? "accept" : "deny");
|
|
552
|
+
entry.set("service", quote(objects.service(match, family)));
|
|
553
|
+
entry.set("schedule", quote("always"));
|
|
554
|
+
if (rule.description)
|
|
555
|
+
entry.set("comments", quote(rule.description));
|
|
556
|
+
}
|
|
557
|
+
function sourceNat(entry, objects, device, rule) {
|
|
558
|
+
const match = rule.match;
|
|
559
|
+
entry.set("orig-addr", quote(objects.address(match?.src, match?.src_set, "ipv4")));
|
|
560
|
+
entry.set("srcintf", quote(match?.in_interface ? nameOf(device, match.in_interface) : "any"));
|
|
561
|
+
entry.set("dst-addr", quote(objects.address(match?.dst, match?.dst_set, "ipv4")));
|
|
562
|
+
entry.set("dstintf", quote(match?.out_interface ? nameOf(device, match.out_interface) : "any"));
|
|
563
|
+
if (match?.protocol)
|
|
564
|
+
entry.set("protocol", PROTOCOL_NUMBER[match.protocol]);
|
|
565
|
+
const ports = portList(match?.src_port);
|
|
566
|
+
if (rule.action === "snat") {
|
|
567
|
+
objects.pool(rule.to);
|
|
568
|
+
entry.set("nat-ippool", quote(rule.to));
|
|
569
|
+
if (ports.length)
|
|
570
|
+
entry.set("orig-port", ports[0]).set("nat-port", rule.to_port ?? ports[0]);
|
|
571
|
+
} else if (ports.length) {
|
|
572
|
+
entry.set("orig-port", ports[0]);
|
|
573
|
+
}
|
|
574
|
+
if (rule.description)
|
|
575
|
+
entry.set("comments", quote(rule.description));
|
|
576
|
+
}
|
|
577
|
+
function destinationNat(entry, device, rule) {
|
|
578
|
+
if (rule.action !== "dnat")
|
|
579
|
+
return;
|
|
580
|
+
const match = rule.match;
|
|
581
|
+
if (rule.description)
|
|
582
|
+
entry.set("comment", quote(rule.description));
|
|
583
|
+
if (match?.dst)
|
|
584
|
+
entry.set("extip", host2(withLength(match.dst)));
|
|
585
|
+
entry.set("extintf", quote(match?.in_interface ? nameOf(device, match.in_interface) : "any"));
|
|
586
|
+
const sources = match?.src ? [match.src] : match?.src_set ? device.firewall?.address_sets?.[match.src_set] ?? [] : [];
|
|
587
|
+
if (sources.length)
|
|
588
|
+
entry.set("src-filter", quoteAll(sources.map(network2)));
|
|
589
|
+
const ports = portList(match?.dst_port);
|
|
590
|
+
if (ports.length)
|
|
591
|
+
entry.set("portforward", "enable");
|
|
592
|
+
entry.set("mappedip", quote(rule.to));
|
|
593
|
+
if (ports.length) {
|
|
594
|
+
if (match?.protocol === "udp")
|
|
595
|
+
entry.set("protocol", "udp");
|
|
596
|
+
entry.set("extport", ports[0]);
|
|
597
|
+
entry.set("mappedport", rule.to_port ?? ports[0]);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function acls(root, objects, device) {
|
|
601
|
+
const counters = { ipv4: 0, ipv6: 0 };
|
|
602
|
+
for (const [port, settings] of Object.entries(device.ports ?? {})) {
|
|
603
|
+
if (!settings?.acl)
|
|
604
|
+
continue;
|
|
605
|
+
for (const rule of device.acls?.[settings.acl] ?? []) {
|
|
606
|
+
if (rule.action !== "drop")
|
|
607
|
+
continue;
|
|
608
|
+
const match = rule.match;
|
|
609
|
+
for (const family of familiesOf(device, match)) {
|
|
610
|
+
const entry = root.block(family === "ipv4" ? "firewall acl" : "firewall acl6").entry(++counters[family]);
|
|
611
|
+
if (rule.description)
|
|
612
|
+
entry.set("comments", quote(rule.description));
|
|
613
|
+
entry.set("interface", quote(nameOf(device, port)));
|
|
614
|
+
entry.set("srcaddr", quote(objects.address(match?.src, undefined, family)));
|
|
615
|
+
entry.set("dstaddr", quote(objects.address(match?.dst, undefined, family)));
|
|
616
|
+
entry.set("service", quote(objects.service(match, family)));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function acceptsBeforeDrop(rules) {
|
|
622
|
+
const lastDrop = rules.map((rule) => rule.action).lastIndexOf("drop");
|
|
623
|
+
return rules.slice(0, Math.max(0, lastDrop)).some((rule) => rule.action === "accept");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/adapters/devices/fortios/routing.ts
|
|
627
|
+
var REJECT_ALL = "circuit-reject-all";
|
|
628
|
+
var FILLED = "Filled by circuit refresh";
|
|
629
|
+
var PREFIX_LIST = { ipv4: "router prefix-list", ipv6: "router prefix-list6" };
|
|
630
|
+
var BITS = { ipv4: 32, ipv6: 128 };
|
|
631
|
+
function neighborSettings(device, neighbor) {
|
|
632
|
+
const group = neighbor.group ? device.routing?.bgp?.groups?.[neighbor.group] : undefined;
|
|
633
|
+
return { ...group, ...neighbor };
|
|
634
|
+
}
|
|
635
|
+
function familiesOfNeighbor(device, neighbor) {
|
|
636
|
+
return [...neighborSettings(device, neighbor).families ?? [familyOf2(neighbor.address)]];
|
|
637
|
+
}
|
|
638
|
+
function usesBfd(device) {
|
|
639
|
+
const areas = Object.values(device.routing?.ospf?.areas ?? {});
|
|
640
|
+
const ospf = areas.some((area) => Object.values(area.interfaces).some((settings) => settings?.bfd));
|
|
641
|
+
const bgp = Object.values(device.routing?.bgp?.neighbors ?? {}).some((neighbor) => neighborSettings(device, neighbor).bfd);
|
|
642
|
+
return ospf || bgp;
|
|
643
|
+
}
|
|
644
|
+
function routing(root, device) {
|
|
645
|
+
prefixLists(root, device);
|
|
646
|
+
ospf(root, device);
|
|
647
|
+
ospf6(root, device);
|
|
648
|
+
bgp(root, device);
|
|
649
|
+
}
|
|
650
|
+
var listPrefix = (prefix, family) => family === "ipv4" ? subnet(network2(prefix)) : network2(prefix);
|
|
651
|
+
function listRule(rule, family, prefix, lengths = {}) {
|
|
652
|
+
rule.set(family === "ipv4" ? "prefix" : "prefix6", listPrefix(prefix, family));
|
|
653
|
+
rule.set("ge", lengths.ge ?? UNSET);
|
|
654
|
+
rule.set("le", lengths.le ?? UNSET);
|
|
655
|
+
}
|
|
656
|
+
function bounds(prefix, family, min = 0, max = BITS[family]) {
|
|
657
|
+
const own = split(network2(prefix)).len;
|
|
658
|
+
const low = Math.max(min, own);
|
|
659
|
+
if (max < low)
|
|
660
|
+
return;
|
|
661
|
+
if (low === own && max === own)
|
|
662
|
+
return {};
|
|
663
|
+
return { ...low > own ? { ge: low } : {}, le: max };
|
|
664
|
+
}
|
|
665
|
+
function prefixLists(root, device) {
|
|
666
|
+
for (const [name, set] of Object.entries(device.prefix_sets ?? {})) {
|
|
667
|
+
if ("source" in set) {
|
|
668
|
+
const { registry, query, family } = set.source;
|
|
669
|
+
const entry = root.block(PREFIX_LIST[family]).entry(name).set("comments", quote(`${FILLED} from ${registry} ${query}`));
|
|
670
|
+
const rule = entry.block("rule").entry(1);
|
|
671
|
+
listRule(rule, family, family === "ipv4" ? "0.0.0.0/0" : "::/0", { le: BITS[family] });
|
|
672
|
+
rule.set("action", "deny");
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const family = set.prefixes[0] ? familyOfPrefix(set.prefixes[0]) : "ipv4";
|
|
676
|
+
const entry = root.block(PREFIX_LIST[family]).entry(name);
|
|
677
|
+
set.prefixes.forEach((prefix, index) => listRule(entry.block("rule").entry(index + 1), family, prefix, bounds(prefix, family)));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function networksOf(device, name, family) {
|
|
681
|
+
return addressesOf2(settingsOf(device, name)).filter((address) => familyOfPrefix(address) === family).map(network2);
|
|
682
|
+
}
|
|
683
|
+
function ospf(root, device) {
|
|
684
|
+
const ospf = device.routing?.ospf;
|
|
685
|
+
if (!ospf || !(ospf.families ?? ["ipv4"]).includes("ipv4"))
|
|
686
|
+
return;
|
|
687
|
+
const block = root.block("router ospf");
|
|
688
|
+
if (device.routing?.router_id)
|
|
689
|
+
block.set("router-id", device.routing.router_id);
|
|
690
|
+
const passive = [];
|
|
691
|
+
let networks = 0;
|
|
692
|
+
for (const [areaId, area] of Object.entries(ospf.areas)) {
|
|
693
|
+
block.block("area").entry(areaId, true);
|
|
694
|
+
for (const [name, settings] of Object.entries(area.interfaces)) {
|
|
695
|
+
const iface = nameOf(device, name);
|
|
696
|
+
if (settings?.passive)
|
|
697
|
+
passive.push(iface);
|
|
698
|
+
if (settings?.cost !== undefined || settings?.network === "point-to-point" || settings?.bfd) {
|
|
699
|
+
const entry = block.block("ospf-interface").entry(iface);
|
|
700
|
+
entry.set("interface", quote(iface));
|
|
701
|
+
if (settings.cost !== undefined)
|
|
702
|
+
entry.set("cost", settings.cost);
|
|
703
|
+
if (settings.network === "point-to-point")
|
|
704
|
+
entry.set("network-type", "point-to-point");
|
|
705
|
+
if (settings.bfd)
|
|
706
|
+
entry.set("bfd", "enable");
|
|
707
|
+
}
|
|
708
|
+
for (const prefix of networksOf(device, name, "ipv4")) {
|
|
709
|
+
const entry = block.block("network").entry(++networks);
|
|
710
|
+
entry.set("prefix", subnet(prefix));
|
|
711
|
+
if (areaId !== "0.0.0.0")
|
|
712
|
+
entry.set("area", areaId);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (passive.length)
|
|
717
|
+
block.set("passive-interface", quoteAll(passive));
|
|
718
|
+
for (const source of ospf.redistribute ?? [])
|
|
719
|
+
block.block(`redistribute "${source}"`).set("status", "enable");
|
|
720
|
+
}
|
|
721
|
+
function ospf6(root, device) {
|
|
722
|
+
const ospf = device.routing?.ospf;
|
|
723
|
+
if (!ospf?.families?.includes("ipv6"))
|
|
724
|
+
return;
|
|
725
|
+
const block = root.block("router ospf6");
|
|
726
|
+
if (device.routing?.router_id)
|
|
727
|
+
block.set("router-id", device.routing.router_id);
|
|
728
|
+
const passive = [];
|
|
729
|
+
for (const [areaId, area] of Object.entries(ospf.areas)) {
|
|
730
|
+
block.block("area").entry(areaId, true);
|
|
731
|
+
for (const [name, settings] of Object.entries(area.interfaces)) {
|
|
732
|
+
const iface = nameOf(device, name);
|
|
733
|
+
if (settings?.passive)
|
|
734
|
+
passive.push(iface);
|
|
735
|
+
const entry = block.block("ospf6-interface").entry(iface);
|
|
736
|
+
entry.set("interface", quote(iface));
|
|
737
|
+
if (areaId !== "0.0.0.0")
|
|
738
|
+
entry.set("area-id", areaId);
|
|
739
|
+
if (settings?.cost !== undefined)
|
|
740
|
+
entry.set("cost", settings.cost);
|
|
741
|
+
if (settings?.network === "point-to-point")
|
|
742
|
+
entry.set("network-type", "point-to-point");
|
|
743
|
+
if (settings?.bfd)
|
|
744
|
+
entry.set("bfd", "enable");
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (passive.length)
|
|
748
|
+
block.set("passive-interface", quoteAll(passive));
|
|
749
|
+
for (const source of ospf.redistribute ?? [])
|
|
750
|
+
block.block(`redistribute "${source}"`).set("status", "enable");
|
|
751
|
+
}
|
|
752
|
+
function bgp(root, device) {
|
|
753
|
+
const bgp = device.routing?.bgp;
|
|
754
|
+
if (!bgp)
|
|
755
|
+
return;
|
|
756
|
+
const block = root.block("router bgp");
|
|
757
|
+
block.set("as", bgp.asn);
|
|
758
|
+
if (device.routing?.router_id)
|
|
759
|
+
block.set("router-id", device.routing.router_id);
|
|
760
|
+
const maps = new RouteMaps(root, device);
|
|
761
|
+
for (const neighbor of Object.values(bgp.neighbors ?? {})) {
|
|
762
|
+
const settings = neighborSettings(device, neighbor);
|
|
763
|
+
const families = familiesOfNeighbor(device, neighbor);
|
|
764
|
+
const entry = block.block("neighbor").entry(neighbor.address);
|
|
765
|
+
if (!families.includes("ipv4"))
|
|
766
|
+
entry.set("activate", "disable");
|
|
767
|
+
if (!families.includes("ipv6"))
|
|
768
|
+
entry.set("activate6", "disable");
|
|
769
|
+
if (settings.bfd)
|
|
770
|
+
entry.set("bfd", "enable");
|
|
771
|
+
if (settings.multihop)
|
|
772
|
+
entry.set("ebgp-enforce-multihop", "enable");
|
|
773
|
+
if (settings.passive)
|
|
774
|
+
entry.set("passive", "enable");
|
|
775
|
+
if (settings.description)
|
|
776
|
+
entry.set("description", quote(settings.description));
|
|
777
|
+
if (settings.remote_as !== undefined)
|
|
778
|
+
entry.set("remote-as", settings.remote_as);
|
|
779
|
+
for (const family of families) {
|
|
780
|
+
const suffix = family === "ipv4" ? "" : "6";
|
|
781
|
+
if (settings.max_prefixes !== undefined)
|
|
782
|
+
entry.set(`maximum-prefix${suffix}`, settings.max_prefixes);
|
|
783
|
+
entry.set(`route-map-in${suffix}`, quote(maps.of(settings.import, family)));
|
|
784
|
+
entry.set(`route-map-out${suffix}`, quote(maps.of(settings.export, family)));
|
|
785
|
+
}
|
|
786
|
+
if (settings.keepalive !== undefined)
|
|
787
|
+
entry.set("keep-alive-timer", settings.keepalive);
|
|
788
|
+
if (settings.hold_time !== undefined)
|
|
789
|
+
entry.set("holdtime-timer", settings.hold_time);
|
|
790
|
+
const source = settings.local_address ? holderOf(device, settings.local_address) : undefined;
|
|
791
|
+
if (source)
|
|
792
|
+
entry.set("update-source", quote(nameOf(device, source)));
|
|
793
|
+
if (settings.password)
|
|
794
|
+
entry.set("password", quote(marker(settings.password)));
|
|
795
|
+
}
|
|
796
|
+
const networks = { ipv4: 0, ipv6: 0 };
|
|
797
|
+
for (const prefix of bgp.networks ?? []) {
|
|
798
|
+
const family = familyOfPrefix(prefix);
|
|
799
|
+
const entry = block.block(family === "ipv4" ? "network" : "network6").entry(++networks[family]);
|
|
800
|
+
entry.set(family === "ipv4" ? "prefix" : "prefix6", listPrefix(prefix, family));
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function familyOfRule(device, rule) {
|
|
804
|
+
const match = rule.match;
|
|
805
|
+
if (match?.family)
|
|
806
|
+
return match.family;
|
|
807
|
+
if (match?.prefix)
|
|
808
|
+
return familyOfPrefix(match.prefix);
|
|
809
|
+
const set = match?.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
|
|
810
|
+
if (set && "source" in set)
|
|
811
|
+
return set.source.family;
|
|
812
|
+
if (set?.prefixes[0])
|
|
813
|
+
return familyOfPrefix(set.prefixes[0]);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
function flatten(device, policy, seen = [policy]) {
|
|
817
|
+
const flat = [];
|
|
818
|
+
for (const rule of device.policies?.[policy] ?? []) {
|
|
819
|
+
if (rule.call && !seen.includes(rule.call))
|
|
820
|
+
flat.push(...flatten(device, rule.call, [...seen, rule.call]));
|
|
821
|
+
if (rule.match || rule.set || rule.action)
|
|
822
|
+
flat.push(rule);
|
|
823
|
+
}
|
|
824
|
+
return flat;
|
|
825
|
+
}
|
|
826
|
+
var slug = (value) => value.replace(/\*/g, "any").replace(/[^A-Za-z0-9]+/g, "-");
|
|
827
|
+
var patternOf = (pattern) => `^${pattern.replace(/\*/g, "[0-9]+")}$`;
|
|
828
|
+
|
|
829
|
+
class RouteMaps {
|
|
830
|
+
root;
|
|
831
|
+
device;
|
|
832
|
+
built = new Set;
|
|
833
|
+
constructor(root, device) {
|
|
834
|
+
this.root = root;
|
|
835
|
+
this.device = device;
|
|
836
|
+
}
|
|
837
|
+
of(policy, family) {
|
|
838
|
+
if (!policy) {
|
|
839
|
+
if (!this.built.has(REJECT_ALL)) {
|
|
840
|
+
this.built.add(REJECT_ALL);
|
|
841
|
+
this.root.block("router route-map").entry(REJECT_ALL).block("rule").entry(1).set("action", "deny");
|
|
842
|
+
}
|
|
843
|
+
return REJECT_ALL;
|
|
844
|
+
}
|
|
845
|
+
const name = family === "ipv4" ? policy : `${policy}-v6`;
|
|
846
|
+
if (this.built.has(name))
|
|
847
|
+
return name;
|
|
848
|
+
this.built.add(name);
|
|
849
|
+
const map = this.root.block("router route-map").entry(name);
|
|
850
|
+
let number = 0;
|
|
851
|
+
for (const rule of flatten(this.device, policy)) {
|
|
852
|
+
const only = familyOfRule(this.device, rule);
|
|
853
|
+
if (only && only !== family)
|
|
854
|
+
continue;
|
|
855
|
+
this.rule(map.block("rule").entry(++number), rule, family, `${name}-${number}`);
|
|
856
|
+
}
|
|
857
|
+
return name;
|
|
858
|
+
}
|
|
859
|
+
rule(entry, rule, family, label) {
|
|
860
|
+
const match = rule.match ?? {};
|
|
861
|
+
const set = rule.set ?? {};
|
|
862
|
+
const address = family === "ipv4" ? "match-ip-address" : "match-ip6-address";
|
|
863
|
+
if (match.prefix) {
|
|
864
|
+
this.list(label, family, [[match.prefix, {}]]);
|
|
865
|
+
entry.set(address, quote(label));
|
|
866
|
+
} else if (match.prefix_set && match.prefix_length) {
|
|
867
|
+
const name = `${match.prefix_set}-${match.prefix_length.min ?? 0}-${match.prefix_length.max ?? BITS[family]}`;
|
|
868
|
+
const prefixes = this.prefixesOf(match.prefix_set);
|
|
869
|
+
this.list(name, family, prefixes.map((prefix) => [prefix, bounds(prefix, family, match.prefix_length?.min, match.prefix_length?.max)]));
|
|
870
|
+
entry.set(address, quote(name));
|
|
871
|
+
} else if (match.prefix_set) {
|
|
872
|
+
entry.set(address, quote(match.prefix_set));
|
|
873
|
+
} else if (match.prefix_length) {
|
|
874
|
+
const { min = 0, max = BITS[family] } = match.prefix_length;
|
|
875
|
+
const name = `circuit-length-${min}-${max}`;
|
|
876
|
+
const everything = family === "ipv4" ? "0.0.0.0/0" : "::/0";
|
|
877
|
+
this.list(name, family, [[everything, bounds(everything, family, min, max)]]);
|
|
878
|
+
entry.set(address, quote(name));
|
|
879
|
+
}
|
|
880
|
+
if (match.community)
|
|
881
|
+
entry.set("match-community", quote(this.communities(`circuit-match-${slug(match.community)}`, [match.community], false)));
|
|
882
|
+
if (set.local_pref !== undefined)
|
|
883
|
+
entry.set("set-local-preference", set.local_pref);
|
|
884
|
+
if (set.med !== undefined)
|
|
885
|
+
entry.set("set-metric", set.med);
|
|
886
|
+
if (set.prepend !== undefined)
|
|
887
|
+
entry.set("set-aspath", quote(Array(set.prepend).fill(this.device.routing.bgp.asn).join(" ")));
|
|
888
|
+
if (set.communities) {
|
|
889
|
+
entry.set("set-community", quoteAll([...set.communities, ...set.add_communities ?? []]));
|
|
890
|
+
} else {
|
|
891
|
+
if (set.add_communities)
|
|
892
|
+
entry.set("set-community", quoteAll(set.add_communities)).set("set-community-additive", "enable");
|
|
893
|
+
if (set.remove_communities) {
|
|
894
|
+
const name = `circuit-remove-${slug(set.remove_communities.join(" "))}`;
|
|
895
|
+
entry.set("set-community-delete", quote(this.communities(name, set.remove_communities, true)));
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (set.next_hop && familyOf2(set.next_hop) === family)
|
|
899
|
+
entry.set(family === "ipv4" ? "set-ip-nexthop" : "set-ip6-nexthop", set.next_hop);
|
|
900
|
+
if (rule.action === "reject")
|
|
901
|
+
entry.set("action", "deny");
|
|
902
|
+
}
|
|
903
|
+
prefixesOf(set) {
|
|
904
|
+
const prefixes = this.device.prefix_sets?.[set];
|
|
905
|
+
return prefixes && "prefixes" in prefixes ? [...prefixes.prefixes] : [];
|
|
906
|
+
}
|
|
907
|
+
list(name, family, rules) {
|
|
908
|
+
const rulesBlock = this.root.block(PREFIX_LIST[family]).entry(name).block("rule");
|
|
909
|
+
let number = 0;
|
|
910
|
+
for (const [prefix, lengths] of rules)
|
|
911
|
+
if (lengths)
|
|
912
|
+
listRule(rulesBlock.entry(++number), family, prefix, lengths);
|
|
913
|
+
}
|
|
914
|
+
communities(name, values, patterns) {
|
|
915
|
+
const list = this.root.block("router community-list").entry(name);
|
|
916
|
+
if (patterns)
|
|
917
|
+
list.set("type", "expanded");
|
|
918
|
+
values.forEach((value, index) => {
|
|
919
|
+
const rule = list.block("rule").entry(index + 1).set("action", "permit");
|
|
920
|
+
if (patterns)
|
|
921
|
+
rule.set("regexp", quote(patternOf(value)));
|
|
922
|
+
else
|
|
923
|
+
rule.set("match", quote(value));
|
|
924
|
+
});
|
|
925
|
+
return name;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function routingGaps(device) {
|
|
929
|
+
const found = [];
|
|
930
|
+
const routing = device.routing;
|
|
931
|
+
if (routing?.rpki)
|
|
932
|
+
found.push("routing.rpki, the platform has no route origin validation");
|
|
933
|
+
if (routing?.router_id && !routing.ospf && !routing.bgp)
|
|
934
|
+
found.push("routing.router_id without OSPF or BGP, which each hold it");
|
|
935
|
+
const ospf = routing?.ospf;
|
|
936
|
+
if (ospf?.families?.includes("ipv6") && !routing?.router_id)
|
|
937
|
+
found.push("routing.ospf for IPv6 without routing.router_id");
|
|
938
|
+
for (const area of Object.values(ospf?.areas ?? {})) {
|
|
939
|
+
for (const name of Object.keys(area.interfaces)) {
|
|
940
|
+
if (settingsOf(device, name)?.vrf)
|
|
941
|
+
found.push(`routing.ospf on ${name}, which routes in a VRF: OSPF runs in the default table`);
|
|
942
|
+
const ipv4 = addressesOf2(settingsOf(device, name)).some((address) => familyOf2(host2(address)) === "ipv4");
|
|
943
|
+
if ((ospf?.families ?? ["ipv4"]).includes("ipv4") && !ipv4)
|
|
944
|
+
found.push(`routing.ospf on ${name}, which has no IPv4 address for OSPF to run on`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
for (const [name, neighbor] of Object.entries(routing?.bgp?.neighbors ?? {})) {
|
|
948
|
+
const where = `routing.bgp.neighbors.${name}`;
|
|
949
|
+
const settings = neighborSettings(device, neighbor);
|
|
950
|
+
if (settings.role)
|
|
951
|
+
found.push(`${where}.role, the platform has no BGP roles`);
|
|
952
|
+
if (settings.local_address && !holderOf(device, settings.local_address)) {
|
|
953
|
+
found.push(`${where}.local_address, no interface holds ${settings.local_address}`);
|
|
954
|
+
}
|
|
955
|
+
if (familyOf2(neighbor.address) === "ipv6" && familiesOfNeighbor(device, neighbor).includes("ipv4")) {
|
|
956
|
+
found.push(`${where}.families ipv4 over an IPv6 session`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
for (const [name, rules] of Object.entries(device.policies ?? {})) {
|
|
960
|
+
rules.forEach((rule, index) => found.push(...ruleGaps(device, `policies.${name}[${index}]`, rule)));
|
|
961
|
+
}
|
|
962
|
+
return found;
|
|
963
|
+
}
|
|
964
|
+
function ruleGaps(device, where, rule) {
|
|
965
|
+
const found = [];
|
|
966
|
+
const match = rule.match ?? {};
|
|
967
|
+
const set = rule.set ?? {};
|
|
968
|
+
if ((rule.match || rule.set) && !rule.action)
|
|
969
|
+
found.push(`${where}, a rule without action: the platform stops at the first rule that matches`);
|
|
970
|
+
for (const field of ["as_path_length", "rpki", "large_community"]) {
|
|
971
|
+
if (match[field] !== undefined)
|
|
972
|
+
found.push(`${where}.match.${field}`);
|
|
973
|
+
}
|
|
974
|
+
for (const field of [
|
|
975
|
+
"large_communities",
|
|
976
|
+
"add_large_communities",
|
|
977
|
+
"remove_large_communities",
|
|
978
|
+
"preferred_source",
|
|
979
|
+
"blackhole"
|
|
980
|
+
]) {
|
|
981
|
+
if (set[field] !== undefined)
|
|
982
|
+
found.push(`${where}.set.${field}`);
|
|
983
|
+
}
|
|
984
|
+
if (match.prefix && match.prefix_length)
|
|
985
|
+
found.push(`${where}.match.prefix with prefix_length`);
|
|
986
|
+
if (match.prefix && match.prefix_set)
|
|
987
|
+
found.push(`${where}.match.prefix with prefix_set`);
|
|
988
|
+
const prefixSet = match.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
|
|
989
|
+
if (prefixSet && "source" in prefixSet && match.prefix_length) {
|
|
990
|
+
found.push(`${where}.match.prefix_length with a prefix set a registry fills`);
|
|
991
|
+
}
|
|
992
|
+
return found;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/adapters/devices/fortios/plan.ts
|
|
996
|
+
var FACTORY_ADDRESSES = [
|
|
997
|
+
"none",
|
|
998
|
+
"login.microsoftonline.com",
|
|
999
|
+
"login.microsoft.com",
|
|
1000
|
+
"login.windows.net",
|
|
1001
|
+
"gmail.com",
|
|
1002
|
+
"wildcard.google.com",
|
|
1003
|
+
"wildcard.dropbox.com",
|
|
1004
|
+
"all",
|
|
1005
|
+
"FIREWALL_AUTH_PORTAL_ADDRESS",
|
|
1006
|
+
"FABRIC_DEVICE",
|
|
1007
|
+
"SSLVPN_TUNNEL_ADDR1"
|
|
1008
|
+
];
|
|
1009
|
+
var FACTORY_SERVICES = [
|
|
1010
|
+
"ALL",
|
|
1011
|
+
"ALL_TCP",
|
|
1012
|
+
"ALL_UDP",
|
|
1013
|
+
"ALL_ICMP",
|
|
1014
|
+
"ALL_ICMP6",
|
|
1015
|
+
"GRE",
|
|
1016
|
+
"AH",
|
|
1017
|
+
"ESP",
|
|
1018
|
+
"AOL",
|
|
1019
|
+
"BGP",
|
|
1020
|
+
"DHCP",
|
|
1021
|
+
"DNS",
|
|
1022
|
+
"FINGER",
|
|
1023
|
+
"FTP",
|
|
1024
|
+
"FTP_GET",
|
|
1025
|
+
"FTP_PUT",
|
|
1026
|
+
"GOPHER",
|
|
1027
|
+
"H323",
|
|
1028
|
+
"HTTP",
|
|
1029
|
+
"HTTPS",
|
|
1030
|
+
"IKE",
|
|
1031
|
+
"IMAP",
|
|
1032
|
+
"IMAPS",
|
|
1033
|
+
"Internet-Locator-Service",
|
|
1034
|
+
"IRC",
|
|
1035
|
+
"L2TP",
|
|
1036
|
+
"LDAP",
|
|
1037
|
+
"NetMeeting",
|
|
1038
|
+
"NFS",
|
|
1039
|
+
"NNTP",
|
|
1040
|
+
"NTP",
|
|
1041
|
+
"OSPF",
|
|
1042
|
+
"PC-Anywhere",
|
|
1043
|
+
"PING",
|
|
1044
|
+
"TIMESTAMP",
|
|
1045
|
+
"INFO_REQUEST",
|
|
1046
|
+
"INFO_ADDRESS",
|
|
1047
|
+
"ONC-RPC",
|
|
1048
|
+
"DCE-RPC",
|
|
1049
|
+
"POP3",
|
|
1050
|
+
"POP3S",
|
|
1051
|
+
"PPTP",
|
|
1052
|
+
"QUAKE",
|
|
1053
|
+
"RAUDIO",
|
|
1054
|
+
"REXEC",
|
|
1055
|
+
"RIP",
|
|
1056
|
+
"RLOGIN",
|
|
1057
|
+
"RSH",
|
|
1058
|
+
"SCCP",
|
|
1059
|
+
"SIP",
|
|
1060
|
+
"SIP-MSNmessenger",
|
|
1061
|
+
"SAMBA",
|
|
1062
|
+
"SMTP",
|
|
1063
|
+
"SMTPS",
|
|
1064
|
+
"SNMP",
|
|
1065
|
+
"SSH",
|
|
1066
|
+
"SYSLOG",
|
|
1067
|
+
"TALK",
|
|
1068
|
+
"TELNET",
|
|
1069
|
+
"TFTP",
|
|
1070
|
+
"MGCP",
|
|
1071
|
+
"UUCP",
|
|
1072
|
+
"VDOLIVE",
|
|
1073
|
+
"WAIS",
|
|
1074
|
+
"WINFRAME",
|
|
1075
|
+
"X-WINDOWS",
|
|
1076
|
+
"PING6",
|
|
1077
|
+
"MS-SQL",
|
|
1078
|
+
"MYSQL",
|
|
1079
|
+
"RDP",
|
|
1080
|
+
"VNC",
|
|
1081
|
+
"DHCP6",
|
|
1082
|
+
"SQUID",
|
|
1083
|
+
"SOCKS",
|
|
1084
|
+
"WINS",
|
|
1085
|
+
"RADIUS",
|
|
1086
|
+
"RADIUS-OLD",
|
|
1087
|
+
"CVSPSERVER",
|
|
1088
|
+
"AFS3",
|
|
1089
|
+
"TRACEROUTE",
|
|
1090
|
+
"RTSP",
|
|
1091
|
+
"MMS",
|
|
1092
|
+
"KERBEROS",
|
|
1093
|
+
"LDAP_UDP",
|
|
1094
|
+
"SMB",
|
|
1095
|
+
"NONE",
|
|
1096
|
+
"webproxy"
|
|
1097
|
+
];
|
|
1098
|
+
var FACTORY_CERTIFICATES = [
|
|
1099
|
+
"Fortinet_CA_SSL",
|
|
1100
|
+
"Fortinet_CA_Untrusted",
|
|
1101
|
+
"Fortinet_SSL",
|
|
1102
|
+
"Fortinet_SSL_RSA1024",
|
|
1103
|
+
"Fortinet_SSL_RSA2048",
|
|
1104
|
+
"Fortinet_SSL_RSA4096",
|
|
1105
|
+
"Fortinet_SSL_DSA1024",
|
|
1106
|
+
"Fortinet_SSL_DSA2048",
|
|
1107
|
+
"Fortinet_SSL_ECDSA256",
|
|
1108
|
+
"Fortinet_SSL_ECDSA384",
|
|
1109
|
+
"Fortinet_SSL_ECDSA521",
|
|
1110
|
+
"Fortinet_SSL_ED25519",
|
|
1111
|
+
"Fortinet_SSL_ED448"
|
|
1112
|
+
];
|
|
1113
|
+
var ORDERED = { ordered: true };
|
|
1114
|
+
var TABLES = {
|
|
1115
|
+
"system interface": {
|
|
1116
|
+
factory: ["modem", "ssl.root"],
|
|
1117
|
+
created: (entry) => {
|
|
1118
|
+
if (entry.settings.get("type") === "aggregate")
|
|
1119
|
+
return ["device-identification", "lldp-transmission"];
|
|
1120
|
+
if (entry.settings.has("interface") && !entry.settings.has("type"))
|
|
1121
|
+
return ["device-identification", "role"];
|
|
1122
|
+
return [];
|
|
1123
|
+
},
|
|
1124
|
+
fixed: ["type", "interface"]
|
|
1125
|
+
},
|
|
1126
|
+
"system accprofile": { factory: ["prof_admin"] },
|
|
1127
|
+
"vpn certificate local": { factory: FACTORY_CERTIFICATES },
|
|
1128
|
+
"system session-helper": { byContent: true },
|
|
1129
|
+
"firewall address": { factory: FACTORY_ADDRESSES },
|
|
1130
|
+
"firewall address6": { factory: ["SSLVPN_TUNNEL_IPv6_ADDR1", "all", "none"] },
|
|
1131
|
+
"firewall addrgrp": { factory: ["G Suite", "Microsoft Office 365"] },
|
|
1132
|
+
"firewall service custom": { factory: FACTORY_SERVICES },
|
|
1133
|
+
"firewall policy": ORDERED,
|
|
1134
|
+
"firewall policy6": ORDERED,
|
|
1135
|
+
"firewall local-in-policy": ORDERED,
|
|
1136
|
+
"firewall local-in-policy6": ORDERED,
|
|
1137
|
+
"firewall central-snat-map": ORDERED
|
|
1138
|
+
};
|
|
1139
|
+
var IGNORED = new Set(["uuid", "snmp-index"]);
|
|
1140
|
+
var IGNORED_IN = { "system global": ["cfg-save", "cfg-revert-timeout", "alias"] };
|
|
1141
|
+
var KEPT = { "vpn certificate local": ["certificate", "private-key"], "vpn certificate ca": ["ca"] };
|
|
1142
|
+
var DENY = { action: "deny" };
|
|
1143
|
+
var DEFAULTS = {
|
|
1144
|
+
"system global": {
|
|
1145
|
+
"admin-ssh-port": "22",
|
|
1146
|
+
"admin-sport": "443",
|
|
1147
|
+
"admin-port": "80",
|
|
1148
|
+
"admin-telnet-port": "23",
|
|
1149
|
+
"admin-ssh-grace-time": "120"
|
|
1150
|
+
},
|
|
1151
|
+
"system interface": { mtu: "1500" },
|
|
1152
|
+
"system snmp community": { "query-v2c-port": "161" },
|
|
1153
|
+
"system snmp user": { "query-port": "161" },
|
|
1154
|
+
"system dhcp server": { "dns-service": "specify" },
|
|
1155
|
+
"firewall policy": DENY,
|
|
1156
|
+
"firewall policy6": DENY,
|
|
1157
|
+
"firewall local-in-policy": DENY,
|
|
1158
|
+
"firewall local-in-policy6": DENY
|
|
1159
|
+
};
|
|
1160
|
+
var SECRET_KEYS = new Set(["private-key"]);
|
|
1161
|
+
var CERTIFICATES = new Set(["certificate", "ca"]);
|
|
1162
|
+
var FIRST = new Set(["ntpsync"]);
|
|
1163
|
+
var LISTS = new Set([
|
|
1164
|
+
"allowaccess",
|
|
1165
|
+
"ip6-allowaccess",
|
|
1166
|
+
"member",
|
|
1167
|
+
"srcintf",
|
|
1168
|
+
"dstintf",
|
|
1169
|
+
"srcaddr",
|
|
1170
|
+
"dstaddr",
|
|
1171
|
+
"service",
|
|
1172
|
+
"src-filter",
|
|
1173
|
+
"dhcp-relay-ip"
|
|
1174
|
+
]);
|
|
1175
|
+
var isSecret2 = (value) => /<secret:|<redacted>/.test(value) || value.startsWith("ENC ");
|
|
1176
|
+
function canonical(key, value) {
|
|
1177
|
+
if (value === UNSET)
|
|
1178
|
+
return UNSET;
|
|
1179
|
+
if (isSecret2(value) || SECRET_KEYS.has(key))
|
|
1180
|
+
return "<secret>";
|
|
1181
|
+
if (CERTIFICATES.has(key))
|
|
1182
|
+
return bare(value).replace(/\s+/g, "");
|
|
1183
|
+
const words = wordsOf(value).map(bare);
|
|
1184
|
+
return (LISTS.has(key) ? words.sort() : words).join(" ");
|
|
1185
|
+
}
|
|
1186
|
+
function changesFixed(table, before, after) {
|
|
1187
|
+
const value = (block, key) => canonical(key, block.settings.get(key) ?? "");
|
|
1188
|
+
return (table?.fixed ?? []).some((key) => value(before, key) !== value(after, key));
|
|
1189
|
+
}
|
|
1190
|
+
function isFactory(table, entry) {
|
|
1191
|
+
return table?.factory?.includes(bare(entry.id)) ?? false;
|
|
1192
|
+
}
|
|
1193
|
+
function clean(text) {
|
|
1194
|
+
const root = parse(text);
|
|
1195
|
+
for (const [section, block] of root.blocks) {
|
|
1196
|
+
const table = TABLES[section];
|
|
1197
|
+
for (const [key, entry] of block.entries)
|
|
1198
|
+
if (isFactory(table, entry))
|
|
1199
|
+
block.entries.delete(key);
|
|
1200
|
+
for (const key of IGNORED_IN[section] ?? [])
|
|
1201
|
+
block.settings.delete(key);
|
|
1202
|
+
dropIgnored(block);
|
|
1203
|
+
const kept = KEPT[section];
|
|
1204
|
+
if (!kept)
|
|
1205
|
+
continue;
|
|
1206
|
+
for (const { block: entry } of block.entries.values()) {
|
|
1207
|
+
for (const key of entry.settings.keys())
|
|
1208
|
+
if (!kept.includes(key))
|
|
1209
|
+
entry.settings.delete(key);
|
|
1210
|
+
entry.blocks.clear();
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return print(root);
|
|
1214
|
+
}
|
|
1215
|
+
function dropIgnored(block) {
|
|
1216
|
+
for (const key of block.settings.keys())
|
|
1217
|
+
if (IGNORED.has(key))
|
|
1218
|
+
block.settings.delete(key);
|
|
1219
|
+
for (const child of block.blocks.values())
|
|
1220
|
+
dropIgnored(child);
|
|
1221
|
+
for (const { block: entry } of block.entries.values())
|
|
1222
|
+
dropIgnored(entry);
|
|
1223
|
+
}
|
|
1224
|
+
var labelOf = (path) => path.length ? `${path.join(" ")}: ` : "";
|
|
1225
|
+
function shown(sign, path, block) {
|
|
1226
|
+
const label = labelOf(path);
|
|
1227
|
+
const lines = linesOf(block).map((line) => `${sign} ${label}${line.trim()}`);
|
|
1228
|
+
return lines.length ? lines : [`${sign} ${path.join(" ")}`];
|
|
1229
|
+
}
|
|
1230
|
+
function creation(entry, table, id = entry.id) {
|
|
1231
|
+
const unset = (table?.created?.(entry.block) ?? []).filter((key) => !entry.block.settings.has(key)).map((key) => `unset ${key}`);
|
|
1232
|
+
return [`edit ${id}`, ...linesOf(entry.block).map((line) => line.trim()), ...unset, "next"];
|
|
1233
|
+
}
|
|
1234
|
+
function compare(path, want, have, table, top, context) {
|
|
1235
|
+
const send = [];
|
|
1236
|
+
const show = [];
|
|
1237
|
+
const removals = [];
|
|
1238
|
+
const removed = [];
|
|
1239
|
+
const label = labelOf(path);
|
|
1240
|
+
const nested = (header, wanted, held) => {
|
|
1241
|
+
const diff = compare([...path, header], wanted, held, undefined, false, context);
|
|
1242
|
+
if (!diff.send.length)
|
|
1243
|
+
return;
|
|
1244
|
+
send.push(`config ${header}`, ...diff.send, "end");
|
|
1245
|
+
show.push(...diff.show);
|
|
1246
|
+
};
|
|
1247
|
+
const unset = (key, old) => {
|
|
1248
|
+
send.push(`unset ${key}`);
|
|
1249
|
+
show.push(`- ${label}set ${key} ${old}`);
|
|
1250
|
+
};
|
|
1251
|
+
const gone = [...have.settings].filter(([key]) => !want.settings.has(key) && !IGNORED.has(key));
|
|
1252
|
+
for (const [key, old] of gone)
|
|
1253
|
+
if (FIRST.has(key))
|
|
1254
|
+
unset(key, old);
|
|
1255
|
+
for (const [header, child] of [...have.blocks].reverse())
|
|
1256
|
+
if (!want.blocks.has(header))
|
|
1257
|
+
nested(header, new Block, child);
|
|
1258
|
+
for (const [key, old] of gone.reverse())
|
|
1259
|
+
if (!FIRST.has(key))
|
|
1260
|
+
unset(key, old);
|
|
1261
|
+
for (const [key, value] of want.settings) {
|
|
1262
|
+
const old = have.settings.get(key);
|
|
1263
|
+
const current = old ?? DEFAULTS[context.section]?.[key];
|
|
1264
|
+
const same = current !== undefined && canonical(key, current) === canonical(key, value);
|
|
1265
|
+
if (same && !(context.secrets && value.includes("<secret:")))
|
|
1266
|
+
continue;
|
|
1267
|
+
send.push(value === UNSET ? `unset ${key}` : `set ${key} ${value}`);
|
|
1268
|
+
if (old !== undefined)
|
|
1269
|
+
show.push(`- ${label}set ${key} ${old}`);
|
|
1270
|
+
show.push(`+ ${label}set ${key} ${value}`);
|
|
1271
|
+
}
|
|
1272
|
+
for (const [header, child] of want.blocks)
|
|
1273
|
+
nested(header, child, have.blocks.get(header) ?? new Block);
|
|
1274
|
+
const entries = table?.byContent ? byContent(path, want, have) : byId(path, want, have, table, context);
|
|
1275
|
+
send.push(...entries.send);
|
|
1276
|
+
show.push(...entries.show);
|
|
1277
|
+
if (top) {
|
|
1278
|
+
removals.push(...entries.removals);
|
|
1279
|
+
removed.push(...entries.removed);
|
|
1280
|
+
} else {
|
|
1281
|
+
send.push(...entries.removals);
|
|
1282
|
+
show.push(...entries.removed);
|
|
1283
|
+
}
|
|
1284
|
+
return { send, show, removals, removed };
|
|
1285
|
+
}
|
|
1286
|
+
function byId(path, want, have, table, context) {
|
|
1287
|
+
const send = [];
|
|
1288
|
+
const show = [];
|
|
1289
|
+
const removals = [];
|
|
1290
|
+
const removed = [];
|
|
1291
|
+
for (const [key, entry] of want.entries) {
|
|
1292
|
+
const old = have.entries.get(key);
|
|
1293
|
+
const where = [...path, bare(entry.id)];
|
|
1294
|
+
const replaced = old && changesFixed(table, old.block, entry.block);
|
|
1295
|
+
if (!old || replaced) {
|
|
1296
|
+
if (old) {
|
|
1297
|
+
send.push(`delete ${old.id}`);
|
|
1298
|
+
show.push(...shown("-", where, old.block));
|
|
1299
|
+
}
|
|
1300
|
+
send.push(...creation(entry, table));
|
|
1301
|
+
show.push(...shown("+", where, entry.block));
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
const diff = compare(where, entry.block, old.block, undefined, false, context);
|
|
1305
|
+
if (!diff.send.length)
|
|
1306
|
+
continue;
|
|
1307
|
+
send.push(`edit ${entry.id}`, ...diff.send, "next");
|
|
1308
|
+
show.push(...diff.show);
|
|
1309
|
+
}
|
|
1310
|
+
const gone = [...have.entries].filter(([key, entry]) => !want.entries.has(key) && !isFactory(table, entry));
|
|
1311
|
+
gone.sort(([, a], [, b]) => Number(b.block.settings.has("interface")) - Number(a.block.settings.has("interface")));
|
|
1312
|
+
for (const [, entry] of gone) {
|
|
1313
|
+
removals.push(`delete ${entry.id}`);
|
|
1314
|
+
removed.push(...shown("-", [...path, bare(entry.id)], entry.block));
|
|
1315
|
+
}
|
|
1316
|
+
if (table?.ordered) {
|
|
1317
|
+
const moved = moves(want, have);
|
|
1318
|
+
send.push(...moved);
|
|
1319
|
+
show.push(...moved.map((line) => `+ ${labelOf(path)}${line}`));
|
|
1320
|
+
}
|
|
1321
|
+
return { send, show, removals, removed };
|
|
1322
|
+
}
|
|
1323
|
+
function moves(want, have) {
|
|
1324
|
+
const wanted = [...want.entries.keys()];
|
|
1325
|
+
const order = [...have.entries.keys()].filter((key) => want.entries.has(key));
|
|
1326
|
+
order.push(...wanted.filter((key) => !have.entries.has(key)));
|
|
1327
|
+
const send = [];
|
|
1328
|
+
const idOf = (key) => want.entries.get(key).id;
|
|
1329
|
+
for (let index = 0;index < wanted.length; index++) {
|
|
1330
|
+
if (order[index] === wanted[index])
|
|
1331
|
+
continue;
|
|
1332
|
+
const key = wanted[index];
|
|
1333
|
+
send.push(index === 0 ? `move ${idOf(key)} before ${idOf(order[0])}` : `move ${idOf(key)} after ${idOf(wanted[index - 1])}`);
|
|
1334
|
+
order.splice(order.indexOf(key), 1);
|
|
1335
|
+
order.splice(index, 0, key);
|
|
1336
|
+
}
|
|
1337
|
+
return send;
|
|
1338
|
+
}
|
|
1339
|
+
function signature(block) {
|
|
1340
|
+
return [...block.settings].map(([key, value]) => `${key} ${canonical(key, value)}`).sort().join(`
|
|
1341
|
+
`);
|
|
1342
|
+
}
|
|
1343
|
+
function byContent(path, want, have) {
|
|
1344
|
+
const send = [];
|
|
1345
|
+
const show = [];
|
|
1346
|
+
const removals = [];
|
|
1347
|
+
const removed = [];
|
|
1348
|
+
const unmatched = [...have.entries.values()];
|
|
1349
|
+
const added = [];
|
|
1350
|
+
for (const entry of want.entries.values()) {
|
|
1351
|
+
const index = unmatched.findIndex((old) => signature(old.block) === signature(entry.block));
|
|
1352
|
+
if (index >= 0)
|
|
1353
|
+
unmatched.splice(index, 1);
|
|
1354
|
+
else
|
|
1355
|
+
added.push(entry);
|
|
1356
|
+
}
|
|
1357
|
+
let next = Math.max(0, ...[...have.entries.keys()].map(Number).filter(Number.isInteger)) + 1;
|
|
1358
|
+
for (const entry of added) {
|
|
1359
|
+
const id = String(next++);
|
|
1360
|
+
send.push(...creation(entry, undefined, id));
|
|
1361
|
+
show.push(...shown("+", [...path, id], entry.block));
|
|
1362
|
+
}
|
|
1363
|
+
for (const entry of unmatched) {
|
|
1364
|
+
removals.push(`delete ${entry.id}`);
|
|
1365
|
+
removed.push(...shown("-", [...path, bare(entry.id)], entry.block));
|
|
1366
|
+
}
|
|
1367
|
+
return { send, show, removals, removed };
|
|
1368
|
+
}
|
|
1369
|
+
var NAMES_INTERFACE = new Set(["device", "interface", "srcintf", "dstintf", "intf", "extintf", "update-source", "passive-interface"]);
|
|
1370
|
+
function interfacesNamed(block) {
|
|
1371
|
+
return [...block.settings].filter(([key]) => NAMES_INTERFACE.has(key)).flatMap(([, value]) => wordsOf(value).map(bare));
|
|
1372
|
+
}
|
|
1373
|
+
var KEYED_BY_INTERFACE = new Set(["system dns-server"]);
|
|
1374
|
+
var INTERFACE_TABLES = new Set(["system interface", "system gre-tunnel", "system vxlan"]);
|
|
1375
|
+
function released(want, have) {
|
|
1376
|
+
const interfaces = have.blocks.get("system interface") ?? new Block;
|
|
1377
|
+
const wanted = want.blocks.get("system interface") ?? new Block;
|
|
1378
|
+
const members = (block) => wordsOf(block?.settings.get("member") ?? "").map(bare);
|
|
1379
|
+
const freed = new Set;
|
|
1380
|
+
const gone = new Set;
|
|
1381
|
+
const trims = [];
|
|
1382
|
+
const deferred = new Set;
|
|
1383
|
+
const changesSpeed = (port) => {
|
|
1384
|
+
const speed = (block) => canonical("speed", block?.settings.get("speed") ?? "");
|
|
1385
|
+
return speed(interfaces.entries.get(port)?.block) !== speed(wanted.entries.get(port)?.block);
|
|
1386
|
+
};
|
|
1387
|
+
for (const [key, entry] of wanted.entries) {
|
|
1388
|
+
const before = interfaces.entries.get(key)?.block;
|
|
1389
|
+
for (const member of members(entry.block))
|
|
1390
|
+
if (!members(before).includes(member))
|
|
1391
|
+
freed.add(member);
|
|
1392
|
+
if (before && changesFixed(TABLES["system interface"], before, entry.block))
|
|
1393
|
+
freed.add(key);
|
|
1394
|
+
}
|
|
1395
|
+
for (const [key, entry] of interfaces.entries) {
|
|
1396
|
+
if (entry.block.settings.get("type") !== "aggregate")
|
|
1397
|
+
continue;
|
|
1398
|
+
const kept = members(entry.block).filter((member) => members(wanted.entries.get(key)?.block).includes(member) && !changesSpeed(member));
|
|
1399
|
+
if (kept.length === members(entry.block).length)
|
|
1400
|
+
continue;
|
|
1401
|
+
if (kept.length && wanted.entries.has(key)) {
|
|
1402
|
+
trims.push({ id: entry.id, members: kept });
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
if (!wanted.entries.has(key) && !members(entry.block).some((member) => freed.has(member))) {
|
|
1406
|
+
for (const member of members(entry.block))
|
|
1407
|
+
deferred.add(member);
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
freed.add(key);
|
|
1411
|
+
gone.add(key);
|
|
1412
|
+
}
|
|
1413
|
+
let grew = true;
|
|
1414
|
+
while (grew) {
|
|
1415
|
+
grew = false;
|
|
1416
|
+
for (const [section, block] of have.blocks) {
|
|
1417
|
+
if (!INTERFACE_TABLES.has(section))
|
|
1418
|
+
continue;
|
|
1419
|
+
for (const [key, entry] of block.entries) {
|
|
1420
|
+
if (freed.has(key) || !interfacesNamed(entry.block).some((name) => freed.has(name)))
|
|
1421
|
+
continue;
|
|
1422
|
+
freed.add(key);
|
|
1423
|
+
grew = true;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
return { freed, gone, trims, deferred };
|
|
1428
|
+
}
|
|
1429
|
+
var HELD_BY = { interface: "server-mode" };
|
|
1430
|
+
function release(block, path, freed, gone, isEntry) {
|
|
1431
|
+
const send = [];
|
|
1432
|
+
const show = [];
|
|
1433
|
+
const label = labelOf(path);
|
|
1434
|
+
if (!isEntry) {
|
|
1435
|
+
for (const [key, value] of [...block.settings]) {
|
|
1436
|
+
if (!NAMES_INTERFACE.has(key) || !wordsOf(value).some((word) => freed.has(bare(word))))
|
|
1437
|
+
continue;
|
|
1438
|
+
const holder = HELD_BY[key];
|
|
1439
|
+
if (holder && block.settings.has(holder)) {
|
|
1440
|
+
send.push(`unset ${holder}`);
|
|
1441
|
+
show.push(`- ${label}set ${holder} ${block.settings.get(holder)}`, `- ${label}set ${key} ${value}`);
|
|
1442
|
+
block.settings.delete(holder);
|
|
1443
|
+
block.settings.delete(key);
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
send.push(`unset ${key}`);
|
|
1447
|
+
show.push(`- ${label}set ${key} ${value}`);
|
|
1448
|
+
block.settings.delete(key);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
for (const [header, child] of block.blocks) {
|
|
1452
|
+
const inner = release(child, [...path, header], freed, gone, false);
|
|
1453
|
+
if (inner.send.length)
|
|
1454
|
+
send.push(`config ${header}`, ...inner.send, "end");
|
|
1455
|
+
show.push(...inner.show);
|
|
1456
|
+
}
|
|
1457
|
+
const entries = [...block.entries].sort(([, a], [, b]) => Number(b.block.settings.has("interface")) - Number(a.block.settings.has("interface")));
|
|
1458
|
+
for (const [key, entry] of entries) {
|
|
1459
|
+
const where = [...path, bare(entry.id)];
|
|
1460
|
+
const named = interfacesNamed(entry.block).some((name) => freed.has(name));
|
|
1461
|
+
if (named || gone.has(key)) {
|
|
1462
|
+
send.push(`delete ${entry.id}`);
|
|
1463
|
+
show.push(...shown("-", where, entry.block));
|
|
1464
|
+
block.entries.delete(key);
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
const inner = release(entry.block, where, freed, gone, true);
|
|
1468
|
+
if (inner.send.length)
|
|
1469
|
+
send.push(`edit ${entry.id}`, ...inner.send, "next");
|
|
1470
|
+
show.push(...inner.show);
|
|
1471
|
+
}
|
|
1472
|
+
return { send, show };
|
|
1473
|
+
}
|
|
1474
|
+
function movedAddresses(want, have) {
|
|
1475
|
+
const wanted = want.blocks.get("system interface");
|
|
1476
|
+
const held = have.blocks.get("system interface");
|
|
1477
|
+
if (!wanted || !held)
|
|
1478
|
+
return;
|
|
1479
|
+
const send = [];
|
|
1480
|
+
const show = [];
|
|
1481
|
+
const same = (key, a, b) => canonical(key, a ?? "") === canonical(key, b ?? "");
|
|
1482
|
+
for (const [key, entry] of held.entries) {
|
|
1483
|
+
const after = wanted.entries.get(key)?.block;
|
|
1484
|
+
const before = entry.block;
|
|
1485
|
+
const lines = [];
|
|
1486
|
+
const label = labelOf([bare(entry.id)]);
|
|
1487
|
+
const primaryMoves = before.settings.has("ip") && !same("ip", before.settings.get("ip"), after?.settings.get("ip"));
|
|
1488
|
+
const secondaries = before.blocks.get("secondaryip");
|
|
1489
|
+
const keptSecondaries = after?.blocks.get("secondaryip");
|
|
1490
|
+
const goneSecondaries = [...secondaries?.entries ?? []].filter(([id, secondary]) => primaryMoves || !same("ip", secondary.block.settings.get("ip"), keptSecondaries?.entries.get(id)?.block.settings.get("ip")));
|
|
1491
|
+
if (goneSecondaries.length) {
|
|
1492
|
+
lines.push("config secondaryip", ...goneSecondaries.map(([, secondary]) => `delete ${secondary.id}`), "end");
|
|
1493
|
+
for (const [id, secondary] of goneSecondaries) {
|
|
1494
|
+
show.push(`- ${label}secondaryip ${bare(secondary.id)}: set ip ${secondary.block.settings.get("ip")}`);
|
|
1495
|
+
secondaries.entries.delete(id);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
if (primaryMoves) {
|
|
1499
|
+
lines.push("unset ip");
|
|
1500
|
+
show.push(`- ${label}set ip ${before.settings.get("ip")}`);
|
|
1501
|
+
before.settings.delete("ip");
|
|
1502
|
+
}
|
|
1503
|
+
const ipv6 = before.blocks.get("ipv6");
|
|
1504
|
+
const keptIpv6 = after?.blocks.get("ipv6");
|
|
1505
|
+
const extra = ipv6?.blocks.get("ip6-extra-addr");
|
|
1506
|
+
const goneExtra = [...extra?.entries ?? []].filter(([id]) => !keptIpv6?.blocks.get("ip6-extra-addr")?.entries.has(id));
|
|
1507
|
+
const v6Lines = [];
|
|
1508
|
+
if (goneExtra.length) {
|
|
1509
|
+
v6Lines.push("config ip6-extra-addr", ...goneExtra.map(([, address]) => `delete ${address.id}`), "end");
|
|
1510
|
+
for (const [id, address] of goneExtra) {
|
|
1511
|
+
show.push(`- ${label}ipv6 ip6-extra-addr ${bare(address.id)}`);
|
|
1512
|
+
extra.entries.delete(id);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
if (ipv6?.settings.has("ip6-address") && !same("ip6-address", ipv6.settings.get("ip6-address"), keptIpv6?.settings.get("ip6-address"))) {
|
|
1516
|
+
v6Lines.push("unset ip6-address");
|
|
1517
|
+
show.push(`- ${label}ipv6: set ip6-address ${ipv6.settings.get("ip6-address")}`);
|
|
1518
|
+
ipv6.settings.delete("ip6-address");
|
|
1519
|
+
}
|
|
1520
|
+
if (v6Lines.length)
|
|
1521
|
+
lines.push("config ipv6", ...v6Lines, "end");
|
|
1522
|
+
if (lines.length)
|
|
1523
|
+
send.push(`edit ${entry.id}`, ...lines, "next");
|
|
1524
|
+
}
|
|
1525
|
+
return send.length ? { title: "system interface, addresses that move", show, send: ["config system interface", ...send, "end"] } : undefined;
|
|
1526
|
+
}
|
|
1527
|
+
function plan(desired, current, options) {
|
|
1528
|
+
const want = separateTunnels(parse(desired));
|
|
1529
|
+
const have = separateTunnels(parse(clean(current)));
|
|
1530
|
+
keepFilled(want, have);
|
|
1531
|
+
const first = [];
|
|
1532
|
+
const steps = [];
|
|
1533
|
+
const removals = [];
|
|
1534
|
+
const { freed, gone, trims, deferred } = released(want, have);
|
|
1535
|
+
const late = deferredPorts(want, have, deferred, options.secrets);
|
|
1536
|
+
if (freed.size) {
|
|
1537
|
+
for (const section of [...have.blocks.keys()].reverse()) {
|
|
1538
|
+
const block = have.blocks.get(section);
|
|
1539
|
+
if (section === TUNNELS) {
|
|
1540
|
+
for (const key of [...block.entries.keys()])
|
|
1541
|
+
if (freed.has(key))
|
|
1542
|
+
block.entries.delete(key);
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
const { send, show } = release(block, [], freed, KEYED_BY_INTERFACE.has(section) ? freed : section === "system interface" ? gone : new Set, false);
|
|
1546
|
+
if (send.length)
|
|
1547
|
+
first.push({ title: `${section}, released`, show, send: [`config ${section}`, ...send, "end"] });
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
if (trims.length) {
|
|
1551
|
+
const interfaces = have.blocks.get("system interface");
|
|
1552
|
+
const send = trims.flatMap(({ id, members }) => [`edit ${id}`, `set member ${quoteAll(members)}`, "next"]);
|
|
1553
|
+
const show = trims.map(({ id, members }) => `+ ${bare(id)}: set member ${quoteAll(members)}`);
|
|
1554
|
+
first.push({ title: "system interface, ports leaving a LAG", show, send: ["config system interface", ...send, "end"] });
|
|
1555
|
+
for (const { id, members } of trims)
|
|
1556
|
+
interfaces.entries.get(bare(id)).block.settings.set("member", quoteAll(members));
|
|
1557
|
+
}
|
|
1558
|
+
const moved = movedAddresses(want, have);
|
|
1559
|
+
if (moved)
|
|
1560
|
+
first.push(moved);
|
|
1561
|
+
for (const [section, block] of want.blocks) {
|
|
1562
|
+
const header = section === TUNNELS ? "system interface" : section;
|
|
1563
|
+
const context = { section: header, secrets: options.secrets };
|
|
1564
|
+
const table = section === TUNNELS ? undefined : TABLES[header];
|
|
1565
|
+
const diff = compare([], block, have.blocks.get(section) ?? new Block, table, true, context);
|
|
1566
|
+
if (diff.send.length)
|
|
1567
|
+
steps.push({ title: section, show: diff.show, send: [`config ${header}`, ...diff.send, "end"] });
|
|
1568
|
+
if (diff.removals.length && section !== TUNNELS) {
|
|
1569
|
+
removals.unshift({ title: `${section}, removed`, show: diff.removed, send: [`config ${header}`, ...diff.removals, "end"] });
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return { steps: [...first, ...steps, ...removals, ...late], problems: [] };
|
|
1573
|
+
}
|
|
1574
|
+
var TUNNELS = "system interface, tunnels";
|
|
1575
|
+
function separateTunnels(root) {
|
|
1576
|
+
const interfaces = root.blocks.get("system interface");
|
|
1577
|
+
if (!interfaces)
|
|
1578
|
+
return root;
|
|
1579
|
+
const tunnels = new Block;
|
|
1580
|
+
for (const [key, entry] of interfaces.entries) {
|
|
1581
|
+
const type = entry.block.settings.get("type");
|
|
1582
|
+
if (type !== "tunnel" && type !== "vxlan")
|
|
1583
|
+
continue;
|
|
1584
|
+
entry.block.settings.delete("interface");
|
|
1585
|
+
tunnels.entries.set(key, entry);
|
|
1586
|
+
interfaces.entries.delete(key);
|
|
1587
|
+
}
|
|
1588
|
+
const sections = [...root.blocks];
|
|
1589
|
+
root.blocks.clear();
|
|
1590
|
+
for (const [section, block] of sections) {
|
|
1591
|
+
root.blocks.set(section, block);
|
|
1592
|
+
if (section === "system vxlan")
|
|
1593
|
+
root.blocks.set(TUNNELS, tunnels);
|
|
1594
|
+
}
|
|
1595
|
+
if (!root.blocks.has(TUNNELS))
|
|
1596
|
+
root.blocks.set(TUNNELS, tunnels);
|
|
1597
|
+
return root;
|
|
1598
|
+
}
|
|
1599
|
+
function keepFilled(want, have) {
|
|
1600
|
+
for (const table of Object.values(PREFIX_LIST)) {
|
|
1601
|
+
for (const [key, entry] of want.blocks.get(table)?.entries ?? []) {
|
|
1602
|
+
if (!bare(entry.block.settings.get("comments") ?? "").startsWith(FILLED))
|
|
1603
|
+
continue;
|
|
1604
|
+
const held = have.blocks.get(table)?.entries.get(key)?.block.blocks.get("rule");
|
|
1605
|
+
if (held)
|
|
1606
|
+
entry.block.blocks.set("rule", held);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
function deferredPorts(want, have, ports, secrets) {
|
|
1611
|
+
const wanted = want.blocks.get("system interface");
|
|
1612
|
+
const held = have.blocks.get("system interface");
|
|
1613
|
+
if (!ports.size || !wanted || !held)
|
|
1614
|
+
return [];
|
|
1615
|
+
const later = { want: new Block, have: new Block };
|
|
1616
|
+
for (const port of ports) {
|
|
1617
|
+
const after = wanted.entries.get(port);
|
|
1618
|
+
const before = held.entries.get(port);
|
|
1619
|
+
if (!after || !before)
|
|
1620
|
+
continue;
|
|
1621
|
+
later.want.entries.set(port, after);
|
|
1622
|
+
later.have.entries.set(port, before);
|
|
1623
|
+
wanted.entries.delete(port);
|
|
1624
|
+
held.entries.delete(port);
|
|
1625
|
+
}
|
|
1626
|
+
const diff = compare([], later.want, later.have, TABLES["system interface"], true, { section: "system interface", secrets });
|
|
1627
|
+
if (!diff.send.length)
|
|
1628
|
+
return [];
|
|
1629
|
+
return [
|
|
1630
|
+
{ title: "system interface, ports of a LAG that went", show: diff.show, send: ["config system interface", ...diff.send, "end"] }
|
|
1631
|
+
];
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// src/adapters/devices/fortios/render.ts
|
|
1635
|
+
var BANNER = 'system replacemsg admin "pre_admin-disclaimer-text"';
|
|
1636
|
+
var SECTIONS = [
|
|
1637
|
+
"vpn certificate local",
|
|
1638
|
+
"vpn certificate ca",
|
|
1639
|
+
"system global",
|
|
1640
|
+
BANNER,
|
|
1641
|
+
"system settings",
|
|
1642
|
+
"system accprofile",
|
|
1643
|
+
"system admin",
|
|
1644
|
+
"system interface",
|
|
1645
|
+
"system gre-tunnel",
|
|
1646
|
+
"system vxlan",
|
|
1647
|
+
"system dns",
|
|
1648
|
+
"system dns-server",
|
|
1649
|
+
"system ntp",
|
|
1650
|
+
"system snmp sysinfo",
|
|
1651
|
+
"system snmp community",
|
|
1652
|
+
"system snmp user",
|
|
1653
|
+
"log memory setting",
|
|
1654
|
+
"log memory filter",
|
|
1655
|
+
"log disk setting",
|
|
1656
|
+
...["", "2", "3", "4"].flatMap((number) => [`log syslogd${number} setting`, `log syslogd${number} filter`]),
|
|
1657
|
+
"system sflow",
|
|
1658
|
+
"system netflow",
|
|
1659
|
+
"system dhcp server",
|
|
1660
|
+
"router static",
|
|
1661
|
+
"router static6",
|
|
1662
|
+
"router prefix-list",
|
|
1663
|
+
"router prefix-list6",
|
|
1664
|
+
"router community-list",
|
|
1665
|
+
"router route-map",
|
|
1666
|
+
"router ospf",
|
|
1667
|
+
"router ospf6",
|
|
1668
|
+
"router bgp",
|
|
1669
|
+
"system session-helper",
|
|
1670
|
+
"firewall address",
|
|
1671
|
+
"firewall address6",
|
|
1672
|
+
"firewall addrgrp",
|
|
1673
|
+
"firewall addrgrp6",
|
|
1674
|
+
"firewall service custom",
|
|
1675
|
+
"firewall ippool",
|
|
1676
|
+
"firewall vip",
|
|
1677
|
+
"firewall policy",
|
|
1678
|
+
"firewall policy6",
|
|
1679
|
+
"firewall local-in-policy",
|
|
1680
|
+
"firewall local-in-policy6",
|
|
1681
|
+
"firewall central-snat-map",
|
|
1682
|
+
"firewall acl",
|
|
1683
|
+
"firewall acl6"
|
|
1684
|
+
];
|
|
1685
|
+
var FULL = ["vpn certificate local", "vpn certificate ca"];
|
|
1686
|
+
var ZONES = {
|
|
1687
|
+
"00": ["Pacific/Kwajalein"],
|
|
1688
|
+
"01": ["Pacific/Midway", "Pacific/Pago_Pago"],
|
|
1689
|
+
"02": ["Pacific/Honolulu"],
|
|
1690
|
+
"03": ["America/Anchorage"],
|
|
1691
|
+
"04": ["America/Los_Angeles", "America/Vancouver", "America/Tijuana"],
|
|
1692
|
+
"05": ["America/Phoenix"],
|
|
1693
|
+
"06": ["America/Denver", "America/Edmonton"],
|
|
1694
|
+
"07": ["America/Guatemala", "America/Costa_Rica", "America/El_Salvador", "America/Tegucigalpa", "America/Managua"],
|
|
1695
|
+
"08": ["America/Chicago", "America/Winnipeg"],
|
|
1696
|
+
"09": ["America/Mexico_City"],
|
|
1697
|
+
"10": ["America/Regina"],
|
|
1698
|
+
"11": ["America/Bogota", "America/Lima", "America/Guayaquil"],
|
|
1699
|
+
"12": ["America/New_York", "America/Toronto"],
|
|
1700
|
+
"13": ["America/Indiana/Indianapolis"],
|
|
1701
|
+
"14": ["America/Halifax"],
|
|
1702
|
+
"15": ["America/La_Paz"],
|
|
1703
|
+
"16": ["America/Santiago"],
|
|
1704
|
+
"17": ["America/St_Johns"],
|
|
1705
|
+
"18": ["America/Sao_Paulo"],
|
|
1706
|
+
"19": ["America/Argentina/Buenos_Aires"],
|
|
1707
|
+
"20": ["America/Nuuk", "America/Godthab"],
|
|
1708
|
+
"21": ["Atlantic/South_Georgia"],
|
|
1709
|
+
"22": ["Atlantic/Azores"],
|
|
1710
|
+
"23": ["Atlantic/Cape_Verde"],
|
|
1711
|
+
"24": ["Africa/Monrovia"],
|
|
1712
|
+
"25": ["Europe/London", "Europe/Dublin", "Europe/Lisbon", "Atlantic/Canary"],
|
|
1713
|
+
"26": ["Europe/Amsterdam", "Europe/Berlin", "Europe/Zurich", "Europe/Rome", "Europe/Stockholm", "Europe/Vienna"],
|
|
1714
|
+
"27": ["Europe/Belgrade", "Europe/Bratislava", "Europe/Budapest", "Europe/Ljubljana", "Europe/Prague"],
|
|
1715
|
+
"28": ["Europe/Brussels", "Europe/Copenhagen", "Europe/Madrid", "Europe/Paris"],
|
|
1716
|
+
"29": ["Europe/Sarajevo", "Europe/Skopje", "Europe/Warsaw", "Europe/Zagreb"],
|
|
1717
|
+
"30": ["Africa/Lagos"],
|
|
1718
|
+
"31": ["Europe/Athens", "Europe/Sofia", "Europe/Vilnius"],
|
|
1719
|
+
"32": ["Europe/Bucharest"],
|
|
1720
|
+
"33": ["Africa/Cairo"],
|
|
1721
|
+
"34": ["Africa/Harare", "Africa/Johannesburg"],
|
|
1722
|
+
"35": ["Europe/Helsinki", "Europe/Riga", "Europe/Tallinn"],
|
|
1723
|
+
"36": ["Asia/Jerusalem"],
|
|
1724
|
+
"37": ["Asia/Baghdad"],
|
|
1725
|
+
"38": ["Asia/Kuwait", "Asia/Riyadh"],
|
|
1726
|
+
"39": ["Europe/Volgograd"],
|
|
1727
|
+
"40": ["Africa/Nairobi"],
|
|
1728
|
+
"41": ["Asia/Tehran"],
|
|
1729
|
+
"42": ["Asia/Dubai", "Asia/Muscat"],
|
|
1730
|
+
"43": ["Asia/Baku"],
|
|
1731
|
+
"44": ["Asia/Kabul"],
|
|
1732
|
+
"45": ["Asia/Yekaterinburg"],
|
|
1733
|
+
"46": ["Asia/Karachi", "Asia/Tashkent"],
|
|
1734
|
+
"47": ["Asia/Kolkata", "Asia/Calcutta"],
|
|
1735
|
+
"48": ["Asia/Kathmandu"],
|
|
1736
|
+
"49": ["Asia/Almaty", "Asia/Novosibirsk"],
|
|
1737
|
+
"50": ["Asia/Dhaka"],
|
|
1738
|
+
"51": ["Asia/Colombo"],
|
|
1739
|
+
"52": ["Asia/Yangon", "Asia/Rangoon"],
|
|
1740
|
+
"53": ["Asia/Jakarta", "Asia/Bangkok", "Asia/Ho_Chi_Minh"],
|
|
1741
|
+
"54": ["Asia/Krasnoyarsk"],
|
|
1742
|
+
"55": ["Asia/Shanghai", "Asia/Chongqing", "Asia/Hong_Kong", "Asia/Urumqi", "Asia/Irkutsk"],
|
|
1743
|
+
"56": ["Asia/Ulaanbaatar"],
|
|
1744
|
+
"57": ["Asia/Kuala_Lumpur", "Asia/Singapore"],
|
|
1745
|
+
"58": ["Australia/Perth"],
|
|
1746
|
+
"59": ["Asia/Taipei"],
|
|
1747
|
+
"60": ["Asia/Tokyo", "Asia/Seoul"],
|
|
1748
|
+
"61": ["Asia/Yakutsk"],
|
|
1749
|
+
"62": ["Australia/Adelaide"],
|
|
1750
|
+
"63": ["Australia/Darwin"],
|
|
1751
|
+
"64": ["Australia/Brisbane"],
|
|
1752
|
+
"65": ["Australia/Sydney", "Australia/Melbourne", "Australia/Canberra"],
|
|
1753
|
+
"66": ["Pacific/Guam", "Pacific/Port_Moresby"],
|
|
1754
|
+
"67": ["Australia/Hobart"],
|
|
1755
|
+
"68": ["Asia/Vladivostok"],
|
|
1756
|
+
"69": ["Asia/Magadan"],
|
|
1757
|
+
"70": ["Pacific/Guadalcanal", "Pacific/Noumea"],
|
|
1758
|
+
"71": ["Pacific/Auckland"],
|
|
1759
|
+
"72": ["Pacific/Fiji", "Asia/Kamchatka", "Pacific/Majuro"],
|
|
1760
|
+
"73": ["Pacific/Tongatapu"],
|
|
1761
|
+
"74": ["America/Caracas"],
|
|
1762
|
+
"75": ["America/Montevideo"],
|
|
1763
|
+
"76": ["Pacific/Kiritimati"],
|
|
1764
|
+
"77": ["America/Guyana"],
|
|
1765
|
+
"78": ["Africa/Windhoek"],
|
|
1766
|
+
"79": ["Africa/Casablanca"],
|
|
1767
|
+
"80": ["UTC", "Etc/UTC", "Etc/GMT", "GMT"],
|
|
1768
|
+
"81": ["America/Mazatlan", "America/Chihuahua"],
|
|
1769
|
+
"82": ["Pacific/Chatham"],
|
|
1770
|
+
"83": ["Europe/Moscow"],
|
|
1771
|
+
"84": ["Europe/Minsk"],
|
|
1772
|
+
"85": ["Europe/Istanbul"],
|
|
1773
|
+
"86": ["Pacific/Apia"],
|
|
1774
|
+
"87": ["America/Asuncion"]
|
|
1775
|
+
};
|
|
1776
|
+
function zoneOf(timezone = "UTC") {
|
|
1777
|
+
return Object.entries(ZONES).find(([, places]) => places.includes(timezone))?.[0];
|
|
1778
|
+
}
|
|
1779
|
+
var PROFILE = { admin: "super_admin", operator: "circuit-operator", "read-only": "circuit-read-only" };
|
|
1780
|
+
var PERMISSION_GROUPS = [
|
|
1781
|
+
"secfabgrp",
|
|
1782
|
+
"ftviewgrp",
|
|
1783
|
+
"authgrp",
|
|
1784
|
+
"sysgrp",
|
|
1785
|
+
"netgrp",
|
|
1786
|
+
"loggrp",
|
|
1787
|
+
"fwgrp",
|
|
1788
|
+
"vpngrp",
|
|
1789
|
+
"utmgrp",
|
|
1790
|
+
"wanoptgrp",
|
|
1791
|
+
"wifi"
|
|
1792
|
+
];
|
|
1793
|
+
var SSH_KEYS = 3;
|
|
1794
|
+
var RESOLVERS = { device: 2, dhcp: 4 };
|
|
1795
|
+
var COLLECTORS = 4;
|
|
1796
|
+
var VRFS = 31;
|
|
1797
|
+
var LEVEL = {
|
|
1798
|
+
emergency: "emergency",
|
|
1799
|
+
alert: "alert",
|
|
1800
|
+
critical: "critical",
|
|
1801
|
+
error: "error",
|
|
1802
|
+
warning: "warning",
|
|
1803
|
+
notice: "notification",
|
|
1804
|
+
info: "information",
|
|
1805
|
+
debug: "debug"
|
|
1806
|
+
};
|
|
1807
|
+
var HELPERS = {
|
|
1808
|
+
ftp: [["ftp", 6, 21]],
|
|
1809
|
+
tftp: [["tftp", 17, 69]],
|
|
1810
|
+
sip: [["sip", 17, 5060]],
|
|
1811
|
+
h323: [
|
|
1812
|
+
["h323", 6, 1720],
|
|
1813
|
+
["ras", 17, 1719]
|
|
1814
|
+
],
|
|
1815
|
+
pptp: [["pptp", 6, 1723]],
|
|
1816
|
+
rtsp: [
|
|
1817
|
+
["rtsp", 6, 554],
|
|
1818
|
+
["rtsp", 6, 7070],
|
|
1819
|
+
["rtsp", 6, 8554]
|
|
1820
|
+
]
|
|
1821
|
+
};
|
|
1822
|
+
var SPEEDS = {
|
|
1823
|
+
"40g": { "40g": "40000full" },
|
|
1824
|
+
"10g": { "10g": "10000full", "1g": "1000full" },
|
|
1825
|
+
"1g": { "1g": "1000full", "100m": "100full" }
|
|
1826
|
+
};
|
|
1827
|
+
function allowAccess(device) {
|
|
1828
|
+
const management = device.management ?? {};
|
|
1829
|
+
const services = ["ping"];
|
|
1830
|
+
if (management.https)
|
|
1831
|
+
services.push("https");
|
|
1832
|
+
if (management.ssh)
|
|
1833
|
+
services.push("ssh");
|
|
1834
|
+
if (management.snmp)
|
|
1835
|
+
services.push("snmp");
|
|
1836
|
+
if (management.http)
|
|
1837
|
+
services.push("http");
|
|
1838
|
+
if (management.telnet)
|
|
1839
|
+
services.push("telnet");
|
|
1840
|
+
if (management.native)
|
|
1841
|
+
services.push("fgfm");
|
|
1842
|
+
return services.join(" ");
|
|
1843
|
+
}
|
|
1844
|
+
function render(device) {
|
|
1845
|
+
const root = new Block;
|
|
1846
|
+
for (const section of SECTIONS)
|
|
1847
|
+
root.block(section);
|
|
1848
|
+
const section = (name) => root.block(name);
|
|
1849
|
+
certificates(section("vpn certificate local"), section("vpn certificate ca"), device);
|
|
1850
|
+
global(section("system global"), device);
|
|
1851
|
+
if (device.system?.banner)
|
|
1852
|
+
section(BANNER).set("buffer", quote(device.system.banner));
|
|
1853
|
+
if (natRules(device).length)
|
|
1854
|
+
section("system settings").set("central-nat", "enable");
|
|
1855
|
+
section("system settings").set("default-voip-alg-mode", "kernel-helper-based");
|
|
1856
|
+
if (usesBfd(device))
|
|
1857
|
+
section("system settings").set("bfd", "enable");
|
|
1858
|
+
users(section("system accprofile"), section("system admin"), device);
|
|
1859
|
+
interfaces(root, device);
|
|
1860
|
+
dns(section("system dns"), section("system dns-server"), device);
|
|
1861
|
+
ntp(section("system ntp"), device);
|
|
1862
|
+
snmp(root, device);
|
|
1863
|
+
logging(root, device);
|
|
1864
|
+
flowExport(root, device);
|
|
1865
|
+
dhcpServers(section("system dhcp server"), device);
|
|
1866
|
+
staticRoutes(section("router static"), section("router static6"), device);
|
|
1867
|
+
routing(root, device);
|
|
1868
|
+
helpers(section("system session-helper"), device);
|
|
1869
|
+
firewall(root, device);
|
|
1870
|
+
return print(root);
|
|
1871
|
+
}
|
|
1872
|
+
function certificates(local, trusted, device) {
|
|
1873
|
+
for (const [name, certificate] of Object.entries(device.certificates ?? {})) {
|
|
1874
|
+
const text = quote(certificate.certificate.trim());
|
|
1875
|
+
if (certificate.private_key)
|
|
1876
|
+
local.entry(name).set("private-key", quote(marker(certificate.private_key))).set("certificate", text);
|
|
1877
|
+
else
|
|
1878
|
+
trusted.entry(name).set("ca", text);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
function global(block, device) {
|
|
1882
|
+
const management = device.management ?? {};
|
|
1883
|
+
block.set("hostname", quote(device.name));
|
|
1884
|
+
const zone = zoneOf(device.system?.timezone);
|
|
1885
|
+
if (zone && zone !== "00")
|
|
1886
|
+
block.set("timezone", zone);
|
|
1887
|
+
if (management.ssh?.port)
|
|
1888
|
+
block.set("admin-ssh-port", management.ssh.port);
|
|
1889
|
+
if (management.ssh?.auth_timeout)
|
|
1890
|
+
block.set("admin-ssh-grace-time", management.ssh.auth_timeout);
|
|
1891
|
+
if (management.ssh?.weak_crypto)
|
|
1892
|
+
block.set("strong-crypto", "disable");
|
|
1893
|
+
if (management.https?.port)
|
|
1894
|
+
block.set("admin-sport", management.https.port);
|
|
1895
|
+
if (management.https?.certificate)
|
|
1896
|
+
block.set("admin-server-cert", quote(management.https.certificate));
|
|
1897
|
+
if (management.http?.port)
|
|
1898
|
+
block.set("admin-port", management.http.port);
|
|
1899
|
+
if (management.telnet?.port)
|
|
1900
|
+
block.set("admin-telnet-port", management.telnet.port);
|
|
1901
|
+
if (management.http)
|
|
1902
|
+
block.set("admin-https-redirect", "disable");
|
|
1903
|
+
if (device.system?.banner)
|
|
1904
|
+
block.set("pre-login-banner", "enable");
|
|
1905
|
+
if (device.lldp === true)
|
|
1906
|
+
block.set("lldp-transmission", "enable").set("lldp-reception", "enable");
|
|
1907
|
+
}
|
|
1908
|
+
function users(profiles, admins, device) {
|
|
1909
|
+
const roles = new Set(Object.values(device.users ?? {}).map((user) => user.role));
|
|
1910
|
+
for (const role of ["operator", "read-only"]) {
|
|
1911
|
+
if (!roles.has(role))
|
|
1912
|
+
continue;
|
|
1913
|
+
const profile = profiles.entry(PROFILE[role]);
|
|
1914
|
+
for (const group of PERMISSION_GROUPS)
|
|
1915
|
+
profile.set(group, role === "operator" && group !== "sysgrp" ? "read-write" : "read");
|
|
1916
|
+
}
|
|
1917
|
+
for (const [name, user] of Object.entries(device.users ?? {})) {
|
|
1918
|
+
const admin = admins.entry(name);
|
|
1919
|
+
admin.set("accprofile", quote(PROFILE[user.role]));
|
|
1920
|
+
if (user.description)
|
|
1921
|
+
admin.set("comments", quote(user.description));
|
|
1922
|
+
admin.set("vdom", quote("root"));
|
|
1923
|
+
(user.ssh_keys ?? []).forEach((key, index) => admin.set(`ssh-public-key${index + 1}`, quote(key)));
|
|
1924
|
+
admin.set("password", quote(marker(user.password)));
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
function interfaces(root, device) {
|
|
1928
|
+
const block = root.block("system interface");
|
|
1929
|
+
const access = allowAccess(device);
|
|
1930
|
+
for (const port of portsOf(device.platform, device.model)) {
|
|
1931
|
+
const settings = device.ports?.[port];
|
|
1932
|
+
const entry = block.entry(spell(device.platform, device.model, port));
|
|
1933
|
+
entry.set("vdom", quote("root"));
|
|
1934
|
+
entry.set("type", "physical");
|
|
1935
|
+
if (!settings) {
|
|
1936
|
+
redirects(entry, device);
|
|
1937
|
+
entry.set("status", "down");
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
if (settings.description)
|
|
1941
|
+
entry.set("description", quote(settings.description));
|
|
1942
|
+
const speed = settings.speed && SPEEDS[port.split("-")[0]]?.[settings.speed];
|
|
1943
|
+
if (speed)
|
|
1944
|
+
entry.set("speed", speed);
|
|
1945
|
+
if (settings.lag)
|
|
1946
|
+
continue;
|
|
1947
|
+
redirects(entry, device);
|
|
1948
|
+
if (settings.mtu)
|
|
1949
|
+
entry.set("mtu-override", "enable").set("mtu", settings.mtu);
|
|
1950
|
+
routed(entry, device, port, settings, access);
|
|
1951
|
+
}
|
|
1952
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
1953
|
+
if (iface.type !== "lag")
|
|
1954
|
+
continue;
|
|
1955
|
+
const members = Object.entries(device.ports ?? {}).filter(([, port]) => port?.lag === name).map(([port]) => spell(device.platform, device.model, port));
|
|
1956
|
+
const entry = block.entry(name);
|
|
1957
|
+
entry.set("vdom", quote("root"));
|
|
1958
|
+
entry.set("type", "aggregate");
|
|
1959
|
+
if (members.length)
|
|
1960
|
+
entry.set("member", quoteAll(members));
|
|
1961
|
+
if (iface.mode === "static")
|
|
1962
|
+
entry.set("lacp-mode", "static");
|
|
1963
|
+
if (iface.description)
|
|
1964
|
+
entry.set("description", quote(iface.description));
|
|
1965
|
+
if (iface.mtu)
|
|
1966
|
+
entry.set("mtu-override", "enable").set("mtu", iface.mtu);
|
|
1967
|
+
redirects(entry, device);
|
|
1968
|
+
routed(entry, device, name, iface, access);
|
|
1969
|
+
}
|
|
1970
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
1971
|
+
if (iface.type !== "vlan")
|
|
1972
|
+
continue;
|
|
1973
|
+
const parent = vlanParent(device, iface.vlan);
|
|
1974
|
+
const entry = block.entry(name);
|
|
1975
|
+
entry.set("vdom", quote("root"));
|
|
1976
|
+
if (parent)
|
|
1977
|
+
entry.set("interface", quote(nameOf(device, parent)));
|
|
1978
|
+
entry.set("vlanid", device.vlans?.[iface.vlan]?.id ?? 0);
|
|
1979
|
+
if (iface.description)
|
|
1980
|
+
entry.set("description", quote(iface.description));
|
|
1981
|
+
if (iface.mtu)
|
|
1982
|
+
entry.set("mtu-override", "enable").set("mtu", iface.mtu);
|
|
1983
|
+
redirects(entry, device);
|
|
1984
|
+
routed(entry, device, name, iface, access);
|
|
1985
|
+
}
|
|
1986
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
1987
|
+
if (iface.type !== "loopback")
|
|
1988
|
+
continue;
|
|
1989
|
+
const entry = block.entry(name);
|
|
1990
|
+
entry.set("vdom", quote("root"));
|
|
1991
|
+
entry.set("type", "loopback");
|
|
1992
|
+
if (iface.description)
|
|
1993
|
+
entry.set("description", quote(iface.description));
|
|
1994
|
+
redirects(entry, device);
|
|
1995
|
+
routed(entry, device, name, iface, access);
|
|
1996
|
+
}
|
|
1997
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
1998
|
+
if (iface.type !== "gre" && iface.type !== "vxlan")
|
|
1999
|
+
continue;
|
|
2000
|
+
const outgoing = holderOf(device, iface.local);
|
|
2001
|
+
const via = outgoing ? quote(nameOf(device, outgoing)) : '""';
|
|
2002
|
+
const ipv6 = familyOf2(iface.remote) === "ipv6";
|
|
2003
|
+
if (iface.type === "gre") {
|
|
2004
|
+
const tunnel = root.block("system gre-tunnel").entry(name).set("interface", via);
|
|
2005
|
+
if (ipv6)
|
|
2006
|
+
tunnel.set("ip-version", 6).set("remote-gw6", iface.remote).set("local-gw6", iface.local);
|
|
2007
|
+
else
|
|
2008
|
+
tunnel.set("remote-gw", iface.remote).set("local-gw", iface.local);
|
|
2009
|
+
} else {
|
|
2010
|
+
const tunnel = root.block("system vxlan").entry(name).set("interface", via).set("vni", iface.vni);
|
|
2011
|
+
if (ipv6)
|
|
2012
|
+
tunnel.set("ip-version", "ipv6-unicast").set("remote-ip6", quote(iface.remote));
|
|
2013
|
+
else
|
|
2014
|
+
tunnel.set("remote-ip", quote(iface.remote));
|
|
2015
|
+
if (iface.port !== undefined && iface.port !== 4789)
|
|
2016
|
+
tunnel.set("dstport", iface.port);
|
|
2017
|
+
}
|
|
2018
|
+
const entry = block.entry(name);
|
|
2019
|
+
entry.set("vdom", quote("root"));
|
|
2020
|
+
entry.set("type", iface.type === "gre" ? "tunnel" : "vxlan");
|
|
2021
|
+
entry.set("interface", via);
|
|
2022
|
+
if (iface.type === "vxlan" && iface.mac)
|
|
2023
|
+
entry.set("macaddr", iface.mac.toLowerCase());
|
|
2024
|
+
if (iface.description)
|
|
2025
|
+
entry.set("description", quote(iface.description));
|
|
2026
|
+
if (iface.mtu)
|
|
2027
|
+
entry.set("mtu-override", "enable").set("mtu", iface.mtu);
|
|
2028
|
+
redirects(entry, device);
|
|
2029
|
+
routed(entry, device, name, iface, access);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
function redirects(entry, device) {
|
|
2033
|
+
if (!device.system?.ip?.icmp_redirects)
|
|
2034
|
+
entry.set("icmp-send-redirect", "disable");
|
|
2035
|
+
}
|
|
2036
|
+
function vlanParent(device, vlan) {
|
|
2037
|
+
const carriers = [
|
|
2038
|
+
...Object.entries(device.ports ?? {}).filter(([, port]) => !port?.lag && port?.trunk_vlans?.includes(vlan)),
|
|
2039
|
+
...Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "lag" && iface.trunk_vlans?.includes(vlan))
|
|
2040
|
+
];
|
|
2041
|
+
return carriers.length === 1 ? carriers[0][0] : undefined;
|
|
2042
|
+
}
|
|
2043
|
+
function routed(entry, device, name, settings, access) {
|
|
2044
|
+
const addresses = addressesOf2(settings);
|
|
2045
|
+
const ipv4 = addresses.filter((address) => familyOf2(host2(address)) === "ipv4");
|
|
2046
|
+
const ipv6 = addresses.filter((address) => familyOf2(host2(address)) === "ipv6");
|
|
2047
|
+
const vrf = vrfNumber(device, settings.vrf);
|
|
2048
|
+
if (vrf)
|
|
2049
|
+
entry.set("vrf", vrf);
|
|
2050
|
+
const peer = settings.addresses?.find((item) => typeof item !== "string");
|
|
2051
|
+
if (peer) {
|
|
2052
|
+
entry.set("ip", `${host2(peer.address)} 255.255.255.255`);
|
|
2053
|
+
entry.set("remote-ip", `${peer.peer} ${netmask(split(peer.address).len)}`);
|
|
2054
|
+
entry.set("allowaccess", access);
|
|
2055
|
+
} else if (ipv4.length) {
|
|
2056
|
+
entry.set("ip", subnet(ipv4[0]));
|
|
2057
|
+
entry.set("allowaccess", access);
|
|
2058
|
+
}
|
|
2059
|
+
if (!peer && ipv4.length > 1) {
|
|
2060
|
+
entry.set("secondary-IP", "enable");
|
|
2061
|
+
const secondaries = entry.block("secondaryip");
|
|
2062
|
+
ipv4.slice(1).forEach((address, index) => secondaries.entry(index + 1).set("ip", subnet(address)).set("allowaccess", access));
|
|
2063
|
+
}
|
|
2064
|
+
const relays = Object.values(device.dhcp_relay ?? {}).filter((relay) => relay.interface === name);
|
|
2065
|
+
const relayed = relays.flatMap((relay) => relay.servers);
|
|
2066
|
+
const relayed4 = relayed.filter((server) => familyOf2(server) === "ipv4");
|
|
2067
|
+
const relayed6 = relayed.filter((server) => familyOf2(server) === "ipv6");
|
|
2068
|
+
if (ipv6.length || relayed6.length) {
|
|
2069
|
+
const block = entry.block("ipv6");
|
|
2070
|
+
if (ipv6.length) {
|
|
2071
|
+
block.set("ip6-address", ipv6[0]);
|
|
2072
|
+
block.set("ip6-allowaccess", access);
|
|
2073
|
+
}
|
|
2074
|
+
if (settings.ipv6_ra) {
|
|
2075
|
+
block.set("ip6-send-adv", "enable");
|
|
2076
|
+
const prefixes = block.block("ip6-prefix-list");
|
|
2077
|
+
for (const address of ipv6)
|
|
2078
|
+
prefixes.entry(network2(address), true);
|
|
2079
|
+
}
|
|
2080
|
+
if (relayed6.length)
|
|
2081
|
+
block.set("dhcp6-relay-service", "enable").set("dhcp6-relay-ip", relayed6.join(" "));
|
|
2082
|
+
if (ipv6.length > 1) {
|
|
2083
|
+
const extra = block.block("ip6-extra-addr");
|
|
2084
|
+
for (const address of ipv6.slice(1))
|
|
2085
|
+
extra.entry(address, true);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
if (settings.vrrp?.length) {
|
|
2089
|
+
const groups = entry.block("vrrp");
|
|
2090
|
+
for (const group of settings.vrrp) {
|
|
2091
|
+
const vrrp = groups.entry(group.id).set("vrip", group.address);
|
|
2092
|
+
if (group.priority !== undefined && group.priority !== 100)
|
|
2093
|
+
vrrp.set("priority", group.priority);
|
|
2094
|
+
if (group.interval !== undefined && group.interval !== 1)
|
|
2095
|
+
vrrp.set("adv-interval", group.interval);
|
|
2096
|
+
if (!group.preempt)
|
|
2097
|
+
vrrp.set("preempt", "disable");
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
if (relayed4.length) {
|
|
2101
|
+
entry.set("dhcp-relay-service", "enable");
|
|
2102
|
+
entry.set("dhcp-relay-ip", quoteAll(relayed4));
|
|
2103
|
+
}
|
|
2104
|
+
const listed = typeof device.lldp === "object" && device.lldp.interfaces.includes(name);
|
|
2105
|
+
if (settings.lldp === false)
|
|
2106
|
+
entry.set("lldp-transmission", "disable").set("lldp-reception", "disable");
|
|
2107
|
+
else if (listed || settings.lldp === true && device.lldp !== true)
|
|
2108
|
+
entry.set("lldp-transmission", "enable").set("lldp-reception", "enable");
|
|
2109
|
+
const flow = device.flow_export;
|
|
2110
|
+
const tunnel = ["gre", "vxlan"].includes(device.interfaces?.[name]?.type ?? "");
|
|
2111
|
+
const sampled = flow && !tunnel && (!flow.interfaces || flow.interfaces.includes(name));
|
|
2112
|
+
if (sampled && flow.protocol === "sflow") {
|
|
2113
|
+
entry.set("sflow-sampler", "enable");
|
|
2114
|
+
if (flow.sampling !== undefined && flow.sampling !== 2000)
|
|
2115
|
+
entry.set("sample-rate", flow.sampling);
|
|
2116
|
+
}
|
|
2117
|
+
if (sampled && flow.protocol === "netflow-v9")
|
|
2118
|
+
entry.set("netflow-sampler", "both");
|
|
2119
|
+
}
|
|
2120
|
+
function dns(block, servers, device) {
|
|
2121
|
+
const resolvers = device.system?.dns?.servers ?? [];
|
|
2122
|
+
const ipv4 = resolvers.filter((server) => familyOf2(server) === "ipv4");
|
|
2123
|
+
const ipv6 = resolvers.filter((server) => familyOf2(server) === "ipv6");
|
|
2124
|
+
if (ipv4[0])
|
|
2125
|
+
block.set("primary", ipv4[0]);
|
|
2126
|
+
if (ipv4[1])
|
|
2127
|
+
block.set("secondary", ipv4[1]);
|
|
2128
|
+
if (ipv6[0])
|
|
2129
|
+
block.set("ip6-primary", ipv6[0]);
|
|
2130
|
+
if (ipv6[1])
|
|
2131
|
+
block.set("ip6-secondary", ipv6[1]);
|
|
2132
|
+
if (device.system?.dns?.serve)
|
|
2133
|
+
for (const name of addressed(device))
|
|
2134
|
+
servers.entry(nameOf(device, name));
|
|
2135
|
+
}
|
|
2136
|
+
function ntp(block, device) {
|
|
2137
|
+
const ntp = device.system?.ntp;
|
|
2138
|
+
const servers = ntp?.servers ?? [];
|
|
2139
|
+
if (servers.length) {
|
|
2140
|
+
block.set("ntpsync", "enable");
|
|
2141
|
+
block.set("type", "custom");
|
|
2142
|
+
const list = block.block("ntpserver");
|
|
2143
|
+
servers.forEach((server, index) => list.entry(index + 1).set("server", quote(server)));
|
|
2144
|
+
}
|
|
2145
|
+
if (ntp?.serve) {
|
|
2146
|
+
block.set("server-mode", "enable");
|
|
2147
|
+
block.set("interface", quoteAll(addressed(device).map((name) => nameOf(device, name))));
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
function snmp(root, device) {
|
|
2151
|
+
const snmp = device.management?.snmp;
|
|
2152
|
+
if (!snmp)
|
|
2153
|
+
return;
|
|
2154
|
+
const sysinfo = root.block("system snmp sysinfo");
|
|
2155
|
+
sysinfo.set("status", "enable");
|
|
2156
|
+
if (snmp.contact)
|
|
2157
|
+
sysinfo.set("contact-info", quote(snmp.contact));
|
|
2158
|
+
if (snmp.location)
|
|
2159
|
+
sysinfo.set("location", quote(snmp.location));
|
|
2160
|
+
if (snmp.community) {
|
|
2161
|
+
const community = root.block("system snmp community").entry(1);
|
|
2162
|
+
community.set("name", quote(marker(snmp.community)));
|
|
2163
|
+
const allow = snmp.allow ?? device.management?.allow ?? [];
|
|
2164
|
+
const ipv4 = allow.filter((prefix) => familyOf2(host2(prefix)) === "ipv4");
|
|
2165
|
+
const ipv6 = allow.filter((prefix) => familyOf2(host2(prefix)) === "ipv6");
|
|
2166
|
+
ipv4.forEach((prefix, index) => community.block("hosts").entry(index + 1).set("ip", subnet(network2(prefix))));
|
|
2167
|
+
ipv6.forEach((prefix, index) => community.block("hosts6").entry(index + 1).set("ipv6", network2(prefix)));
|
|
2168
|
+
community.set("query-v1-status", "disable");
|
|
2169
|
+
if (snmp.port)
|
|
2170
|
+
community.set("query-v2c-port", snmp.port);
|
|
2171
|
+
community.set("trap-v1-status", "disable");
|
|
2172
|
+
community.set("trap-v2c-status", "disable");
|
|
2173
|
+
}
|
|
2174
|
+
for (const [name, user] of Object.entries(snmp.users ?? {})) {
|
|
2175
|
+
const entry = root.block("system snmp user").entry(name);
|
|
2176
|
+
entry.set("security-level", "auth-priv");
|
|
2177
|
+
if (snmp.port)
|
|
2178
|
+
entry.set("query-port", snmp.port);
|
|
2179
|
+
entry.set("auth-pwd", quote(marker(user.auth_password)));
|
|
2180
|
+
if (user.privacy === "des")
|
|
2181
|
+
entry.set("priv-proto", "des");
|
|
2182
|
+
entry.set("priv-pwd", quote(marker(user.privacy_password)));
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
function logging(root, device) {
|
|
2186
|
+
const logging = device.system?.logging;
|
|
2187
|
+
root.block("log memory setting").set("status", logging?.local ? "enable" : "disable");
|
|
2188
|
+
if (logging?.local && logging.local.level !== "info")
|
|
2189
|
+
root.block("log memory filter").set("severity", LEVEL[logging.local.level]);
|
|
2190
|
+
root.block("log disk setting").set("status", "disable");
|
|
2191
|
+
(logging?.remote ?? []).slice(0, COLLECTORS).forEach((collector, index) => {
|
|
2192
|
+
const number = index === 0 ? "" : String(index + 1);
|
|
2193
|
+
const setting = root.block(`log syslogd${number} setting`);
|
|
2194
|
+
setting.set("status", "enable");
|
|
2195
|
+
setting.set("server", quote(collector.address));
|
|
2196
|
+
if (collector.port && collector.port !== 514)
|
|
2197
|
+
setting.set("port", collector.port);
|
|
2198
|
+
if (collector.level !== "info")
|
|
2199
|
+
root.block(`log syslogd${number} filter`).set("severity", LEVEL[collector.level]);
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
function flowExport(root, device) {
|
|
2203
|
+
const flow = device.flow_export;
|
|
2204
|
+
const collector = flow?.collectors[0];
|
|
2205
|
+
if (!flow || !collector)
|
|
2206
|
+
return;
|
|
2207
|
+
if (flow.protocol === "sflow") {
|
|
2208
|
+
const block = root.block("system sflow").set("collector-ip", collector.address);
|
|
2209
|
+
if (collector.port && collector.port !== 6343)
|
|
2210
|
+
block.set("collector-port", collector.port);
|
|
2211
|
+
}
|
|
2212
|
+
if (flow.protocol === "netflow-v9") {
|
|
2213
|
+
const block = root.block("system netflow").set("collector-ip", collector.address);
|
|
2214
|
+
if (collector.port && collector.port !== 2055)
|
|
2215
|
+
block.set("collector-port", collector.port);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
function dhcpServers(block, device) {
|
|
2219
|
+
Object.values(device.dhcp ?? {}).forEach((server, index) => {
|
|
2220
|
+
const entry = block.entry(index + 1);
|
|
2221
|
+
if (server.lease_time !== undefined && server.lease_time !== 604800)
|
|
2222
|
+
entry.set("lease-time", server.lease_time);
|
|
2223
|
+
entry.set("default-gateway", server.gateway);
|
|
2224
|
+
entry.set("netmask", netmask(split(server.network).len));
|
|
2225
|
+
entry.set("interface", quote(nameOf(device, server.interface)));
|
|
2226
|
+
entry.set("dns-service", "specify");
|
|
2227
|
+
entry.block("ip-range").entry(1).set("start-ip", server.pool[0]).set("end-ip", server.pool[1]);
|
|
2228
|
+
(server.reservations ?? []).forEach((reservation, number) => {
|
|
2229
|
+
const reserved = entry.block("reserved-address").entry(number + 1);
|
|
2230
|
+
reserved.set("ip", reservation.address).set("mac", reservation.mac.toLowerCase());
|
|
2231
|
+
if (reservation.description)
|
|
2232
|
+
reserved.set("description", quote(reservation.description));
|
|
2233
|
+
});
|
|
2234
|
+
(server.dns ?? []).forEach((resolver, number) => entry.set(`dns-server${number + 1}`, resolver));
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
function staticRoutes(ipv4, ipv6, device) {
|
|
2238
|
+
const counters = { ipv4: 0, ipv6: 0 };
|
|
2239
|
+
for (const route of device.routing?.static ?? []) {
|
|
2240
|
+
const family = familyOf2(split(route.prefix).addr);
|
|
2241
|
+
const entry = (family === "ipv4" ? ipv4 : ipv6).entry(++counters[family]);
|
|
2242
|
+
const prefix = network2(route.prefix);
|
|
2243
|
+
if (family === "ipv6" && prefix !== "::/0")
|
|
2244
|
+
entry.set("dst", prefix);
|
|
2245
|
+
if (family === "ipv4" && prefix !== "0.0.0.0/0")
|
|
2246
|
+
entry.set("dst", subnet(prefix));
|
|
2247
|
+
if (route.via)
|
|
2248
|
+
entry.set("gateway", route.via);
|
|
2249
|
+
const outgoing = route.interface ?? (route.via ? interfaceFor(device, route.via, route.vrf) : undefined);
|
|
2250
|
+
if (outgoing && !route.blackhole)
|
|
2251
|
+
entry.set("device", quote(nameOf(device, outgoing)));
|
|
2252
|
+
if (route.distance !== undefined && route.distance !== 10)
|
|
2253
|
+
entry.set("distance", route.distance);
|
|
2254
|
+
if (route.description)
|
|
2255
|
+
entry.set("comment", quote(route.description));
|
|
2256
|
+
if (route.blackhole)
|
|
2257
|
+
entry.set("blackhole", "enable");
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
function helpers(block, device) {
|
|
2261
|
+
let number = 0;
|
|
2262
|
+
for (const helper of device.firewall?.helpers ?? []) {
|
|
2263
|
+
for (const [name, protocol, port] of HELPERS[helper] ?? [])
|
|
2264
|
+
block.entry(++number).set("name", name).set("protocol", protocol).set("port", port);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// src/adapters/devices/fortios/index.ts
|
|
2269
|
+
var SUPPORTED = "6.2";
|
|
2270
|
+
var NAME_LENGTH = { interface: 15, object: 79 };
|
|
2271
|
+
var RESERVED_INTERFACES = new Set(["any", "modem", "ssl.root"]);
|
|
2272
|
+
var PROMPT = /(?:^|\n)[^\n]*[#$] $/;
|
|
2273
|
+
var QUESTION = /\(y\/n\)\s*$/;
|
|
2274
|
+
var BANNER2 = /\(Press 'a' to accept\):\s*$/;
|
|
2275
|
+
var LOGGED_OUT = /(?:^|\n)[^\n]*login: $/;
|
|
2276
|
+
var PAGER = /--More-- \r? *\r/g;
|
|
2277
|
+
var REFUSED = /Command fail\. Return code|Unknown action|parse error|discard the setting|MUST be set|Can not config|isn't supported|(?:^|\n)Invalid /;
|
|
2278
|
+
|
|
2279
|
+
class Cli {
|
|
2280
|
+
channel;
|
|
2281
|
+
newline;
|
|
2282
|
+
prompt = "";
|
|
2283
|
+
constructor(channel, newline = `
|
|
2284
|
+
`) {
|
|
2285
|
+
this.channel = channel;
|
|
2286
|
+
this.newline = newline;
|
|
2287
|
+
}
|
|
2288
|
+
async ready(timeout = 30000) {
|
|
2289
|
+
await this.collect(timeout);
|
|
2290
|
+
}
|
|
2291
|
+
async run(line, timeout = 60000) {
|
|
2292
|
+
this.channel.write(line + this.newline);
|
|
2293
|
+
const output = await this.collect(timeout);
|
|
2294
|
+
const lines = output.split(`
|
|
2295
|
+
`);
|
|
2296
|
+
return lines.slice(1, -1).join(`
|
|
2297
|
+
`);
|
|
2298
|
+
}
|
|
2299
|
+
answer(reply, timeout = 60000) {
|
|
2300
|
+
this.channel.write(reply);
|
|
2301
|
+
return this.collect(timeout);
|
|
2302
|
+
}
|
|
2303
|
+
collect(timeout) {
|
|
2304
|
+
return new Promise((done, fail) => {
|
|
2305
|
+
let raw = "";
|
|
2306
|
+
let settle;
|
|
2307
|
+
const text = () => raw.replace(PAGER, "").replace(/\r/g, "");
|
|
2308
|
+
const stop = () => {
|
|
2309
|
+
clearTimeout(limit);
|
|
2310
|
+
clearTimeout(settle);
|
|
2311
|
+
this.channel.removeListener("data", onData);
|
|
2312
|
+
this.channel.removeListener("close", onClose);
|
|
2313
|
+
};
|
|
2314
|
+
const finish = () => {
|
|
2315
|
+
stop();
|
|
2316
|
+
const output = text();
|
|
2317
|
+
this.prompt = output.split(`
|
|
2318
|
+
`).at(-1) ?? "";
|
|
2319
|
+
done(output);
|
|
2320
|
+
};
|
|
2321
|
+
const onData = (chunk) => {
|
|
2322
|
+
raw += chunk.toString("utf8");
|
|
2323
|
+
if (raw.endsWith("--More-- "))
|
|
2324
|
+
this.channel.write(" ");
|
|
2325
|
+
if (BANNER2.test(raw))
|
|
2326
|
+
this.channel.write("a");
|
|
2327
|
+
clearTimeout(settle);
|
|
2328
|
+
if (LOGGED_OUT.test(text())) {
|
|
2329
|
+
stop();
|
|
2330
|
+
fail(new Error("the device ended the session"));
|
|
2331
|
+
} else if (PROMPT.test(text()) || QUESTION.test(text())) {
|
|
2332
|
+
settle = setTimeout(finish, 300);
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2335
|
+
const onClose = () => {
|
|
2336
|
+
stop();
|
|
2337
|
+
fail(new Error("the device ended the session"));
|
|
2338
|
+
};
|
|
2339
|
+
const limit = setTimeout(() => {
|
|
2340
|
+
stop();
|
|
2341
|
+
fail(new Error(`no prompt within ${timeout / 1000} seconds: ${text().slice(-200)}`));
|
|
2342
|
+
}, timeout);
|
|
2343
|
+
this.channel.on("data", onData);
|
|
2344
|
+
this.channel.on("close", onClose);
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
async function login(device) {
|
|
2349
|
+
const user = device.users?.[device.connection.user];
|
|
2350
|
+
if (!user)
|
|
2351
|
+
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
2352
|
+
return connect({
|
|
2353
|
+
host: device.connection.host,
|
|
2354
|
+
port: device.connection.port,
|
|
2355
|
+
user: device.connection.user,
|
|
2356
|
+
password: resolve(user.password)
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
async function session(device, work) {
|
|
2360
|
+
const client = await login(device);
|
|
2361
|
+
try {
|
|
2362
|
+
const channel = await shell(client, 4000);
|
|
2363
|
+
const cli = new Cli(channel);
|
|
2364
|
+
await cli.ready();
|
|
2365
|
+
const result = await work(cli);
|
|
2366
|
+
channel.end();
|
|
2367
|
+
return result;
|
|
2368
|
+
} finally {
|
|
2369
|
+
client.end();
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
async function checkVersion(cli, device) {
|
|
2373
|
+
const status = await cli.run("get system status");
|
|
2374
|
+
const version = /^Version: \S+ v(\d+)\.(\d+)\.(\d+)/m.exec(status);
|
|
2375
|
+
if (!version)
|
|
2376
|
+
throw new Error(`${device.name} did not say which FortiOS it runs: ${status.slice(0, 200)}`);
|
|
2377
|
+
if (`${version[1]}.${version[2]}` !== SUPPORTED) {
|
|
2378
|
+
throw new Error(`${device.name} runs FortiOS ${version[1]}.${version[2]}.${version[3]}; this adapter renders for FortiOS ${SUPPORTED}`);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
function scrub(text) {
|
|
2382
|
+
const root = parse(text);
|
|
2383
|
+
for (const { block } of root.blocks.get("system snmp community")?.entries.values() ?? []) {
|
|
2384
|
+
if (block.settings.has("name"))
|
|
2385
|
+
block.settings.set("name", '"<redacted>"');
|
|
2386
|
+
}
|
|
2387
|
+
const hide = (block) => {
|
|
2388
|
+
for (const [key, value] of block.settings) {
|
|
2389
|
+
if (value.startsWith("ENC "))
|
|
2390
|
+
block.settings.set(key, "ENC <redacted>");
|
|
2391
|
+
if (key === "private-key")
|
|
2392
|
+
block.settings.set(key, '"<redacted>"');
|
|
2393
|
+
}
|
|
2394
|
+
for (const child of block.blocks.values())
|
|
2395
|
+
hide(child);
|
|
2396
|
+
for (const entry of block.entries.values())
|
|
2397
|
+
hide(entry.block);
|
|
2398
|
+
};
|
|
2399
|
+
hide(root);
|
|
2400
|
+
return print(root);
|
|
2401
|
+
}
|
|
2402
|
+
async function readConfig(cli, device) {
|
|
2403
|
+
await checkVersion(cli, device);
|
|
2404
|
+
const parts = [];
|
|
2405
|
+
for (const section of SECTIONS) {
|
|
2406
|
+
const show = FULL.includes(section) ? "show full-configuration" : "show";
|
|
2407
|
+
parts.push(await cli.run(`${show} ${section}`, 120000));
|
|
2408
|
+
}
|
|
2409
|
+
const text = parts.join(`
|
|
2410
|
+
`);
|
|
2411
|
+
if (!/^config system global$/m.test(text))
|
|
2412
|
+
throw new Error(`${device.name} returned no configuration`);
|
|
2413
|
+
return scrub(clean(text));
|
|
2414
|
+
}
|
|
2415
|
+
function read(device) {
|
|
2416
|
+
return session(device, (cli) => readConfig(cli, device));
|
|
2417
|
+
}
|
|
2418
|
+
function withSecrets(line) {
|
|
2419
|
+
const key = line.startsWith("set private-key ");
|
|
2420
|
+
const filled = line.replace(MARKER, (_, reference) => {
|
|
2421
|
+
const value = valueOfReference(reference);
|
|
2422
|
+
const text = key && !value.includes("-----BEGIN") ? Buffer.from(value, "base64").toString("utf8").trim() : value;
|
|
2423
|
+
return text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
2424
|
+
});
|
|
2425
|
+
return filled.replace(/\r\n?/g, `
|
|
2426
|
+
`).replace(/\?/g, "\x16?");
|
|
2427
|
+
}
|
|
2428
|
+
async function leave(cli) {
|
|
2429
|
+
for (let level = 0;level < 10 && /\(.*\) [#$] $/.test(cli.prompt); level++)
|
|
2430
|
+
await cli.run("end");
|
|
2431
|
+
}
|
|
2432
|
+
async function saving(cli, mode, seconds) {
|
|
2433
|
+
const lines = ["config system global", `set cfg-save ${mode}`, ...seconds ? [`set cfg-revert-timeout ${seconds}`] : [], "end"];
|
|
2434
|
+
for (const line of lines) {
|
|
2435
|
+
const answer = await cli.run(line);
|
|
2436
|
+
if (REFUSED.test(answer))
|
|
2437
|
+
throw new Error(`the device refused ${line}: ${answer.trim().slice(0, 300)}`);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
async function configure(cli, plan, guard) {
|
|
2441
|
+
const minutes = guard.seconds / 60;
|
|
2442
|
+
await saving(cli, "revert", guard.seconds);
|
|
2443
|
+
const restore = async (reason) => {
|
|
2444
|
+
await leave(cli).catch(() => {
|
|
2445
|
+
return;
|
|
2446
|
+
});
|
|
2447
|
+
await cli.run("execute cfg reload").catch(() => {
|
|
2448
|
+
return;
|
|
2449
|
+
});
|
|
2450
|
+
return new Reloaded(`${reason}
|
|
2451
|
+
the device reloads its saved configuration, which undoes what was sent, at the latest in ${minutes} minutes`);
|
|
2452
|
+
};
|
|
2453
|
+
for (const step of plan.steps) {
|
|
2454
|
+
for (const line of step.send) {
|
|
2455
|
+
const answer = await cli.run(withSecrets(line)).catch((error) => error);
|
|
2456
|
+
if (answer instanceof Error)
|
|
2457
|
+
throw await restore(`${step.title}: ${line}
|
|
2458
|
+
${answer.message}`);
|
|
2459
|
+
if (/System will reload/.test(answer)) {
|
|
2460
|
+
throw new Reloaded(`${step.title}: ${line}
|
|
2461
|
+
the rollback time ran out while the plan was sent, and the device is reloading its saved configuration`);
|
|
2462
|
+
}
|
|
2463
|
+
if (REFUSED.test(answer))
|
|
2464
|
+
throw await restore(`${step.title}: ${line}
|
|
2465
|
+
device said: ${answer.trim().slice(0, 300)}`);
|
|
2466
|
+
if (QUESTION.test(cli.prompt)) {
|
|
2467
|
+
await cli.answer("n").catch(() => {
|
|
2468
|
+
return;
|
|
2469
|
+
});
|
|
2470
|
+
throw await restore(`${step.title}: ${line}
|
|
2471
|
+
device asked: ${cli.prompt.trim()}`);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
console.log(` sent; the device reloads its saved configuration in ${minutes} minutes unless access is proved`);
|
|
2476
|
+
try {
|
|
2477
|
+
await guard.prove();
|
|
2478
|
+
} catch (error) {
|
|
2479
|
+
throw await restore(`a fresh login failed: ${error.message}`);
|
|
2480
|
+
}
|
|
2481
|
+
const saved = await cli.run("execute cfg save", 120000);
|
|
2482
|
+
if (!/config saved|no config to be saved/.test(saved)) {
|
|
2483
|
+
throw new Error(`access was proved but the save failed; the device reverts in ${minutes} minutes:
|
|
2484
|
+
${saved.trim().slice(0, 300)}`);
|
|
2485
|
+
}
|
|
2486
|
+
await saving(cli, "automatic");
|
|
2487
|
+
console.log(" logged in again, saved");
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
class Reloaded extends Error {
|
|
2491
|
+
}
|
|
2492
|
+
var RELOAD = { attempts: 20, interval: 15000 };
|
|
2493
|
+
async function settle(device) {
|
|
2494
|
+
for (let attempt = 0;attempt < RELOAD.attempts; attempt++) {
|
|
2495
|
+
await new Promise((done) => setTimeout(done, RELOAD.interval));
|
|
2496
|
+
const settled = await session(device, async (cli) => {
|
|
2497
|
+
if (!/no config to be reloaded/.test(await cli.run("execute cfg reload")))
|
|
2498
|
+
return false;
|
|
2499
|
+
await saving(cli, "automatic");
|
|
2500
|
+
return true;
|
|
2501
|
+
}).catch(() => false);
|
|
2502
|
+
if (settled)
|
|
2503
|
+
return "the device is back and saves changes on its own again";
|
|
2504
|
+
}
|
|
2505
|
+
return "the device did not come back in time and still reverts every change; set `cfg-save automatic` under `config system global`";
|
|
2506
|
+
}
|
|
2507
|
+
async function apply(device, _network, plan, options) {
|
|
2508
|
+
try {
|
|
2509
|
+
await session(device, async (cli) => {
|
|
2510
|
+
await checkVersion(cli, device);
|
|
2511
|
+
await configure(cli, plan, {
|
|
2512
|
+
seconds: options.rollback * 60,
|
|
2513
|
+
prove: async () => {
|
|
2514
|
+
const proof = await login(device);
|
|
2515
|
+
proof.end();
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
});
|
|
2519
|
+
} catch (error) {
|
|
2520
|
+
if (!(error instanceof Reloaded))
|
|
2521
|
+
throw error;
|
|
2522
|
+
throw new Error(`${error.message}
|
|
2523
|
+
${await settle(device)}`);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function fillLines(set, family, prefixes) {
|
|
2527
|
+
const list = new Block;
|
|
2528
|
+
prefixes.forEach((prefix, index) => {
|
|
2529
|
+
const rule = list.entry(index + 1);
|
|
2530
|
+
rule.set(family === "ipv4" ? "prefix" : "prefix6", family === "ipv4" ? subnet(network2(prefix)) : network2(prefix));
|
|
2531
|
+
if (split(network2(prefix)).len < BITS[family])
|
|
2532
|
+
rule.set("le", BITS[family]);
|
|
2533
|
+
});
|
|
2534
|
+
return [
|
|
2535
|
+
`config ${PREFIX_LIST[family]}`,
|
|
2536
|
+
`edit ${quote(set)}`,
|
|
2537
|
+
"config rule",
|
|
2538
|
+
"purge",
|
|
2539
|
+
...linesOf(list).map((line) => line.trim()),
|
|
2540
|
+
"end",
|
|
2541
|
+
"next",
|
|
2542
|
+
"end"
|
|
2543
|
+
];
|
|
2544
|
+
}
|
|
2545
|
+
async function fill(device, _network, set, family, prefixes) {
|
|
2546
|
+
await session(device, async (cli) => {
|
|
2547
|
+
await checkVersion(cli, device);
|
|
2548
|
+
for (const line of fillLines(set, family, prefixes)) {
|
|
2549
|
+
let answer = await cli.run(line);
|
|
2550
|
+
if (QUESTION.test(cli.prompt) && line === "purge")
|
|
2551
|
+
answer = await cli.answer("y");
|
|
2552
|
+
if (REFUSED.test(answer))
|
|
2553
|
+
throw new Error(`${set}: ${line}
|
|
2554
|
+
device said: ${answer.trim().slice(0, 300)}`);
|
|
2555
|
+
}
|
|
2556
|
+
console.log(` ${set}: ${prefixes.length} prefixes on the device`);
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
function unsupported(device) {
|
|
2560
|
+
const found = [];
|
|
2561
|
+
found.push(...systemGaps(device));
|
|
2562
|
+
found.push(...managementGaps(device));
|
|
2563
|
+
found.push(...interfaceGaps(device));
|
|
2564
|
+
found.push(...staticGaps(device));
|
|
2565
|
+
found.push(...routingGaps(device));
|
|
2566
|
+
found.push(...firewallGaps(device));
|
|
2567
|
+
found.push(...serviceGaps(device));
|
|
2568
|
+
if (device.stp)
|
|
2569
|
+
found.push("stp, ports are routed");
|
|
2570
|
+
if (Object.keys(device.vrfs ?? {}).length > VRFS)
|
|
2571
|
+
found.push(`vrfs, at most ${VRFS}`);
|
|
2572
|
+
for (const [name, vrf] of Object.entries(device.vrfs ?? {})) {
|
|
2573
|
+
if (vrf.description)
|
|
2574
|
+
found.push(`vrfs.${name}.description, the platform numbers a VRF and keeps no description`);
|
|
2575
|
+
}
|
|
2576
|
+
return found;
|
|
2577
|
+
}
|
|
2578
|
+
function systemGaps(device) {
|
|
2579
|
+
const found = [];
|
|
2580
|
+
const system = device.system ?? {};
|
|
2581
|
+
if (system.timezone && !zoneOf(system.timezone))
|
|
2582
|
+
found.push(`system.timezone ${system.timezone}, the platform has no zone for it`);
|
|
2583
|
+
if (system.release_channel)
|
|
2584
|
+
found.push("system.release_channel, the platform's firmware has no release channel to follow");
|
|
2585
|
+
if (system.transceiver_monitoring !== undefined)
|
|
2586
|
+
found.push("system.transceiver_monitoring, the platform reads optics without being told");
|
|
2587
|
+
if (system.ip?.syn_cookies)
|
|
2588
|
+
found.push("system.ip.syn_cookies, the platform has a SYN proxy per DoS policy, not SYN cookies");
|
|
2589
|
+
const local = system.logging?.local;
|
|
2590
|
+
if (local?.files !== undefined || local?.lines_per_file !== undefined) {
|
|
2591
|
+
found.push("system.logging.local.files and lines_per_file, logs kept in memory do not rotate into files");
|
|
2592
|
+
}
|
|
2593
|
+
if ((system.logging?.remote?.length ?? 0) > COLLECTORS)
|
|
2594
|
+
found.push(`system.logging.remote, at most ${COLLECTORS} collectors`);
|
|
2595
|
+
const servers = system.dns?.servers ?? [];
|
|
2596
|
+
for (const family of ["ipv4", "ipv6"]) {
|
|
2597
|
+
if (servers.filter((server) => familyOf2(server) === family).length > RESOLVERS.device) {
|
|
2598
|
+
found.push(`system.dns.servers, at most ${RESOLVERS.device} of each family`);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
return found;
|
|
2602
|
+
}
|
|
2603
|
+
function managementGaps(device) {
|
|
2604
|
+
const found = [];
|
|
2605
|
+
const management = device.management ?? {};
|
|
2606
|
+
if (management.api || management.api_tls)
|
|
2607
|
+
found.push("management.api and api_tls, the API is served by management.https");
|
|
2608
|
+
if (management.ftp)
|
|
2609
|
+
found.push("management.ftp, the platform has no FTP service");
|
|
2610
|
+
if (management.console)
|
|
2611
|
+
found.push("management.console, the console logs in with the device's users");
|
|
2612
|
+
if (management.privilege_password)
|
|
2613
|
+
found.push("management.privilege_password, the platform has no privileged mode");
|
|
2614
|
+
if (management.native?.interfaces?.length)
|
|
2615
|
+
found.push("management.native.interfaces, FortiManager access runs over IP only");
|
|
2616
|
+
if (management.native?.port)
|
|
2617
|
+
found.push("management.native.port, FortiManager access listens on 541");
|
|
2618
|
+
if (management.ssh?.auth_retries)
|
|
2619
|
+
found.push("management.ssh.auth_retries, the platform counts failed logins per source, not per connection");
|
|
2620
|
+
for (const [name, user] of Object.entries(management.snmp?.users ?? {})) {
|
|
2621
|
+
if (user.auth === "sha256")
|
|
2622
|
+
found.push(`management.snmp.users.${name}.auth sha256, the platform authenticates with SHA1 or MD5`);
|
|
2623
|
+
}
|
|
2624
|
+
for (const [name, user] of Object.entries(device.users ?? {})) {
|
|
2625
|
+
if ((user.ssh_keys?.length ?? 0) > SSH_KEYS)
|
|
2626
|
+
found.push(`users.${name}.ssh_keys, at most ${SSH_KEYS}`);
|
|
2627
|
+
}
|
|
2628
|
+
return found;
|
|
2629
|
+
}
|
|
2630
|
+
function portNames(device) {
|
|
2631
|
+
return new Set(portsOf(device.platform, device.model).map((port) => spell(device.platform, device.model, port)));
|
|
2632
|
+
}
|
|
2633
|
+
function interfaceGaps(device) {
|
|
2634
|
+
const found = [];
|
|
2635
|
+
const vlanInterfaces = new Set(Object.values(device.interfaces ?? {}).flatMap((iface) => iface.type === "vlan" ? [iface.vlan] : []));
|
|
2636
|
+
for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
|
|
2637
|
+
if (vlan.description)
|
|
2638
|
+
found.push(`vlans.${name}.description, a VLAN interface holds its own`);
|
|
2639
|
+
}
|
|
2640
|
+
for (const [name, port] of Object.entries(device.ports ?? {})) {
|
|
2641
|
+
if (!port)
|
|
2642
|
+
continue;
|
|
2643
|
+
const where = `ports.${name}`;
|
|
2644
|
+
const speedClass = name.split("-")[0];
|
|
2645
|
+
if (port.speed && !SPEEDS[speedClass]?.[port.speed])
|
|
2646
|
+
found.push(`${where}.speed ${port.speed}, not in a ${speedClass} cage`);
|
|
2647
|
+
for (const field of ["access_vlan", "native_vlan", "stp", "storm_control"]) {
|
|
2648
|
+
if (port[field] !== undefined)
|
|
2649
|
+
found.push(`${where}.${field}, ports are routed`);
|
|
2650
|
+
}
|
|
2651
|
+
if (port.hardware_offload !== undefined)
|
|
2652
|
+
found.push(`${where}.hardware_offload, offload is decided per policy, which can match many interfaces`);
|
|
2653
|
+
if (port.lag) {
|
|
2654
|
+
for (const field of ["addresses", "mtu", "vrrp", "vrf", "lldp", "trunk_vlans", "ipv6_ra", "acl"]) {
|
|
2655
|
+
if (port[field] !== undefined)
|
|
2656
|
+
found.push(`${where}.${field}, a LAG member takes its settings from the LAG`);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
const acl = port.acl ? device.acls?.[port.acl] : undefined;
|
|
2660
|
+
if (acl)
|
|
2661
|
+
found.push(...aclGaps(`acls.${port.acl}`, acl));
|
|
2662
|
+
found.push(...trunkGaps(where, name, port.trunk_vlans, vlanInterfaces, device));
|
|
2663
|
+
found.push(...routedGaps(where, port, false));
|
|
2664
|
+
}
|
|
2665
|
+
const ports = portNames(device);
|
|
2666
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
2667
|
+
const where = `interfaces.${name}`;
|
|
2668
|
+
if (iface.type === "wireguard") {
|
|
2669
|
+
found.push(`${where}, a wireguard: the platform has no WireGuard`);
|
|
2670
|
+
continue;
|
|
2671
|
+
}
|
|
2672
|
+
if (name.length > NAME_LENGTH.interface)
|
|
2673
|
+
found.push(`${where}, a name longer than ${NAME_LENGTH.interface} characters`);
|
|
2674
|
+
if (ports.has(name) || RESERVED_INTERFACES.has(name))
|
|
2675
|
+
found.push(`${where}, a name the platform gives a port of its own`);
|
|
2676
|
+
if (iface.hardware_offload !== undefined)
|
|
2677
|
+
found.push(`${where}.hardware_offload, offload is decided per policy, which can match many interfaces`);
|
|
2678
|
+
if (iface.type === "lag") {
|
|
2679
|
+
if (iface.id !== undefined)
|
|
2680
|
+
found.push(`${where}.id, the platform names a LAG by its key`);
|
|
2681
|
+
for (const field of ["access_vlan", "native_vlan", "stp", "storm_control"]) {
|
|
2682
|
+
if (iface[field] !== undefined)
|
|
2683
|
+
found.push(`${where}.${field}, LAGs are routed`);
|
|
2684
|
+
}
|
|
2685
|
+
found.push(...trunkGaps(where, name, iface.trunk_vlans, vlanInterfaces, device));
|
|
2686
|
+
}
|
|
2687
|
+
if (iface.type === "vlan" && !vlanParent(device, iface.vlan)) {
|
|
2688
|
+
found.push(`${where}, VLAN ${iface.vlan} must be carried tagged by exactly one port or LAG`);
|
|
2689
|
+
}
|
|
2690
|
+
if (iface.type === "loopback") {
|
|
2691
|
+
if (iface.mtu !== undefined)
|
|
2692
|
+
found.push(`${where}.mtu, a loopback has no MTU of its own`);
|
|
2693
|
+
if (iface.vrrp?.length)
|
|
2694
|
+
found.push(`${where}.vrrp, a loopback runs no VRRP`);
|
|
2695
|
+
}
|
|
2696
|
+
if (iface.type === "gre" || iface.type === "vxlan") {
|
|
2697
|
+
if (!holderOf(device, iface.local))
|
|
2698
|
+
found.push(`${where}.local, no interface holds ${iface.local}`);
|
|
2699
|
+
if (familyOf2(iface.local) !== familyOf2(iface.remote))
|
|
2700
|
+
found.push(`${where}, endpoints of two families`);
|
|
2701
|
+
if (iface.vrrp?.length)
|
|
2702
|
+
found.push(`${where}.vrrp, on a tunnel`);
|
|
2703
|
+
if (iface.ipv6_ra)
|
|
2704
|
+
found.push(`${where}.ipv6_ra, on a tunnel`);
|
|
2705
|
+
}
|
|
2706
|
+
if (iface.type === "gre") {
|
|
2707
|
+
for (const entry of iface.addresses ?? []) {
|
|
2708
|
+
if (typeof entry !== "string" && familyOf2(host2(entry.address)) === "ipv6")
|
|
2709
|
+
found.push(`${where}.addresses, a peer on an IPv6 address`);
|
|
2710
|
+
}
|
|
2711
|
+
const ipv4 = (iface.addresses ?? []).filter((entry) => familyOf2(host2(typeof entry === "string" ? entry : entry.address)) === "ipv4");
|
|
2712
|
+
if (ipv4.some((entry) => typeof entry === "string"))
|
|
2713
|
+
found.push(`${where}.addresses, an IPv4 address on a tunnel takes its peer`);
|
|
2714
|
+
if (ipv4.length > 1)
|
|
2715
|
+
found.push(`${where}.addresses, one IPv4 address`);
|
|
2716
|
+
}
|
|
2717
|
+
found.push(...routedGaps(where, iface, iface.type === "gre"));
|
|
2718
|
+
}
|
|
2719
|
+
if (device.lldp && typeof device.lldp === "object") {
|
|
2720
|
+
for (const name of device.lldp.interfaces) {
|
|
2721
|
+
const settings = device.interfaces?.[name];
|
|
2722
|
+
if (settings && settings.type !== "lag" && settings.type !== "vlan")
|
|
2723
|
+
found.push(`lldp.interfaces ${name}, a ${settings.type}`);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
return found;
|
|
2727
|
+
}
|
|
2728
|
+
function trunkGaps(where, name, vlans, withInterface, device) {
|
|
2729
|
+
const found = [];
|
|
2730
|
+
for (const vlan of vlans ?? []) {
|
|
2731
|
+
if (!withInterface.has(vlan)) {
|
|
2732
|
+
found.push(`${where}.trunk_vlans ${vlan}, a VLAN with no interface here: the platform routes and does not switch`);
|
|
2733
|
+
} else if (vlanParent(device, vlan) !== name) {
|
|
2734
|
+
found.push(`${where}.trunk_vlans ${vlan}, which another port or LAG also carries`);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
return found;
|
|
2738
|
+
}
|
|
2739
|
+
function routedGaps(where, settings, tunnel) {
|
|
2740
|
+
const found = [];
|
|
2741
|
+
if (!tunnel && settings.addresses?.some((entry) => typeof entry !== "string"))
|
|
2742
|
+
found.push(`${where}.addresses with a peer, which only a tunnel takes`);
|
|
2743
|
+
for (const group of settings.vrrp ?? []) {
|
|
2744
|
+
if (familyOf2(group.address) === "ipv6")
|
|
2745
|
+
found.push(`${where}.vrrp ${group.id}, IPv6: the platform runs VRRP for IPv4`);
|
|
2746
|
+
}
|
|
2747
|
+
return found;
|
|
2748
|
+
}
|
|
2749
|
+
function aclGaps(where, rules) {
|
|
2750
|
+
const found = [];
|
|
2751
|
+
if (acceptsBeforeDrop(rules))
|
|
2752
|
+
found.push(`${where}, an accept before a drop: the platform's ACLs only drop`);
|
|
2753
|
+
rules.forEach((rule, index) => {
|
|
2754
|
+
if (rule.rate)
|
|
2755
|
+
found.push(`${where}[${index}].rate`);
|
|
2756
|
+
if (rule.match?.vlan)
|
|
2757
|
+
found.push(`${where}[${index}].match.vlan, a port's ACL sees no VLAN`);
|
|
2758
|
+
});
|
|
2759
|
+
return found;
|
|
2760
|
+
}
|
|
2761
|
+
function staticGaps(device) {
|
|
2762
|
+
const found = [];
|
|
2763
|
+
const seen = new Set;
|
|
2764
|
+
for (const route of device.routing?.static ?? []) {
|
|
2765
|
+
const where = `routing.static ${route.prefix}`;
|
|
2766
|
+
if (route.vrf && route.blackhole)
|
|
2767
|
+
found.push(`${where}, a blackhole in a VRF: a route takes the VRF of the interface it leaves through`);
|
|
2768
|
+
if (route.vrf && route.interface && settingsOf(device, route.interface)?.vrf !== route.vrf) {
|
|
2769
|
+
found.push(`${where}.interface, which is not in VRF ${route.vrf}`);
|
|
2770
|
+
}
|
|
2771
|
+
if (route.via && !route.interface && !route.blackhole && !interfaceFor(device, route.via, route.vrf)) {
|
|
2772
|
+
found.push(`${where} via ${route.via}, no interface's own network in its table holds the next hop`);
|
|
2773
|
+
}
|
|
2774
|
+
const key = [
|
|
2775
|
+
split(route.prefix).addr,
|
|
2776
|
+
split(route.prefix).len,
|
|
2777
|
+
route.via,
|
|
2778
|
+
route.interface,
|
|
2779
|
+
route.blackhole,
|
|
2780
|
+
route.distance,
|
|
2781
|
+
route.vrf
|
|
2782
|
+
].join(" ");
|
|
2783
|
+
if (seen.has(key))
|
|
2784
|
+
found.push(`${where}, the same route twice`);
|
|
2785
|
+
seen.add(key);
|
|
2786
|
+
}
|
|
2787
|
+
return found;
|
|
2788
|
+
}
|
|
2789
|
+
function firewallGaps(device) {
|
|
2790
|
+
const found = [];
|
|
2791
|
+
const fw = device.firewall;
|
|
2792
|
+
if (!fw)
|
|
2793
|
+
return found;
|
|
2794
|
+
if (fw.filter?.output)
|
|
2795
|
+
found.push("firewall.filter.output, the platform filters no traffic it sends itself");
|
|
2796
|
+
if (fw.untracked)
|
|
2797
|
+
found.push("firewall.untracked, every session is tracked");
|
|
2798
|
+
for (const helper of fw.helpers ?? []) {
|
|
2799
|
+
if (!HELPERS[helper])
|
|
2800
|
+
found.push(`firewall.helpers ${helper}, the platform has no helper for it`);
|
|
2801
|
+
}
|
|
2802
|
+
for (const chain of ["forward", "input"]) {
|
|
2803
|
+
(fw.filter?.[chain]?.rules ?? []).forEach((rule, index) => found.push(...ruleGaps2(`firewall.filter.${chain}[${index}]`, rule, chain, device)));
|
|
2804
|
+
}
|
|
2805
|
+
(fw.nat?.source ?? []).forEach((rule, index) => {
|
|
2806
|
+
const where = `firewall.nat.source[${index}]`;
|
|
2807
|
+
const match = rule.match;
|
|
2808
|
+
if (familiesOf(device, match).includes("ipv6") && (match?.family === "ipv6" || match?.src || match?.dst)) {
|
|
2809
|
+
found.push(`${where}, IPv6: central NAT translates IPv4`);
|
|
2810
|
+
}
|
|
2811
|
+
if (rule.action === "dnat")
|
|
2812
|
+
found.push(`${where}.action dnat, in source NAT`);
|
|
2813
|
+
if (rule.action === "snat" && familyOf2(rule.to) === "ipv6")
|
|
2814
|
+
found.push(`${where}.to, IPv6: central NAT translates IPv4`);
|
|
2815
|
+
if (match?.dst_port !== undefined)
|
|
2816
|
+
found.push(`${where}.match.dst_port, central NAT matches the source port only`);
|
|
2817
|
+
if (Array.isArray(match?.src_port))
|
|
2818
|
+
found.push(`${where}.match.src_port, one port or range`);
|
|
2819
|
+
if (rule.action === "snat" && rule.to_port !== undefined && match?.src_port === undefined)
|
|
2820
|
+
found.push(`${where}.to_port, without match.src_port`);
|
|
2821
|
+
if (match?.src && match.src_set || match?.dst && match.dst_set)
|
|
2822
|
+
found.push(`${where}.match, a prefix and a set on one side`);
|
|
2823
|
+
if (match?.state || match?.tcp_flags)
|
|
2824
|
+
found.push(`${where}.match.state and tcp_flags`);
|
|
2825
|
+
});
|
|
2826
|
+
(fw.nat?.destination ?? []).forEach((rule, index) => {
|
|
2827
|
+
const where = `firewall.nat.destination[${index}]`;
|
|
2828
|
+
const match = rule.match;
|
|
2829
|
+
if (rule.action !== "dnat") {
|
|
2830
|
+
found.push(`${where}.action ${rule.action}, in destination NAT`);
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
if (!match?.dst || split(match.dst.includes("/") ? match.dst : `${match.dst}/32`).len !== 32) {
|
|
2834
|
+
found.push(`${where}.match.dst, one IPv4 address the virtual IP answers on`);
|
|
2835
|
+
}
|
|
2836
|
+
const sources = match?.src ? [match.src] : match?.src_set ? fw.address_sets?.[match.src_set] ?? [] : [];
|
|
2837
|
+
if (familyOf2(rule.to) === "ipv6" || sources.some((source) => familyOf2(host2(source)) === "ipv6"))
|
|
2838
|
+
found.push(`${where}, IPv6`);
|
|
2839
|
+
if (match?.dst_port !== undefined && Array.isArray(match.dst_port))
|
|
2840
|
+
found.push(`${where}.match.dst_port, one port or range`);
|
|
2841
|
+
if (match?.dst_port !== undefined && match.protocol !== "tcp" && match.protocol !== "udp") {
|
|
2842
|
+
found.push(`${where}.match.protocol, tcp or udp with a port`);
|
|
2843
|
+
}
|
|
2844
|
+
if (match?.dst_port === undefined && (match?.protocol || rule.to_port)) {
|
|
2845
|
+
found.push(`${where}.match.dst_port, a protocol or port is forwarded by its port`);
|
|
2846
|
+
}
|
|
2847
|
+
if (match?.src && match.src_set)
|
|
2848
|
+
found.push(`${where}.match, a prefix and a set on one side`);
|
|
2849
|
+
for (const field of ["dst_set", "src_port", "out_interface", "state", "tcp_flags", "family"]) {
|
|
2850
|
+
if (match?.[field] !== undefined)
|
|
2851
|
+
found.push(`${where}.match.${field}`);
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
for (const name of Object.keys(fw.address_sets ?? {})) {
|
|
2855
|
+
if (name.length > NAME_LENGTH.object)
|
|
2856
|
+
found.push(`firewall.address_sets.${name}, a name longer than ${NAME_LENGTH.object} characters`);
|
|
2857
|
+
if (name.startsWith("circuit-"))
|
|
2858
|
+
found.push(`firewall.address_sets.${name}, a name Circuit keeps for the sources of management`);
|
|
2859
|
+
}
|
|
2860
|
+
return found;
|
|
2861
|
+
}
|
|
2862
|
+
function ruleGaps2(where, rule, chain, device) {
|
|
2863
|
+
const found = [];
|
|
2864
|
+
const match = rule.match;
|
|
2865
|
+
const state = match?.state ?? [];
|
|
2866
|
+
if (state.some((one) => one === "invalid" || one === "untracked"))
|
|
2867
|
+
found.push(`${where}.match.state ${state.join(", ")}`);
|
|
2868
|
+
else if (state.length && rule.action !== "accept")
|
|
2869
|
+
found.push(`${where}.match.state, on a rule that does not accept`);
|
|
2870
|
+
if (match?.tcp_flags)
|
|
2871
|
+
found.push(`${where}.match.tcp_flags, the platform matches no TCP flags`);
|
|
2872
|
+
if (match?.src && match.src_set || match?.dst && match.dst_set)
|
|
2873
|
+
found.push(`${where}.match, a prefix and a set on one side`);
|
|
2874
|
+
if (isImplied(rule))
|
|
2875
|
+
return found;
|
|
2876
|
+
const families = familiesOf(device, match);
|
|
2877
|
+
if (!families.length)
|
|
2878
|
+
found.push(`${where}, its addresses share no family`);
|
|
2879
|
+
if (match?.src && match.dst && familyOf2(host2(match.src)) !== familyOf2(host2(match.dst))) {
|
|
2880
|
+
found.push(`${where}, a source and destination of two families`);
|
|
2881
|
+
}
|
|
2882
|
+
for (const family of families) {
|
|
2883
|
+
if (serviceOf(match, family).name.length > NAME_LENGTH.object)
|
|
2884
|
+
found.push(`${where}.match ports, too many for one service`);
|
|
2885
|
+
}
|
|
2886
|
+
if (chain === "input") {
|
|
2887
|
+
if (match?.out_interface)
|
|
2888
|
+
found.push(`${where}.match.out_interface, on traffic to the device`);
|
|
2889
|
+
if (rule.action === "reject")
|
|
2890
|
+
found.push(`${where}.action reject, the platform drops traffic to itself silently`);
|
|
2891
|
+
if (rule.offload)
|
|
2892
|
+
found.push(`${where}.offload, on traffic to the device`);
|
|
2893
|
+
}
|
|
2894
|
+
return found;
|
|
2895
|
+
}
|
|
2896
|
+
function serviceGaps(device) {
|
|
2897
|
+
const found = [];
|
|
2898
|
+
for (const [name, server] of Object.entries(device.dhcp ?? {})) {
|
|
2899
|
+
if (familyOf2(host2(server.network)) === "ipv6")
|
|
2900
|
+
found.push(`dhcp.${name}.network, IPv6: DHCPv6 hands out no gateway`);
|
|
2901
|
+
if ((server.dns?.length ?? 0) > RESOLVERS.dhcp)
|
|
2902
|
+
found.push(`dhcp.${name}.dns, at most ${RESOLVERS.dhcp}`);
|
|
2903
|
+
}
|
|
2904
|
+
const flow = device.flow_export;
|
|
2905
|
+
if (flow?.protocol === "ipfix")
|
|
2906
|
+
found.push("flow_export.protocol ipfix, the platform exports sFlow and NetFlow 9");
|
|
2907
|
+
if (flow && flow.collectors.length > 1)
|
|
2908
|
+
found.push("flow_export.collectors, one collector");
|
|
2909
|
+
for (const name of flow?.interfaces ?? []) {
|
|
2910
|
+
const type = device.interfaces?.[name]?.type;
|
|
2911
|
+
if (type === "gre" || type === "vxlan")
|
|
2912
|
+
found.push(`flow_export.interfaces ${name}, a tunnel: the platform samples the interface it leaves through`);
|
|
2913
|
+
}
|
|
2914
|
+
if (flow?.protocol === "netflow-v9" && flow.sampling !== undefined)
|
|
2915
|
+
found.push("flow_export.sampling, NetFlow here exports every flow");
|
|
2916
|
+
if (flow?.protocol === "sflow" && (flow.sampling === undefined || flow.sampling < 10 || flow.sampling > 99999)) {
|
|
2917
|
+
found.push("flow_export.sampling, required and from 10 to 99999: the platform samples");
|
|
2918
|
+
}
|
|
2919
|
+
return found;
|
|
2920
|
+
}
|
|
2921
|
+
var fortios = {
|
|
2922
|
+
extension: ".conf",
|
|
2923
|
+
unsupported,
|
|
2924
|
+
render,
|
|
2925
|
+
read,
|
|
2926
|
+
plan,
|
|
2927
|
+
apply,
|
|
2928
|
+
fill
|
|
2929
|
+
};
|
|
2930
|
+
|
|
205
2931
|
// src/adapters/devices/derive.ts
|
|
206
2932
|
function lagIds(device) {
|
|
207
2933
|
const numbers = new Map;
|
|
@@ -262,7 +2988,7 @@ var KEY_STATEMENT = {
|
|
|
262
2988
|
var SNMP_AUTH = { sha1: "authentication-sha", sha256: "authentication-sha256" };
|
|
263
2989
|
var SNMP_PRIVACY = { aes128: "privacy-aes128", des: "privacy-des" };
|
|
264
2990
|
var SNMP_GROUP = "circuit";
|
|
265
|
-
var
|
|
2991
|
+
var LEVEL2 = {
|
|
266
2992
|
emergency: "emergency",
|
|
267
2993
|
alert: "alert",
|
|
268
2994
|
critical: "critical",
|
|
@@ -287,7 +3013,7 @@ var SERVICE_PORTS = {
|
|
|
287
3013
|
native: ["tcp", 830]
|
|
288
3014
|
};
|
|
289
3015
|
var BFD_INTERVAL = 300;
|
|
290
|
-
function
|
|
3016
|
+
function quote2(value) {
|
|
291
3017
|
if (/^[\w.:/@-]+$/.test(value))
|
|
292
3018
|
return value;
|
|
293
3019
|
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
@@ -383,13 +3109,13 @@ function stormProfile(settings) {
|
|
|
383
3109
|
const left = STORM_KINDS.filter(([kind]) => !storm[kind]).map(([, statement]) => statement);
|
|
384
3110
|
return { name: `${kinds.join("-")}-${first.percent}-percent`, percent: first.percent, left };
|
|
385
3111
|
}
|
|
386
|
-
function
|
|
3112
|
+
function render2(device) {
|
|
387
3113
|
const out = new Statements;
|
|
388
3114
|
const names = new Names(device);
|
|
389
3115
|
system(out, device);
|
|
390
|
-
|
|
3116
|
+
users2(out, device);
|
|
391
3117
|
services(out, device);
|
|
392
|
-
|
|
3118
|
+
snmp2(out, device);
|
|
393
3119
|
vlans(out, device, names);
|
|
394
3120
|
ports(out, device, names);
|
|
395
3121
|
lags(out, device, names);
|
|
@@ -397,25 +3123,25 @@ function render(device) {
|
|
|
397
3123
|
stormProfiles(out, device);
|
|
398
3124
|
servicesFilter(out, device, names);
|
|
399
3125
|
vrfs(out, device, names);
|
|
400
|
-
|
|
3126
|
+
staticRoutes2(out, device, names);
|
|
401
3127
|
spanningTree(out, device, names);
|
|
402
3128
|
lldp(out, device, names);
|
|
403
|
-
|
|
3129
|
+
ospf2(out, device, names);
|
|
404
3130
|
dhcpRelay(out, device, names);
|
|
405
|
-
|
|
3131
|
+
dhcpServers2(out, device, names);
|
|
406
3132
|
routerAdvertisements(out, device, names);
|
|
407
|
-
|
|
3133
|
+
acls2(out, device, names);
|
|
408
3134
|
renderBgp(out, device);
|
|
409
3135
|
sflow(out, device, names);
|
|
410
3136
|
return out.text();
|
|
411
3137
|
}
|
|
412
3138
|
function system(out, device) {
|
|
413
|
-
out.set("system host-name",
|
|
3139
|
+
out.set("system host-name", quote2(device.name));
|
|
414
3140
|
const system = device.system ?? {};
|
|
415
3141
|
if (system.timezone)
|
|
416
3142
|
out.set("system time-zone", system.timezone);
|
|
417
3143
|
if (system.banner)
|
|
418
|
-
out.set("system login message",
|
|
3144
|
+
out.set("system login message", quote2(system.banner));
|
|
419
3145
|
for (const server of system.dns?.servers ?? [])
|
|
420
3146
|
out.set("system name-server", server);
|
|
421
3147
|
for (const server of system.ntp?.servers ?? [])
|
|
@@ -426,27 +3152,27 @@ function system(out, device) {
|
|
|
426
3152
|
}
|
|
427
3153
|
const local = system.logging?.local;
|
|
428
3154
|
if (local) {
|
|
429
|
-
out.set("system syslog file messages any",
|
|
3155
|
+
out.set("system syslog file messages any", LEVEL2[local.level]);
|
|
430
3156
|
if (local.files !== undefined)
|
|
431
3157
|
out.set("system syslog file messages archive files", local.files);
|
|
432
3158
|
}
|
|
433
3159
|
for (const remote of system.logging?.remote ?? []) {
|
|
434
|
-
out.set("system syslog host", remote.address, "any",
|
|
3160
|
+
out.set("system syslog host", remote.address, "any", LEVEL2[remote.level]);
|
|
435
3161
|
if (remote.port !== undefined && remote.port !== 514)
|
|
436
3162
|
out.set("system syslog host", remote.address, "port", remote.port);
|
|
437
3163
|
}
|
|
438
3164
|
}
|
|
439
|
-
function
|
|
3165
|
+
function users2(out, device) {
|
|
440
3166
|
for (const [name, user] of Object.entries(device.users ?? {})) {
|
|
441
3167
|
const login = name === "root" ? "system root-authentication" : `system login user ${name} authentication`;
|
|
442
3168
|
if (name !== "root") {
|
|
443
3169
|
out.set(`system login user ${name} class`, USER_CLASS[user.role]);
|
|
444
3170
|
if (user.description)
|
|
445
|
-
out.set(`system login user ${name} full-name`,
|
|
3171
|
+
out.set(`system login user ${name} full-name`, quote2(user.description));
|
|
446
3172
|
}
|
|
447
3173
|
out.set(login, "plain-text-password-value", marker(user.password));
|
|
448
3174
|
for (const key of user.ssh_keys ?? [])
|
|
449
|
-
out.set(login, KEY_STATEMENT[key.split(" ")[0]],
|
|
3175
|
+
out.set(login, KEY_STATEMENT[key.split(" ")[0]], quote2(key));
|
|
450
3176
|
}
|
|
451
3177
|
}
|
|
452
3178
|
function services(out, device) {
|
|
@@ -483,14 +3209,14 @@ function services(out, device) {
|
|
|
483
3209
|
out.set(statement, "port", settings.port);
|
|
484
3210
|
}
|
|
485
3211
|
}
|
|
486
|
-
function
|
|
3212
|
+
function snmp2(out, device) {
|
|
487
3213
|
const snmp = device.management?.snmp;
|
|
488
3214
|
if (!snmp)
|
|
489
3215
|
return;
|
|
490
3216
|
if (snmp.contact)
|
|
491
|
-
out.set("snmp contact",
|
|
3217
|
+
out.set("snmp contact", quote2(snmp.contact));
|
|
492
3218
|
if (snmp.location)
|
|
493
|
-
out.set("snmp location",
|
|
3219
|
+
out.set("snmp location", quote2(snmp.location));
|
|
494
3220
|
if (snmp.community)
|
|
495
3221
|
out.set("snmp community", marker(snmp.community), "authorization read-only");
|
|
496
3222
|
if (!snmp.users)
|
|
@@ -508,7 +3234,7 @@ function vlans(out, device, names) {
|
|
|
508
3234
|
for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
|
|
509
3235
|
out.set(`vlans ${name} vlan-id`, vlan.id);
|
|
510
3236
|
if (vlan.description)
|
|
511
|
-
out.set(`vlans ${name} description`,
|
|
3237
|
+
out.set(`vlans ${name} description`, quote2(vlan.description));
|
|
512
3238
|
}
|
|
513
3239
|
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
514
3240
|
if (iface.type === "vlan")
|
|
@@ -566,7 +3292,7 @@ function ports(out, device, names) {
|
|
|
566
3292
|
continue;
|
|
567
3293
|
}
|
|
568
3294
|
if (settings.description)
|
|
569
|
-
out.set(`interfaces ${name} description`,
|
|
3295
|
+
out.set(`interfaces ${name} description`, quote2(settings.description));
|
|
570
3296
|
if (settings.lag) {
|
|
571
3297
|
out.set(`interfaces ${name} ether-options 802.3ad`, `ae${names.lags.get(settings.lag)}`);
|
|
572
3298
|
continue;
|
|
@@ -586,7 +3312,7 @@ function lags(out, device, names) {
|
|
|
586
3312
|
const physical = names.physical(name);
|
|
587
3313
|
highest = Math.max(highest, names.lags.get(name));
|
|
588
3314
|
if (lag.description)
|
|
589
|
-
out.set(`interfaces ${physical} description`,
|
|
3315
|
+
out.set(`interfaces ${physical} description`, quote2(lag.description));
|
|
590
3316
|
if (lag.mode !== "static")
|
|
591
3317
|
out.set(`interfaces ${physical} aggregated-ether-options lacp active`);
|
|
592
3318
|
if (lag.mtu !== undefined)
|
|
@@ -605,7 +3331,7 @@ function routedInterfaces(out, device, names) {
|
|
|
605
3331
|
const [physical, number] = unit.split(".");
|
|
606
3332
|
const prefix = `interfaces ${physical} unit ${number}`;
|
|
607
3333
|
if (iface.description)
|
|
608
|
-
out.set(`${prefix} description`,
|
|
3334
|
+
out.set(`${prefix} description`, quote2(iface.description));
|
|
609
3335
|
if (iface.type === "gre") {
|
|
610
3336
|
out.set(`${prefix} tunnel source`, iface.local);
|
|
611
3337
|
out.set(`${prefix} tunnel destination`, iface.remote);
|
|
@@ -700,7 +3426,7 @@ function vrfs(out, device, names) {
|
|
|
700
3426
|
for (const [name, vrf] of Object.entries(device.vrfs ?? {})) {
|
|
701
3427
|
out.set(`routing-instances ${name} instance-type virtual-router`);
|
|
702
3428
|
if (vrf.description)
|
|
703
|
-
out.set(`routing-instances ${name} description`,
|
|
3429
|
+
out.set(`routing-instances ${name} description`, quote2(vrf.description));
|
|
704
3430
|
}
|
|
705
3431
|
const routed = [...Object.entries(device.ports ?? {}), ...Object.entries(device.interfaces ?? {})];
|
|
706
3432
|
for (const [name, settings] of routed) {
|
|
@@ -708,7 +3434,7 @@ function vrfs(out, device, names) {
|
|
|
708
3434
|
out.set(`routing-instances ${settings.vrf} interface`, names.unit(name));
|
|
709
3435
|
}
|
|
710
3436
|
}
|
|
711
|
-
function
|
|
3437
|
+
function staticRoutes2(out, device, names) {
|
|
712
3438
|
if (device.routing?.router_id)
|
|
713
3439
|
out.set("routing-options router-id", device.routing.router_id);
|
|
714
3440
|
for (const route of device.routing?.static ?? []) {
|
|
@@ -777,7 +3503,7 @@ function lldp(out, device, names) {
|
|
|
777
3503
|
out.set("protocols lldp interface", names.port(port));
|
|
778
3504
|
}
|
|
779
3505
|
}
|
|
780
|
-
function
|
|
3506
|
+
function ospf2(out, device, names) {
|
|
781
3507
|
const ospf = device.routing?.ospf;
|
|
782
3508
|
if (!ospf)
|
|
783
3509
|
return;
|
|
@@ -852,8 +3578,8 @@ function aclFamily(rule) {
|
|
|
852
3578
|
return address ? familyOf2(host2(address)) : undefined;
|
|
853
3579
|
}
|
|
854
3580
|
var nextHeader = (protocol) => protocol === "icmp" ? "icmp6" : protocol;
|
|
855
|
-
var ruleName = (label, description) =>
|
|
856
|
-
function
|
|
3581
|
+
var ruleName = (label, description) => quote2(description ? `${label} ${description}` : label);
|
|
3582
|
+
function acls2(out, device, names) {
|
|
857
3583
|
const switched = new Set;
|
|
858
3584
|
const routed = new Set;
|
|
859
3585
|
for (const [port, settings] of Object.entries(device.ports ?? {})) {
|
|
@@ -945,7 +3671,7 @@ function routedFilter(out, device, acl, family) {
|
|
|
945
3671
|
});
|
|
946
3672
|
out.set(filter, "term circuit-default then accept");
|
|
947
3673
|
}
|
|
948
|
-
function
|
|
3674
|
+
function dhcpServers2(out, device, names) {
|
|
949
3675
|
for (const [name, server] of Object.entries(device.dhcp ?? {})) {
|
|
950
3676
|
out.set(`system services dhcp-local-server group ${name} interface`, names.unit(server.interface));
|
|
951
3677
|
const pool = `access address-assignment pool ${name} family inet`;
|
|
@@ -978,17 +3704,17 @@ function routerAdvertisements(out, device, names) {
|
|
|
978
3704
|
}
|
|
979
3705
|
|
|
980
3706
|
// src/adapters/devices/junos/bgp.ts
|
|
981
|
-
var
|
|
3707
|
+
var REJECT_ALL2 = "circuit-reject-all";
|
|
982
3708
|
var ORIGIN = "circuit-bgp-origin";
|
|
983
3709
|
var RPKI_GROUP = "circuit";
|
|
984
3710
|
var GENERATED = "# generated ";
|
|
985
3711
|
var FAMILY = { ipv4: "inet", ipv6: "inet6" };
|
|
986
3712
|
var VALIDATION = { valid: "valid", invalid: "invalid", "not-found": "unknown" };
|
|
987
|
-
function
|
|
3713
|
+
function settingsOf2(device, neighbor) {
|
|
988
3714
|
const group = neighbor.group ? device.routing?.bgp?.groups?.[neighbor.group] : undefined;
|
|
989
3715
|
return { ...group, ...neighbor };
|
|
990
3716
|
}
|
|
991
|
-
var
|
|
3717
|
+
var slug2 = (value) => value.replace(/\*/g, "any").replace(/[^A-Za-z0-9]+/g, "-");
|
|
992
3718
|
var member = (community, large) => large ? `large:${community}` : community;
|
|
993
3719
|
function renderBgp(out, device) {
|
|
994
3720
|
prefixSets(out, device);
|
|
@@ -1001,12 +3727,12 @@ function renderBgp(out, device) {
|
|
|
1001
3727
|
let rejectAll = false;
|
|
1002
3728
|
let exports = false;
|
|
1003
3729
|
for (const [name, neighbor] of Object.entries(bgp.neighbors ?? {})) {
|
|
1004
|
-
const settings =
|
|
3730
|
+
const settings = settingsOf2(device, neighbor);
|
|
1005
3731
|
const group = `protocols bgp group ${name}`;
|
|
1006
3732
|
out.set(group, "type", settings.remote_as === bgp.asn ? "internal" : "external");
|
|
1007
3733
|
out.set(group, "peer-as", settings.remote_as);
|
|
1008
3734
|
if (settings.description)
|
|
1009
|
-
out.set(group, "description",
|
|
3735
|
+
out.set(group, "description", quote2(settings.description));
|
|
1010
3736
|
if (settings.local_address)
|
|
1011
3737
|
out.set(group, "local-address", settings.local_address);
|
|
1012
3738
|
if (settings.multihop)
|
|
@@ -1019,13 +3745,13 @@ function renderBgp(out, device) {
|
|
|
1019
3745
|
out.set(group, "authentication-key", marker(settings.password));
|
|
1020
3746
|
if (settings.bfd)
|
|
1021
3747
|
out.set(group, "bfd-liveness-detection minimum-interval", BFD_INTERVAL);
|
|
1022
|
-
out.set(group, "import", settings.import ??
|
|
3748
|
+
out.set(group, "import", settings.import ?? REJECT_ALL2);
|
|
1023
3749
|
if (settings.export) {
|
|
1024
3750
|
exports = true;
|
|
1025
3751
|
out.set(group, "export", ORIGIN);
|
|
1026
3752
|
out.set(group, "export", settings.export);
|
|
1027
3753
|
} else
|
|
1028
|
-
out.set(group, "export",
|
|
3754
|
+
out.set(group, "export", REJECT_ALL2);
|
|
1029
3755
|
for (const policy of [settings.import, settings.export]) {
|
|
1030
3756
|
if (policy)
|
|
1031
3757
|
used.add(policy);
|
|
@@ -1043,7 +3769,7 @@ function renderBgp(out, device) {
|
|
|
1043
3769
|
out.set(group, "neighbor", neighbor.address);
|
|
1044
3770
|
}
|
|
1045
3771
|
if (rejectAll)
|
|
1046
|
-
out.set(`policy-options policy-statement ${
|
|
3772
|
+
out.set(`policy-options policy-statement ${REJECT_ALL2} then reject`);
|
|
1047
3773
|
if (exports)
|
|
1048
3774
|
origin(out, device);
|
|
1049
3775
|
for (const policy of [...used].sort())
|
|
@@ -1077,7 +3803,7 @@ function rpki(out, device) {
|
|
|
1077
3803
|
out.set(`routing-options validation group ${RPKI_GROUP} session`, server.address, "port", server.port);
|
|
1078
3804
|
}
|
|
1079
3805
|
}
|
|
1080
|
-
function
|
|
3806
|
+
function familyOfRule2(device, rule) {
|
|
1081
3807
|
const match = rule.match;
|
|
1082
3808
|
if (match?.family)
|
|
1083
3809
|
return match.family;
|
|
@@ -1090,24 +3816,24 @@ function familyOfRule(device, rule) {
|
|
|
1090
3816
|
return familyOf2(host2(set.prefixes[0]));
|
|
1091
3817
|
return;
|
|
1092
3818
|
}
|
|
1093
|
-
function
|
|
3819
|
+
function flatten2(device, policy, label, seen) {
|
|
1094
3820
|
const rules = device.policies?.[policy] ?? [];
|
|
1095
3821
|
const flat = [];
|
|
1096
3822
|
rules.forEach((rule, index) => {
|
|
1097
3823
|
const here = label ? `${label}.${index + 1}` : `${index + 1}`;
|
|
1098
3824
|
if (rule.call && !seen.includes(rule.call))
|
|
1099
|
-
flat.push(...
|
|
3825
|
+
flat.push(...flatten2(device, rule.call, here, [...seen, rule.call]));
|
|
1100
3826
|
if (rule.match || rule.set || rule.action)
|
|
1101
3827
|
flat.push({ label: here, rule });
|
|
1102
3828
|
});
|
|
1103
3829
|
return flat;
|
|
1104
3830
|
}
|
|
1105
|
-
var termName = (label, rule) =>
|
|
3831
|
+
var termName = (label, rule) => quote2(rule.description ? `${label} ${rule.description}` : label);
|
|
1106
3832
|
function policyStatement(out, device, policy) {
|
|
1107
3833
|
const statement = `policy-options policy-statement ${policy}`;
|
|
1108
|
-
const flat =
|
|
3834
|
+
const flat = flatten2(device, policy, "", [policy]);
|
|
1109
3835
|
for (const { label, rule } of flat) {
|
|
1110
|
-
const family =
|
|
3836
|
+
const family = familyOfRule2(device, rule);
|
|
1111
3837
|
const split = !family && (rule.set?.next_hop || rule.match?.prefix_length) ? ["ipv4", "ipv6"] : [family];
|
|
1112
3838
|
for (const part of split) {
|
|
1113
3839
|
const name = split.length > 1 ? termName(`${label}-${part === "ipv4" ? "v4" : "v6"}`, rule) : termName(label, rule);
|
|
@@ -1141,7 +3867,7 @@ function term(out, device, term, rule, family, splitFamily) {
|
|
|
1141
3867
|
if (match.as_path_length) {
|
|
1142
3868
|
const { min = 0, max } = match.as_path_length;
|
|
1143
3869
|
const name = `circuit-path-length-${min}-${max ?? "any"}`;
|
|
1144
|
-
out.set(`policy-options as-path ${name}`,
|
|
3870
|
+
out.set(`policy-options as-path ${name}`, quote2(`.{${min},${max ?? ""}}`));
|
|
1145
3871
|
from.push(["as-path", name]);
|
|
1146
3872
|
}
|
|
1147
3873
|
if (match.rpki)
|
|
@@ -1158,7 +3884,7 @@ function term(out, device, term, rule, family, splitFamily) {
|
|
|
1158
3884
|
then.push(["metric", set.med]);
|
|
1159
3885
|
if (set.prepend !== undefined) {
|
|
1160
3886
|
const asn = device.routing.bgp.asn;
|
|
1161
|
-
then.push(["as-path-prepend",
|
|
3887
|
+
then.push(["as-path-prepend", quote2(Array(set.prepend).fill(asn).join(" "))]);
|
|
1162
3888
|
}
|
|
1163
3889
|
const removed = [
|
|
1164
3890
|
...(set.communities ? ["*:*"] : set.remove_communities ?? []).map((pattern) => member(pattern, false)),
|
|
@@ -1190,9 +3916,9 @@ function term(out, device, term, rule, family, splitFamily) {
|
|
|
1190
3916
|
out.set(term);
|
|
1191
3917
|
}
|
|
1192
3918
|
function community(out, purpose, members) {
|
|
1193
|
-
const name = `circuit-${purpose}-${
|
|
3919
|
+
const name = `circuit-${purpose}-${slug2(members.join(" "))}`;
|
|
1194
3920
|
for (const value of members)
|
|
1195
|
-
out.set(`policy-options community ${name} members`,
|
|
3921
|
+
out.set(`policy-options community ${name} members`, quote2(value));
|
|
1196
3922
|
return name;
|
|
1197
3923
|
}
|
|
1198
3924
|
|
|
@@ -1200,10 +3926,10 @@ function community(out, purpose, members) {
|
|
|
1200
3926
|
var SUPPORTED_MAJOR = 21;
|
|
1201
3927
|
var DEVICE_FACTS = [/^set version /, /^set system login user \S+ uid \d+$/];
|
|
1202
3928
|
var SYSTEM_ACCOUNTS = new Set(["bind", "daemon", "kmem", "nobody", "operator", "sshd", "tty", "uucp"]);
|
|
1203
|
-
function
|
|
3929
|
+
function wordsOf2(statement) {
|
|
1204
3930
|
return statement.match(/"(?:[^"\\]|\\.)*"|\S+/g) ?? [];
|
|
1205
3931
|
}
|
|
1206
|
-
var
|
|
3932
|
+
var bare2 = (word) => word.startsWith('"') ? word.slice(1, -1).replace(/\\(.)/g, "$1") : word;
|
|
1207
3933
|
var SECRETS = [
|
|
1208
3934
|
[/ (?:encrypted-password|plain-text-password-value) (?:"[^"]*"|\S+)/, " password <secret>"],
|
|
1209
3935
|
[/ (?:authentication-key|authentication-password) (?:"[^"]*"|\S+)/, " authentication-key <secret>"],
|
|
@@ -1214,15 +3940,15 @@ function keyOf2(statement) {
|
|
|
1214
3940
|
let canonical = statement;
|
|
1215
3941
|
for (const [pattern, replacement] of SECRETS)
|
|
1216
3942
|
canonical = canonical.replace(pattern, replacement);
|
|
1217
|
-
return
|
|
3943
|
+
return wordsOf2(canonical).slice(1).map(bare2);
|
|
1218
3944
|
}
|
|
1219
|
-
function
|
|
3945
|
+
function scrub2(text) {
|
|
1220
3946
|
return text.replace(/ ## SECRET-DATA$/gm, "").replace(/((?:encrypted-password|authentication-key|privacy-key) )(?:"[^"]*"|\S+)/g, "$1<redacted>").replace(/^(set snmp community )(?:"[^"]*"|\S+)/gm, "$1<redacted>").replace(/"\$\d\$[^"]*"/g, "<redacted>");
|
|
1221
3947
|
}
|
|
1222
|
-
function
|
|
3948
|
+
function clean2(output) {
|
|
1223
3949
|
const statements = output.replace(/\r/g, "").split(`
|
|
1224
3950
|
`).map((line) => line.trimEnd()).filter((line) => /^(set|deactivate) /.test(line)).filter((line) => !DEVICE_FACTS.some((fact) => fact.test(line)));
|
|
1225
|
-
return
|
|
3951
|
+
return scrub2(statements.join(`
|
|
1226
3952
|
`) + `
|
|
1227
3953
|
`);
|
|
1228
3954
|
}
|
|
@@ -1286,13 +4012,13 @@ function orderedBlockOf(path) {
|
|
|
1286
4012
|
return;
|
|
1287
4013
|
}
|
|
1288
4014
|
var startsWith = (path, prefix) => prefix.every((word, index) => path[index] === word);
|
|
1289
|
-
function
|
|
4015
|
+
function plan2(desired, current, options) {
|
|
1290
4016
|
const lines = (text, verb) => text.split(`
|
|
1291
4017
|
`).filter((line) => line.startsWith(`${verb} `));
|
|
1292
4018
|
const wanted = lines(desired, "set");
|
|
1293
4019
|
const deactivated = lines(current, "deactivate");
|
|
1294
4020
|
const generated = desired.split(`
|
|
1295
|
-
`).filter((line) => line.startsWith(GENERATED)).map((line) =>
|
|
4021
|
+
`).filter((line) => line.startsWith(GENERATED)).map((line) => wordsOf2(line.slice(GENERATED.length)).map(bare2));
|
|
1296
4022
|
const printed = lines(current, "set").filter((line) => !DEVICE_FACTS.some((fact) => fact.test(line)));
|
|
1297
4023
|
const isGenerated = (line) => generated.some((path) => keyOf2(line).length > path.length && startsWith(keyOf2(line), path));
|
|
1298
4024
|
const existing = printed.filter((line) => !isGenerated(line));
|
|
@@ -1329,7 +4055,7 @@ function plan(desired, current, options) {
|
|
|
1329
4055
|
rewritten.push(block);
|
|
1330
4056
|
const section = sectionFor(block);
|
|
1331
4057
|
if (existingHere.length)
|
|
1332
|
-
section.remove.push(`delete ${
|
|
4058
|
+
section.remove.push(`delete ${wordsOf2(existingHere[0]).slice(1, 1 + block.length).join(" ")}`);
|
|
1333
4059
|
section.show.push(...existingHere.map((line) => `- ${line}`), ...wantedHere.map((line) => `+ ${line}`));
|
|
1334
4060
|
section.add.push(...wantedHere);
|
|
1335
4061
|
}
|
|
@@ -1351,22 +4077,22 @@ function plan(desired, current, options) {
|
|
|
1351
4077
|
const hidden = path.indexOf("<secret>");
|
|
1352
4078
|
if (hidden >= 0 && hidden < length && !NAMED_LISTS.has(path[hidden - 1]))
|
|
1353
4079
|
length = hidden;
|
|
1354
|
-
removed.push(
|
|
4080
|
+
removed.push(wordsOf2(line).slice(1, 1 + length));
|
|
1355
4081
|
}
|
|
1356
|
-
const isInside = (path, other) => other.length < path.length && other.every((word, index) =>
|
|
4082
|
+
const isInside = (path, other) => other.length < path.length && other.every((word, index) => bare2(path[index]) === bare2(word));
|
|
1357
4083
|
const removals = removed.filter((path, index) => !removed.some((other) => isInside(path, other)) && removed.findIndex((other) => join(other) === join(path)) === index);
|
|
1358
|
-
const isRemoved = (line) => removals.some((path) => startsWith(keyOf2(line), path.map(
|
|
4084
|
+
const isRemoved = (line) => removals.some((path) => startsWith(keyOf2(line), path.map(bare2)));
|
|
1359
4085
|
for (const path of removals) {
|
|
1360
|
-
const section = sectionFor(path.map(
|
|
4086
|
+
const section = sectionFor(path.map(bare2));
|
|
1361
4087
|
section.remove.push(`delete ${path.join(" ")}`);
|
|
1362
|
-
const under = existing.filter((line) => startsWith(keyOf2(line), path.map(
|
|
4088
|
+
const under = existing.filter((line) => startsWith(keyOf2(line), path.map(bare2)));
|
|
1363
4089
|
section.show.push(...under.map((line) => `- ${line}`));
|
|
1364
4090
|
}
|
|
1365
4091
|
for (const line of deactivated) {
|
|
1366
|
-
const path =
|
|
1367
|
-
if (!wantedUnder(path.map(
|
|
4092
|
+
const path = wordsOf2(line).slice(1);
|
|
4093
|
+
if (!wantedUnder(path.map(bare2)).length)
|
|
1368
4094
|
continue;
|
|
1369
|
-
const section = sectionFor(path.map(
|
|
4095
|
+
const section = sectionFor(path.map(bare2));
|
|
1370
4096
|
section.activate.push(`activate ${path.join(" ")}`);
|
|
1371
4097
|
section.show.push(`+ activate ${path.join(" ")}`);
|
|
1372
4098
|
}
|
|
@@ -1389,7 +4115,7 @@ function plan(desired, current, options) {
|
|
|
1389
4115
|
const steps = [...sections].filter(([, section]) => section.show.length).map(([title, section]) => ({ title, show: section.show, send: [...section.activate, ...section.remove, ...section.add] }));
|
|
1390
4116
|
return { steps, problems: [] };
|
|
1391
4117
|
}
|
|
1392
|
-
async function
|
|
4118
|
+
async function login2(device) {
|
|
1393
4119
|
const user = device.users?.[device.connection.user];
|
|
1394
4120
|
if (!user)
|
|
1395
4121
|
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
@@ -1400,7 +4126,7 @@ async function login(device) {
|
|
|
1400
4126
|
password: resolve(user.password)
|
|
1401
4127
|
});
|
|
1402
4128
|
}
|
|
1403
|
-
async function
|
|
4129
|
+
async function checkVersion2(client, device) {
|
|
1404
4130
|
const version = await exec(client, "show version | match Junos:");
|
|
1405
4131
|
const release = /Junos: (\d+)\.(\S+)/.exec(version);
|
|
1406
4132
|
if (!release)
|
|
@@ -1409,23 +4135,23 @@ async function checkVersion(client, device) {
|
|
|
1409
4135
|
throw new Error(`${device.name} runs Junos ${release[1]}.${release[2]}; this adapter renders for Junos ${SUPPORTED_MAJOR}`);
|
|
1410
4136
|
}
|
|
1411
4137
|
}
|
|
1412
|
-
async function
|
|
1413
|
-
const client = await
|
|
4138
|
+
async function read2(device) {
|
|
4139
|
+
const client = await login2(device);
|
|
1414
4140
|
try {
|
|
1415
|
-
await
|
|
4141
|
+
await checkVersion2(client, device);
|
|
1416
4142
|
const output = await exec(client, "show configuration | display set | no-more", 180000);
|
|
1417
4143
|
if (!/^set /m.test(output))
|
|
1418
4144
|
throw new Error(`${device.name} returned no configuration`);
|
|
1419
|
-
return
|
|
4145
|
+
return clean2(output);
|
|
1420
4146
|
} finally {
|
|
1421
4147
|
client.end();
|
|
1422
4148
|
}
|
|
1423
4149
|
}
|
|
1424
|
-
var
|
|
4150
|
+
var PROMPT2 = /[>#%] $/;
|
|
1425
4151
|
async function run(channel, line, timeout = 60000) {
|
|
1426
4152
|
channel.write(`${line}
|
|
1427
4153
|
`);
|
|
1428
|
-
const output = await readUntil(channel, (text) =>
|
|
4154
|
+
const output = await readUntil(channel, (text) => PROMPT2.test(text.replace(/\r/g, "")), timeout, 300);
|
|
1429
4155
|
return output.replace(/\r/g, "");
|
|
1430
4156
|
}
|
|
1431
4157
|
var EDIT_PROMPT = /\[edit[^\]\n]*\]\n[^\n]*# $/;
|
|
@@ -1436,7 +4162,7 @@ async function edit(channel, line, timeout = 60000) {
|
|
|
1436
4162
|
return output.replace(/\r/g, "");
|
|
1437
4163
|
}
|
|
1438
4164
|
async function openCli(channel) {
|
|
1439
|
-
const banner = await readUntil(channel, (text) =>
|
|
4165
|
+
const banner = await readUntil(channel, (text) => PROMPT2.test(text.replace(/\r/g, "")), 20000, 500);
|
|
1440
4166
|
if (/% $/.test(banner.replace(/\r/g, "")))
|
|
1441
4167
|
await run(channel, "cli");
|
|
1442
4168
|
await run(channel, "set cli screen-length 0");
|
|
@@ -1444,18 +4170,18 @@ async function openCli(channel) {
|
|
|
1444
4170
|
await run(channel, "set cli complete-on-space off");
|
|
1445
4171
|
}
|
|
1446
4172
|
async function named(channel, line) {
|
|
1447
|
-
const words =
|
|
4173
|
+
const words = wordsOf2(line).slice(1);
|
|
1448
4174
|
const at = words.indexOf("<redacted>");
|
|
1449
4175
|
if (at < 0)
|
|
1450
4176
|
return [line];
|
|
1451
4177
|
const shown = await edit(channel, `show ${words.slice(0, at - 1).join(" ")} | display set`);
|
|
1452
4178
|
const inList = (statement) => statement[0] === "set" && words.slice(0, at).every((word, index) => statement[index + 1] === word);
|
|
1453
4179
|
const names = new Set(shown.split(`
|
|
1454
|
-
`).map(
|
|
4180
|
+
`).map(wordsOf2).filter(inList).map((statement) => statement[at + 1]));
|
|
1455
4181
|
return [...names].map((name) => `delete ${[...words.slice(0, at), name, ...words.slice(at + 1)].join(" ")}`);
|
|
1456
4182
|
}
|
|
1457
|
-
var
|
|
1458
|
-
async function
|
|
4183
|
+
var REFUSED2 = /^terminal:\d+:\(\d+\) (?!warning)|^error: /m;
|
|
4184
|
+
async function configure2(channel, plan, guard) {
|
|
1459
4185
|
const sent = plan.steps.flatMap((step) => step.send);
|
|
1460
4186
|
const entered = await run(channel, "configure exclusive");
|
|
1461
4187
|
if (!/Entering configuration mode/.test(entered) || /^error: /m.test(entered)) {
|
|
@@ -1486,7 +4212,7 @@ async function configure(channel, plan, guard) {
|
|
|
1486
4212
|
`);
|
|
1487
4213
|
const loaded = await edit(channel, "\x04", 300000);
|
|
1488
4214
|
const refused = loaded.split(`
|
|
1489
|
-
`).filter((line) =>
|
|
4215
|
+
`).filter((line) => REFUSED2.test(line));
|
|
1490
4216
|
if (refused.length)
|
|
1491
4217
|
throw await discard(`the device refused the plan, nothing was applied:
|
|
1492
4218
|
${refused.join(`
|
|
@@ -1537,16 +4263,16 @@ async function configure(channel, plan, guard) {
|
|
|
1537
4263
|
await run(channel, "exit configuration-mode");
|
|
1538
4264
|
console.log(" logged in again, commit confirmed");
|
|
1539
4265
|
}
|
|
1540
|
-
async function
|
|
1541
|
-
const client = await
|
|
4266
|
+
async function apply2(device, _network, plan, options) {
|
|
4267
|
+
const client = await login2(device);
|
|
1542
4268
|
try {
|
|
1543
|
-
await
|
|
4269
|
+
await checkVersion2(client, device);
|
|
1544
4270
|
const channel = await shell(client);
|
|
1545
4271
|
await openCli(channel);
|
|
1546
|
-
await
|
|
4272
|
+
await configure2(channel, plan, {
|
|
1547
4273
|
minutes: options.rollback,
|
|
1548
4274
|
prove: async () => {
|
|
1549
|
-
const proof = await
|
|
4275
|
+
const proof = await login2(device);
|
|
1550
4276
|
proof.end();
|
|
1551
4277
|
}
|
|
1552
4278
|
});
|
|
@@ -1555,7 +4281,7 @@ async function apply(device, _network, plan, options) {
|
|
|
1555
4281
|
client.end();
|
|
1556
4282
|
}
|
|
1557
4283
|
}
|
|
1558
|
-
function
|
|
4284
|
+
function unsupported2(device) {
|
|
1559
4285
|
const found = [];
|
|
1560
4286
|
const names = new Names(device);
|
|
1561
4287
|
const users = device.users ?? {};
|
|
@@ -1636,7 +4362,7 @@ function unsupported(device) {
|
|
|
1636
4362
|
const acl = port.acl ? device.acls?.[port.acl] : undefined;
|
|
1637
4363
|
if (acl?.some((rule) => rule.match?.vlan))
|
|
1638
4364
|
found.push(`acls.${port.acl} matching a VLAN, on port ${name}`);
|
|
1639
|
-
found.push(...
|
|
4365
|
+
found.push(...routedGaps2(`ports.${name}`, port));
|
|
1640
4366
|
found.push(...switchedGaps(`ports.${name}`, port, device));
|
|
1641
4367
|
}
|
|
1642
4368
|
const loopbackTables = new Set;
|
|
@@ -1658,7 +4384,7 @@ function unsupported(device) {
|
|
|
1658
4384
|
if (iface.mtu > smallest)
|
|
1659
4385
|
found.push(`interfaces.${name}.mtu ${iface.mtu}, above the ${smallest} its VLAN's ports carry`);
|
|
1660
4386
|
}
|
|
1661
|
-
found.push(...
|
|
4387
|
+
found.push(...routedGaps2(`interfaces.${name}`, iface));
|
|
1662
4388
|
if (iface.type === "lag")
|
|
1663
4389
|
found.push(...switchedGaps(`interfaces.${name}`, iface, device));
|
|
1664
4390
|
if (iface.type === "loopback") {
|
|
@@ -1741,7 +4467,7 @@ function bgpGaps(device) {
|
|
|
1741
4467
|
const found = [];
|
|
1742
4468
|
const bgp = device.routing?.bgp;
|
|
1743
4469
|
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
1744
|
-
const settings =
|
|
4470
|
+
const settings = settingsOf2(device, neighbor);
|
|
1745
4471
|
if (settings.role)
|
|
1746
4472
|
found.push(`routing.bgp.neighbors.${name}.role, Junos 21 has no BGP roles`);
|
|
1747
4473
|
if (familyOf2(neighbor.address) === "ipv6" && settings.families?.includes("ipv4")) {
|
|
@@ -1765,7 +4491,7 @@ function bgpGaps(device) {
|
|
|
1765
4491
|
}
|
|
1766
4492
|
return found;
|
|
1767
4493
|
}
|
|
1768
|
-
function
|
|
4494
|
+
function routedGaps2(where, settings) {
|
|
1769
4495
|
const found = [];
|
|
1770
4496
|
const addresses = (settings.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
|
|
1771
4497
|
if (settings.addresses?.some((entry) => typeof entry !== "string"))
|
|
@@ -1795,13 +4521,13 @@ function fillPlan(set, prefixes) {
|
|
|
1795
4521
|
const send = [`delete ${list}`, `set ${list}`, ...prefixes.map((prefix) => `set ${list} ${prefix}`)];
|
|
1796
4522
|
return { steps: [{ title: list, show: [], send }], problems: [] };
|
|
1797
4523
|
}
|
|
1798
|
-
async function
|
|
1799
|
-
const client = await
|
|
4524
|
+
async function fill2(device, _network, set, _family, prefixes) {
|
|
4525
|
+
const client = await login2(device);
|
|
1800
4526
|
try {
|
|
1801
|
-
await
|
|
4527
|
+
await checkVersion2(client, device);
|
|
1802
4528
|
const channel = await shell(client);
|
|
1803
4529
|
await openCli(channel);
|
|
1804
|
-
await
|
|
4530
|
+
await configure2(channel, fillPlan(set, prefixes));
|
|
1805
4531
|
channel.end();
|
|
1806
4532
|
console.log(` ${set}: ${prefixes.length} prefixes on the device`);
|
|
1807
4533
|
} finally {
|
|
@@ -1810,12 +4536,12 @@ async function fill(device, _network, set, _family, prefixes) {
|
|
|
1810
4536
|
}
|
|
1811
4537
|
var junos = {
|
|
1812
4538
|
extension: ".conf",
|
|
1813
|
-
unsupported,
|
|
1814
|
-
render,
|
|
1815
|
-
read,
|
|
1816
|
-
plan,
|
|
1817
|
-
apply,
|
|
1818
|
-
fill
|
|
4539
|
+
unsupported: unsupported2,
|
|
4540
|
+
render: render2,
|
|
4541
|
+
read: read2,
|
|
4542
|
+
plan: plan2,
|
|
4543
|
+
apply: apply2,
|
|
4544
|
+
fill: fill2
|
|
1819
4545
|
};
|
|
1820
4546
|
|
|
1821
4547
|
// src/core/time.ts
|
|
@@ -1874,7 +4600,7 @@ function head(line) {
|
|
|
1874
4600
|
const words = line.split(" ");
|
|
1875
4601
|
return words.slice(0, Math.max(1, words.length - 1)).join(" ");
|
|
1876
4602
|
}
|
|
1877
|
-
function
|
|
4603
|
+
function compare2(desired, current, dialect) {
|
|
1878
4604
|
const canon = dialect.canon ?? ((line) => line);
|
|
1879
4605
|
const keyOf = (entry) => [...entry.path, canon(entry.line)].join("\x00");
|
|
1880
4606
|
const pathKey = (path) => path.join("\x00");
|
|
@@ -1959,7 +4685,7 @@ function steps(groups, dialect, around = nothingAround) {
|
|
|
1959
4685
|
});
|
|
1960
4686
|
}
|
|
1961
4687
|
var STORED_CREDENTIAL = /(password (?:irreversible-)?cipher|community read cipher|^snmp-server community)( +)\S+/gm;
|
|
1962
|
-
var
|
|
4688
|
+
var scrub3 = (text) => text.replace(STORED_CREDENTIAL, "$1$2<redacted>");
|
|
1963
4689
|
|
|
1964
4690
|
// src/adapters/devices/raisecom-ros/index.ts
|
|
1965
4691
|
var DEVICE_FACTS2 = [
|
|
@@ -2046,7 +4772,7 @@ function vlanList(ids) {
|
|
|
2046
4772
|
}
|
|
2047
4773
|
return ranges.join(",");
|
|
2048
4774
|
}
|
|
2049
|
-
function
|
|
4775
|
+
function render3(device) {
|
|
2050
4776
|
const lines = [];
|
|
2051
4777
|
const vlans = device.vlans ?? {};
|
|
2052
4778
|
const vlanId = (name) => vlans[name].id;
|
|
@@ -2236,7 +4962,7 @@ async function open(device) {
|
|
|
2236
4962
|
await send(channel, "terminal page-break disable", 15000, 1000);
|
|
2237
4963
|
return { channel, close };
|
|
2238
4964
|
}
|
|
2239
|
-
async function
|
|
4965
|
+
async function read3(device) {
|
|
2240
4966
|
const { channel, close } = await open(device);
|
|
2241
4967
|
try {
|
|
2242
4968
|
const dump = await send(channel, "show running-config", 150000, 6000);
|
|
@@ -2248,13 +4974,13 @@ async function read2(device) {
|
|
|
2248
4974
|
const body = lines.slice(start).filter((line) => line.startsWith("!") || !/[#>]\s*$/.test(line.trim()));
|
|
2249
4975
|
const text = body.join(`
|
|
2250
4976
|
`).replace(/^!(System (up )?time|Boot times):.*\n/gm, "").trim();
|
|
2251
|
-
return
|
|
4977
|
+
return scrub3(text + `
|
|
2252
4978
|
`).replace(/(authkey \S+|privkey\s+\S+) \S+/g, "$1 <redacted>");
|
|
2253
4979
|
} finally {
|
|
2254
4980
|
close();
|
|
2255
4981
|
}
|
|
2256
4982
|
}
|
|
2257
|
-
function
|
|
4983
|
+
function unsupported3(device) {
|
|
2258
4984
|
const found = [];
|
|
2259
4985
|
for (const [name, port] of Object.entries(device.ports ?? {})) {
|
|
2260
4986
|
if (!port)
|
|
@@ -2369,10 +5095,10 @@ function unsupported2(device) {
|
|
|
2369
5095
|
found.push(...switchUnsupported(device));
|
|
2370
5096
|
return found;
|
|
2371
5097
|
}
|
|
2372
|
-
var
|
|
5098
|
+
var REFUSED3 = /^(%|Error|Incomplete|Unrecognized)|can not|cannot |is in use|Wrong parameter|invalid|unsuccessful/im;
|
|
2373
5099
|
var CONFIRM = /^(portswitch|switchport trunk allowed vlan|switchport mode)/;
|
|
2374
5100
|
var RELEASE = /^no (\S+ access-list \d+)$/;
|
|
2375
|
-
async function
|
|
5101
|
+
async function apply3(device, _network, plan) {
|
|
2376
5102
|
const { channel, close } = await open(device);
|
|
2377
5103
|
const released = [];
|
|
2378
5104
|
try {
|
|
@@ -2383,7 +5109,7 @@ async function apply2(device, _network, plan) {
|
|
|
2383
5109
|
const asks = /[[(]y\/n[\])]|input 'y'/i.test(answer) || CONFIRM.test(line) && !/set successfully/i.test(answer);
|
|
2384
5110
|
if (asks)
|
|
2385
5111
|
answer += await send(channel, "y", 1e4, 1000);
|
|
2386
|
-
if (
|
|
5112
|
+
if (REFUSED3.test(answer))
|
|
2387
5113
|
throw new Error(`${step.title}: ${line}
|
|
2388
5114
|
device said: ${answer.trim().slice(0, 300)}`);
|
|
2389
5115
|
const release = RELEASE.exec(line);
|
|
@@ -2431,8 +5157,8 @@ function createdIds(line) {
|
|
|
2431
5157
|
}
|
|
2432
5158
|
return ids;
|
|
2433
5159
|
}
|
|
2434
|
-
function
|
|
2435
|
-
const groups =
|
|
5160
|
+
function plan3(desired, current) {
|
|
5161
|
+
const groups = compare2(desired, current, dialect);
|
|
2436
5162
|
const existing = entries(current, dialect);
|
|
2437
5163
|
for (const group of groups) {
|
|
2438
5164
|
if (!group.path[0]?.startsWith("access-list "))
|
|
@@ -2481,7 +5207,7 @@ function plan2(desired, current) {
|
|
|
2481
5207
|
}
|
|
2482
5208
|
return { steps: planned, problems: [] };
|
|
2483
5209
|
}
|
|
2484
|
-
var raisecom = { extension: ".txt", unsupported:
|
|
5210
|
+
var raisecom = { extension: ".txt", unsupported: unsupported3, render: render3, read: read3, plan: plan3, apply: apply3 };
|
|
2485
5211
|
|
|
2486
5212
|
// src/adapters/devices/routeros/export.ts
|
|
2487
5213
|
var unwrap = (text) => text.replace(/\r/g, "").replace(/\\\n\s*/g, "");
|
|
@@ -2548,7 +5274,7 @@ function parseLine(menu, line) {
|
|
|
2548
5274
|
}
|
|
2549
5275
|
return { menu, verb, selector, flags, attrs };
|
|
2550
5276
|
}
|
|
2551
|
-
function
|
|
5277
|
+
function parse2(text) {
|
|
2552
5278
|
const commands = [];
|
|
2553
5279
|
const generated = new Set;
|
|
2554
5280
|
let menu = "";
|
|
@@ -2567,7 +5293,7 @@ function parse(text) {
|
|
|
2567
5293
|
}
|
|
2568
5294
|
return { commands, generated };
|
|
2569
5295
|
}
|
|
2570
|
-
var
|
|
5296
|
+
var DEFAULTS2 = {
|
|
2571
5297
|
"*": { disabled: "no", comment: "", instance: "default", "routing-table": "main", vrf: "main" },
|
|
2572
5298
|
"/interface bridge": { "protocol-mode": "rstp", priority: "0x8000", "vlan-filtering": "no", pvid: "1" },
|
|
2573
5299
|
"/interface bridge port": { pvid: "1", "frame-types": "admit-all", edge: "auto", "path-cost": "10", priority: "0x80" },
|
|
@@ -2637,12 +5363,12 @@ function defaultOf(command, property) {
|
|
|
2637
5363
|
return SERVICE_DISABLED[command.selector];
|
|
2638
5364
|
if (command.menu === "/ip firewall service-port" && property === "disabled")
|
|
2639
5365
|
return HELPER_DISABLED[command.selector] ?? "no";
|
|
2640
|
-
return
|
|
5366
|
+
return DEFAULTS2[command.menu]?.[property] ?? DEFAULTS2["*"][property];
|
|
2641
5367
|
}
|
|
2642
5368
|
var CLEARED_WITH = {
|
|
2643
5369
|
"/interface ethernet": { speed: "auto-negotiation" }
|
|
2644
5370
|
};
|
|
2645
|
-
var
|
|
5371
|
+
var SECRET_KEYS2 = new Set(["private-key", "password", "tcp-md5-key", "authentication-password", "encryption-password"]);
|
|
2646
5372
|
var isSecretMarker = (value) => value.startsWith("<secret:");
|
|
2647
5373
|
var canonicalRule = (rule) => rule.replace(/\s*([=&|;{}(),<>!]+)\s*/g, "$1").replace(/\s+/g, " ").trim();
|
|
2648
5374
|
function normalValue(command, property, value) {
|
|
@@ -2663,7 +5389,7 @@ function normalValue(command, property, value) {
|
|
|
2663
5389
|
function normal(command) {
|
|
2664
5390
|
const result = {};
|
|
2665
5391
|
for (const [property, raw] of Object.entries(command.attrs)) {
|
|
2666
|
-
if (
|
|
5392
|
+
if (SECRET_KEYS2.has(property) || isSecretMarker(raw))
|
|
2667
5393
|
continue;
|
|
2668
5394
|
const value = normalValue(command, property, raw);
|
|
2669
5395
|
if (value === defaultOf(command, property))
|
|
@@ -2708,7 +5434,7 @@ function identity(command) {
|
|
|
2708
5434
|
}
|
|
2709
5435
|
return result;
|
|
2710
5436
|
}
|
|
2711
|
-
var
|
|
5437
|
+
var ORDERED2 = new Set([
|
|
2712
5438
|
"/routing filter rule",
|
|
2713
5439
|
"/ip firewall filter",
|
|
2714
5440
|
"/ipv6 firewall filter",
|
|
@@ -2721,7 +5447,7 @@ var ORDERED = new Set([
|
|
|
2721
5447
|
"/interface bridge filter",
|
|
2722
5448
|
"/interface ethernet switch rule"
|
|
2723
5449
|
]);
|
|
2724
|
-
function
|
|
5450
|
+
function canonical2(command) {
|
|
2725
5451
|
const properties = Object.entries(normal(command)).sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => `${name}=${value}`);
|
|
2726
5452
|
const flags = [...command.flags].sort();
|
|
2727
5453
|
return [command.verb, command.selector, ...flags, ...properties].filter(Boolean).join(" ");
|
|
@@ -2798,9 +5524,9 @@ var DYNAMIC = new Set([
|
|
|
2798
5524
|
"/ip dhcp-server lease",
|
|
2799
5525
|
"/interface bridge vlan"
|
|
2800
5526
|
]);
|
|
2801
|
-
var
|
|
2802
|
-
var value = (raw) => /^[\w.:/@,!-]+$/.test(raw) ? raw :
|
|
2803
|
-
var findValue = (raw) => /^[\w.:-]+$/.test(raw) ? raw :
|
|
5527
|
+
var quote3 = (value) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$")}"`;
|
|
5528
|
+
var value = (raw) => /^[\w.:/@,!-]+$/.test(raw) ? raw : quote3(raw);
|
|
5529
|
+
var findValue = (raw) => /^[\w.:-]+$/.test(raw) ? raw : quote3(raw);
|
|
2804
5530
|
function find(menu, id) {
|
|
2805
5531
|
const conditions = Object.entries(id).map(([property, raw]) => `${property}=${findValue(raw)}`);
|
|
2806
5532
|
if (DYNAMIC.has(menu))
|
|
@@ -2844,7 +5570,7 @@ function changes(wanted, existing, sendSecrets, problems, label) {
|
|
|
2844
5570
|
}
|
|
2845
5571
|
if (sendSecrets) {
|
|
2846
5572
|
for (const [property, raw] of Object.entries(wanted.attrs)) {
|
|
2847
|
-
if (
|
|
5573
|
+
if (SECRET_KEYS2.has(property) || isSecretMarker(raw))
|
|
2848
5574
|
result[property] = raw;
|
|
2849
5575
|
}
|
|
2850
5576
|
}
|
|
@@ -2886,13 +5612,13 @@ function planEntries(menu, wanted, existing, sendSecrets, problems) {
|
|
|
2886
5612
|
kept.add(entryKey);
|
|
2887
5613
|
const current = existingByKey.get(entryKey);
|
|
2888
5614
|
if (!current) {
|
|
2889
|
-
changeStep.show.push(`+ ${
|
|
5615
|
+
changeStep.show.push(`+ ${canonical2(entry)}`);
|
|
2890
5616
|
changeStep.send.push(line(menu, "add", "", entry.flags, entry.attrs));
|
|
2891
5617
|
continue;
|
|
2892
5618
|
}
|
|
2893
5619
|
const flagsDiffer = [...entry.flags].sort().join() !== [...current.flags].sort().join();
|
|
2894
5620
|
if (flagsDiffer) {
|
|
2895
|
-
changeStep.show.push(`~ replace ${
|
|
5621
|
+
changeStep.show.push(`~ replace ${canonical2(entry)}`);
|
|
2896
5622
|
removeStep.send.push(`${menu} remove ${find(menu, identity(current))}`);
|
|
2897
5623
|
changeStep.send.push(line(menu, "add", "", entry.flags, entry.attrs));
|
|
2898
5624
|
continue;
|
|
@@ -2915,12 +5641,12 @@ function planEntries(menu, wanted, existing, sendSecrets, problems) {
|
|
|
2915
5641
|
for (const list of wholeLists) {
|
|
2916
5642
|
const count = removed.filter((entry) => entry.attrs.list === list).length;
|
|
2917
5643
|
removeStep.show.push(`- list ${list}, ${count} entr${count === 1 ? "y" : "ies"}`);
|
|
2918
|
-
removeStep.send.push(`${menu} remove [ find list=${
|
|
5644
|
+
removeStep.send.push(`${menu} remove [ find list=${quote3(list)} and !dynamic ]`);
|
|
2919
5645
|
}
|
|
2920
5646
|
for (const entry of removed) {
|
|
2921
5647
|
if (wholeLists.has(entry.attrs.list ?? ""))
|
|
2922
5648
|
continue;
|
|
2923
|
-
removeStep.show.push(`- ${
|
|
5649
|
+
removeStep.show.push(`- ${canonical2(entry)}`);
|
|
2924
5650
|
removeStep.send.push(`${menu} remove ${find(menu, identity(entry))}`);
|
|
2925
5651
|
}
|
|
2926
5652
|
if (changeStep.send.length)
|
|
@@ -2940,26 +5666,26 @@ function planCertificates(wanted, existing) {
|
|
|
2940
5666
|
for (const entry of wanted) {
|
|
2941
5667
|
const name = entry.attrs.name;
|
|
2942
5668
|
const current = existingByName.get(name);
|
|
2943
|
-
if (current &&
|
|
5669
|
+
if (current && canonical2(current) === canonical2(entry))
|
|
2944
5670
|
continue;
|
|
2945
5671
|
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"]);
|
|
2946
5672
|
if (!current && sameCertificate) {
|
|
2947
5673
|
const oldName = sameCertificate.attrs.name;
|
|
2948
5674
|
renamed.add(oldName);
|
|
2949
5675
|
install.show.push(`~ rename ${oldName} to ${name}`);
|
|
2950
|
-
install.send.push(`${menu} set [ find name=${
|
|
5676
|
+
install.send.push(`${menu} set [ find name=${quote3(oldName)} ] name=${quote3(name)}`);
|
|
2951
5677
|
continue;
|
|
2952
5678
|
}
|
|
2953
5679
|
install.show.push(`${current ? "~" : "+"} install ${name}`);
|
|
2954
5680
|
if (current)
|
|
2955
|
-
install.send.push(`${menu} remove [ find name=${
|
|
5681
|
+
install.send.push(`${menu} remove [ find name=${quote3(name)} ]`);
|
|
2956
5682
|
install.send.push(INSTALL_CERTIFICATE + name);
|
|
2957
5683
|
}
|
|
2958
5684
|
for (const entry of existing) {
|
|
2959
5685
|
if (wantedNames.has(entry.attrs.name) || renamed.has(entry.attrs.name))
|
|
2960
5686
|
continue;
|
|
2961
5687
|
remove.show.push(`- ${entry.attrs.name}`);
|
|
2962
|
-
remove.send.push(`${menu} remove [ find name=${
|
|
5688
|
+
remove.send.push(`${menu} remove [ find name=${quote3(entry.attrs.name)} ]`);
|
|
2963
5689
|
}
|
|
2964
5690
|
if (install.send.length)
|
|
2965
5691
|
result.first.push(install);
|
|
@@ -2970,8 +5696,8 @@ function planCertificates(wanted, existing) {
|
|
|
2970
5696
|
var OLD_RULES = "frameworkOldRules";
|
|
2971
5697
|
function planOrdered(menu, wanted, existing) {
|
|
2972
5698
|
const result = { first: [], last: [] };
|
|
2973
|
-
const same = wanted.map((item) =>
|
|
2974
|
-
`) === existing.map((item) =>
|
|
5699
|
+
const same = wanted.map((item) => canonical2(item)).join(`
|
|
5700
|
+
`) === existing.map((item) => canonical2(item)).join(`
|
|
2975
5701
|
`);
|
|
2976
5702
|
if (same)
|
|
2977
5703
|
return result;
|
|
@@ -2981,7 +5707,7 @@ function planOrdered(menu, wanted, existing) {
|
|
|
2981
5707
|
const droppedChains = [...new Set(existing.map((item) => item.attrs.chain ?? ""))].filter((chain) => !keptChains.has(chain));
|
|
2982
5708
|
let removeOld = `${menu} remove $${OLD_RULES}`;
|
|
2983
5709
|
if (hasChains) {
|
|
2984
|
-
const keep = [...keptChains].map((chain) =>
|
|
5710
|
+
const keep = [...keptChains].map((chain) => quote3(chain)).join(";");
|
|
2985
5711
|
const inKeptChain = `[:typeof [:find $keep [${menu} get $id chain]]] != "nil"`;
|
|
2986
5712
|
removeOld = `:local keep {${keep}}; :foreach id in=$${OLD_RULES} do={ :if (${inKeptChain}) do={ ${menu} remove $id } }`;
|
|
2987
5713
|
}
|
|
@@ -2990,19 +5716,19 @@ function planOrdered(menu, wanted, existing) {
|
|
|
2990
5716
|
...wanted.map((item) => line(menu, "add", "", item.flags, item.attrs)),
|
|
2991
5717
|
`:global ${OLD_RULES}; ${removeOld}; /system script environment remove [find name="${OLD_RULES}"]`
|
|
2992
5718
|
];
|
|
2993
|
-
const show = [...existing.map((item) => `- ${
|
|
5719
|
+
const show = [...existing.map((item) => `- ${canonical2(item)}`), ...wanted.map((item) => `+ ${canonical2(item)}`)];
|
|
2994
5720
|
result.first.push({ title: `${menu}, in the order of the config`, show, send });
|
|
2995
5721
|
if (hasChains) {
|
|
2996
5722
|
for (const chain of droppedChains) {
|
|
2997
|
-
const selector = `[ find chain=${
|
|
5723
|
+
const selector = `[ find chain=${quote3(chain)}${DYNAMIC.has(menu) ? " and !dynamic" : ""} ]`;
|
|
2998
5724
|
result.last.push({ title: `${menu} chain ${chain}`, show: [`- chain ${chain}`], send: [`${menu} remove ${selector}`] });
|
|
2999
5725
|
}
|
|
3000
5726
|
}
|
|
3001
5727
|
return result;
|
|
3002
5728
|
}
|
|
3003
|
-
function
|
|
3004
|
-
const want =
|
|
3005
|
-
const have =
|
|
5729
|
+
function plan4(desired, current, options) {
|
|
5730
|
+
const want = parse2(desired);
|
|
5731
|
+
const have = parse2(current);
|
|
3006
5732
|
const owned = (command) => !(command.menu.endsWith("address-list") && want.generated.has(command.attrs.list ?? ""));
|
|
3007
5733
|
const wantedByMenu = byMenu(want.commands.filter(owned));
|
|
3008
5734
|
const existingByMenu = byMenu(have.commands.filter(owned));
|
|
@@ -3016,7 +5742,7 @@ function plan3(desired, current, options) {
|
|
|
3016
5742
|
let menuPlan;
|
|
3017
5743
|
if (menu === "/certificate")
|
|
3018
5744
|
menuPlan = planCertificates(wanted, existing);
|
|
3019
|
-
else if (
|
|
5745
|
+
else if (ORDERED2.has(menu))
|
|
3020
5746
|
menuPlan = planOrdered(menu, wanted, existing);
|
|
3021
5747
|
else {
|
|
3022
5748
|
menuPlan = planEntries(menu, wanted, existing, options.secrets, problems);
|
|
@@ -3047,7 +5773,7 @@ class Output {
|
|
|
3047
5773
|
`;
|
|
3048
5774
|
}
|
|
3049
5775
|
}
|
|
3050
|
-
function
|
|
5776
|
+
function quote4(value) {
|
|
3051
5777
|
const plain = /^[\w.:/@-]+$/.test(value);
|
|
3052
5778
|
if (plain)
|
|
3053
5779
|
return value;
|
|
@@ -3059,7 +5785,7 @@ function command(verb, properties) {
|
|
|
3059
5785
|
for (const [name, value] of Object.entries(properties)) {
|
|
3060
5786
|
if (value === undefined || value === false)
|
|
3061
5787
|
continue;
|
|
3062
|
-
const written = typeof value === "number" ? String(value) :
|
|
5788
|
+
const written = typeof value === "number" ? String(value) : quote4(value);
|
|
3063
5789
|
words.push(`${name}=${written}`);
|
|
3064
5790
|
}
|
|
3065
5791
|
return words.join(" ");
|
|
@@ -3067,7 +5793,7 @@ function command(verb, properties) {
|
|
|
3067
5793
|
var yesNo = (on) => on ? "yes" : "no";
|
|
3068
5794
|
var familySuffix = (family) => family === "ipv4" ? "V4" : "V6";
|
|
3069
5795
|
var eachPort2 = (ports) => [ports].flat().map((port) => typeof port === "number" ? String(port) : `${port.min}-${port.max}`);
|
|
3070
|
-
function
|
|
5796
|
+
function portList2(ports) {
|
|
3071
5797
|
if (ports === undefined)
|
|
3072
5798
|
return;
|
|
3073
5799
|
return eachPort2(ports).join(",");
|
|
@@ -3443,7 +6169,7 @@ var NETWORKS = { ipv4: "bgp-networks-v4", ipv6: "bgp-networks-v6" };
|
|
|
3443
6169
|
function renderAddressing(out, ctx) {
|
|
3444
6170
|
addresses(out, ctx);
|
|
3445
6171
|
dhcp(out, ctx);
|
|
3446
|
-
|
|
6172
|
+
dns2(out, ctx);
|
|
3447
6173
|
addressLists(out, ctx);
|
|
3448
6174
|
}
|
|
3449
6175
|
function addresses(out, ctx) {
|
|
@@ -3501,7 +6227,7 @@ function dhcp(out, ctx) {
|
|
|
3501
6227
|
out.add("/ip dhcp-relay", command("add", { "dhcp-server": relay.servers.join(","), interface: ctx.name(relay.interface), name }));
|
|
3502
6228
|
}
|
|
3503
6229
|
}
|
|
3504
|
-
function
|
|
6230
|
+
function dns2(out, ctx) {
|
|
3505
6231
|
const dns = ctx.device.system?.dns;
|
|
3506
6232
|
out.add("/ip dns", command("set", { "allow-remote-requests": yesNo(dns?.serve), servers: dns?.servers.join(",") ?? "" }));
|
|
3507
6233
|
}
|
|
@@ -3531,7 +6257,7 @@ function addressLists(out, ctx) {
|
|
|
3531
6257
|
}
|
|
3532
6258
|
}
|
|
3533
6259
|
}
|
|
3534
|
-
var
|
|
6260
|
+
var HELPERS2 = ["ftp", "tftp", "irc", "h323", "sip", "pptp", "rtsp"];
|
|
3535
6261
|
var CHAINS = ["input", "forward", "output"];
|
|
3536
6262
|
function renderFirewall(out, ctx) {
|
|
3537
6263
|
for (const family of ["ipv4", "ipv6"]) {
|
|
@@ -3539,7 +6265,7 @@ function renderFirewall(out, ctx) {
|
|
|
3539
6265
|
filter(out, ctx, family);
|
|
3540
6266
|
nat(out, ctx, family);
|
|
3541
6267
|
}
|
|
3542
|
-
|
|
6268
|
+
helpers2(out, ctx);
|
|
3543
6269
|
switchAcls(out, ctx);
|
|
3544
6270
|
}
|
|
3545
6271
|
function untracked(out, ctx, family) {
|
|
@@ -3575,13 +6301,13 @@ function matchProperties(ctx, match, family) {
|
|
|
3575
6301
|
"connection-state": match?.state?.join(","),
|
|
3576
6302
|
"dst-address": match?.dst,
|
|
3577
6303
|
"dst-address-list": match?.dst_set,
|
|
3578
|
-
"dst-port":
|
|
6304
|
+
"dst-port": portList2(match?.dst_port),
|
|
3579
6305
|
"in-interface": match?.in_interface && ctx.name(match.in_interface),
|
|
3580
6306
|
"out-interface": match?.out_interface && ctx.name(match.out_interface),
|
|
3581
6307
|
protocol,
|
|
3582
6308
|
"src-address": match?.src,
|
|
3583
6309
|
"src-address-list": match?.src_set,
|
|
3584
|
-
"src-port":
|
|
6310
|
+
"src-port": portList2(match?.src_port),
|
|
3585
6311
|
"tcp-flags": flags
|
|
3586
6312
|
};
|
|
3587
6313
|
}
|
|
@@ -3623,9 +6349,9 @@ function nat(out, ctx, family) {
|
|
|
3623
6349
|
}
|
|
3624
6350
|
}
|
|
3625
6351
|
}
|
|
3626
|
-
function
|
|
6352
|
+
function helpers2(out, ctx) {
|
|
3627
6353
|
const enabled = ctx.device.firewall?.helpers ?? [];
|
|
3628
|
-
for (const helper of
|
|
6354
|
+
for (const helper of HELPERS2) {
|
|
3629
6355
|
out.add("/ip firewall service-port", command(`set ${helper}`, { disabled: yesNo(!enabled.includes(helper)) }));
|
|
3630
6356
|
}
|
|
3631
6357
|
}
|
|
@@ -3660,7 +6386,7 @@ function switchAcls(out, ctx) {
|
|
|
3660
6386
|
}
|
|
3661
6387
|
}
|
|
3662
6388
|
var RPKI_GROUP2 = "rpki";
|
|
3663
|
-
var
|
|
6389
|
+
var REJECT_ALL3 = "REJECT-ALL";
|
|
3664
6390
|
var ROLES = {
|
|
3665
6391
|
provider: "ebgp-customer",
|
|
3666
6392
|
customer: "ebgp-provider",
|
|
@@ -3669,17 +6395,17 @@ var ROLES = {
|
|
|
3669
6395
|
"rs-client": "ebgp-rs"
|
|
3670
6396
|
};
|
|
3671
6397
|
function renderRouting(out, ctx) {
|
|
3672
|
-
|
|
6398
|
+
staticRoutes3(out, ctx);
|
|
3673
6399
|
rpki2(out, ctx);
|
|
3674
6400
|
if (ctx.device.routing?.bgp) {
|
|
3675
6401
|
bgpInstance(out, ctx);
|
|
3676
6402
|
policies(out, ctx);
|
|
3677
6403
|
bgpConnections(out, ctx);
|
|
3678
6404
|
}
|
|
3679
|
-
|
|
6405
|
+
ospf3(out, ctx);
|
|
3680
6406
|
bfd(out, ctx);
|
|
3681
6407
|
}
|
|
3682
|
-
function
|
|
6408
|
+
function staticRoutes3(out, ctx) {
|
|
3683
6409
|
for (const route of ctx.device.routing?.static ?? []) {
|
|
3684
6410
|
const menu = familyOf2(host2(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
|
|
3685
6411
|
const verb = route.blackhole ? "add blackhole" : "add";
|
|
@@ -3707,12 +6433,12 @@ function bgpInstance(out, ctx) {
|
|
|
3707
6433
|
const bgp = ctx.device.routing.bgp;
|
|
3708
6434
|
out.add("/routing bgp instance", command("add", { as: bgp.asn, name: "default", "router-id": ctx.device.routing?.router_id }));
|
|
3709
6435
|
}
|
|
3710
|
-
function
|
|
6436
|
+
function settingsOf3(ctx, neighbor) {
|
|
3711
6437
|
const group = neighbor.group ? ctx.device.routing?.bgp?.groups?.[neighbor.group] : undefined;
|
|
3712
6438
|
return { ...group, ...neighbor };
|
|
3713
6439
|
}
|
|
3714
|
-
function
|
|
3715
|
-
return
|
|
6440
|
+
function familiesOf2(ctx, neighbor) {
|
|
6441
|
+
return settingsOf3(ctx, neighbor).families ?? [familyOf2(neighbor.address)];
|
|
3716
6442
|
}
|
|
3717
6443
|
var chainName = (policy, family) => `${policy}-${familySuffix(family)}`;
|
|
3718
6444
|
function appliesTo(ctx, rule, family) {
|
|
@@ -3817,8 +6543,8 @@ function policies(out, ctx) {
|
|
|
3817
6543
|
};
|
|
3818
6544
|
let rejectAllNeeded = false;
|
|
3819
6545
|
for (const neighbor of Object.values(bgp.neighbors ?? {})) {
|
|
3820
|
-
const settings =
|
|
3821
|
-
for (const family of
|
|
6546
|
+
const settings = settingsOf3(ctx, neighbor);
|
|
6547
|
+
for (const family of familiesOf2(ctx, neighbor)) {
|
|
3822
6548
|
for (const policy of [settings.import, settings.export]) {
|
|
3823
6549
|
if (policy)
|
|
3824
6550
|
want(policy, family);
|
|
@@ -3848,7 +6574,7 @@ function policies(out, ctx) {
|
|
|
3848
6574
|
if (rejectAllNeeded) {
|
|
3849
6575
|
for (const family of ["ipv4", "ipv6"])
|
|
3850
6576
|
out.add("/routing filter rule", command("add", {
|
|
3851
|
-
chain: chainName(
|
|
6577
|
+
chain: chainName(REJECT_ALL3, family),
|
|
3852
6578
|
rule: "reject"
|
|
3853
6579
|
}));
|
|
3854
6580
|
}
|
|
@@ -3856,8 +6582,8 @@ function policies(out, ctx) {
|
|
|
3856
6582
|
function bgpConnections(out, ctx) {
|
|
3857
6583
|
const bgp = ctx.device.routing.bgp;
|
|
3858
6584
|
for (const [name, neighbor] of Object.entries(bgp.neighbors ?? {})) {
|
|
3859
|
-
const settings =
|
|
3860
|
-
const [family] =
|
|
6585
|
+
const settings = settingsOf3(ctx, neighbor);
|
|
6586
|
+
const [family] = familiesOf2(ctx, neighbor);
|
|
3861
6587
|
const addressFamily = familyOf2(neighbor.address);
|
|
3862
6588
|
const internal = settings.remote_as === bgp.asn;
|
|
3863
6589
|
const originates = bgp.networks?.some((prefix) => familyOf2(host2(prefix)) === family);
|
|
@@ -3867,7 +6593,7 @@ function bgpConnections(out, ctx) {
|
|
|
3867
6593
|
as: bgp.asn,
|
|
3868
6594
|
connect: settings.passive ? "no" : undefined,
|
|
3869
6595
|
"hold-time": settings.hold_time ? duration(settings.hold_time) : undefined,
|
|
3870
|
-
"input.filter": chainName(settings.import ??
|
|
6596
|
+
"input.filter": chainName(settings.import ?? REJECT_ALL3, family),
|
|
3871
6597
|
instance: "default",
|
|
3872
6598
|
[`input.limit-process-routes-${family}`]: settings.max_prefixes,
|
|
3873
6599
|
"keepalive-time": settings.keepalive ? duration(settings.keepalive) : undefined,
|
|
@@ -3876,7 +6602,7 @@ function bgpConnections(out, ctx) {
|
|
|
3876
6602
|
"local.role": role,
|
|
3877
6603
|
multihop: settings.multihop ? "yes" : undefined,
|
|
3878
6604
|
name,
|
|
3879
|
-
"output.filter-chain": chainName(settings.export ??
|
|
6605
|
+
"output.filter-chain": chainName(settings.export ?? REJECT_ALL3, family),
|
|
3880
6606
|
"output.network": originates ? NETWORKS[family] : undefined,
|
|
3881
6607
|
"remote.address": `${neighbor.address}/${addressFamily === "ipv4" ? 32 : 128}`,
|
|
3882
6608
|
"remote.as": settings.remote_as,
|
|
@@ -3885,7 +6611,7 @@ function bgpConnections(out, ctx) {
|
|
|
3885
6611
|
}));
|
|
3886
6612
|
}
|
|
3887
6613
|
}
|
|
3888
|
-
function
|
|
6614
|
+
function ospf3(out, ctx) {
|
|
3889
6615
|
const ospf = ctx.device.routing?.ospf;
|
|
3890
6616
|
if (!ospf)
|
|
3891
6617
|
return;
|
|
@@ -3916,12 +6642,12 @@ function ospf2(out, ctx) {
|
|
|
3916
6642
|
}
|
|
3917
6643
|
function bfd(out, ctx) {
|
|
3918
6644
|
const routing = ctx.device.routing;
|
|
3919
|
-
const bgpWants = Object.values(routing?.bgp?.neighbors ?? {}).some((neighbor) =>
|
|
6645
|
+
const bgpWants = Object.values(routing?.bgp?.neighbors ?? {}).some((neighbor) => settingsOf3(ctx, neighbor).bfd);
|
|
3920
6646
|
const ospfWants = Object.values(routing?.ospf?.areas ?? {}).some((area) => Object.values(area.interfaces).some((iface) => iface?.bfd));
|
|
3921
6647
|
if (bgpWants || ospfWants)
|
|
3922
6648
|
out.add("/routing bfd configuration", command("add", { interfaces: "all" }));
|
|
3923
6649
|
}
|
|
3924
|
-
var
|
|
6650
|
+
var SERVICES2 = {
|
|
3925
6651
|
ftp: "ftp",
|
|
3926
6652
|
ssh: "ssh",
|
|
3927
6653
|
telnet: "telnet",
|
|
@@ -3937,13 +6663,13 @@ var SNMP_PRIVACY2 = { aes128: "AES", des: "DES" };
|
|
|
3937
6663
|
function renderManagement(out, ctx) {
|
|
3938
6664
|
services2(out, ctx);
|
|
3939
6665
|
layerTwo(out, ctx);
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
6666
|
+
snmp3(out, ctx);
|
|
6667
|
+
users3(out, ctx);
|
|
6668
|
+
certificates2(out, ctx);
|
|
3943
6669
|
}
|
|
3944
6670
|
function services2(out, ctx) {
|
|
3945
6671
|
const management = ctx.device.management ?? {};
|
|
3946
|
-
for (const [neutral, platform] of Object.entries(
|
|
6672
|
+
for (const [neutral, platform] of Object.entries(SERVICES2)) {
|
|
3947
6673
|
const service = management[neutral];
|
|
3948
6674
|
if (!service) {
|
|
3949
6675
|
out.add("/ip service", command(`set ${platform}`, { disabled: "yes" }));
|
|
@@ -3973,7 +6699,7 @@ function layerTwo(out, ctx) {
|
|
|
3973
6699
|
discovery = "all";
|
|
3974
6700
|
out.add("/ip neighbor discovery-settings", command("set", { "discover-interface-list": discovery, protocol: "lldp" }));
|
|
3975
6701
|
}
|
|
3976
|
-
function
|
|
6702
|
+
function snmp3(out, ctx) {
|
|
3977
6703
|
const management = ctx.device.management ?? {};
|
|
3978
6704
|
const snmp = management.snmp;
|
|
3979
6705
|
if (!snmp) {
|
|
@@ -3999,7 +6725,7 @@ function snmp2(out, ctx) {
|
|
|
3999
6725
|
}));
|
|
4000
6726
|
}
|
|
4001
6727
|
}
|
|
4002
|
-
function
|
|
6728
|
+
function users3(out, ctx) {
|
|
4003
6729
|
for (const [name, user] of Object.entries(ctx.device.users ?? {})) {
|
|
4004
6730
|
out.add("/user", command("add", {
|
|
4005
6731
|
comment: user.description,
|
|
@@ -4011,7 +6737,7 @@ function users2(out, ctx) {
|
|
|
4011
6737
|
out.add("/user ssh-keys", command("add", { key, user: name }));
|
|
4012
6738
|
}
|
|
4013
6739
|
}
|
|
4014
|
-
function
|
|
6740
|
+
function certificates2(out, ctx) {
|
|
4015
6741
|
for (const [name, certificate] of Object.entries(ctx.device.certificates ?? {})) {
|
|
4016
6742
|
out.add("/certificate", command("add", {
|
|
4017
6743
|
fingerprint: fingerprint(certificate.certificate),
|
|
@@ -4048,12 +6774,12 @@ function renderSystem(out, ctx) {
|
|
|
4048
6774
|
out.add("/system ntp server", command("set", { enabled: yesNo(system.ntp?.serve) }));
|
|
4049
6775
|
for (const server of system.ntp?.servers ?? [])
|
|
4050
6776
|
out.add("/system ntp client servers", command("add", { address: server }));
|
|
4051
|
-
|
|
6777
|
+
logging2(out, ctx);
|
|
4052
6778
|
out.add("/system package update", command("set", { channel: system.release_channel ?? "stable" }));
|
|
4053
6779
|
out.add("/system routerboard settings", command("set", { "enter-setup-on": "delete-key" }));
|
|
4054
|
-
|
|
6780
|
+
flowExport2(out, ctx);
|
|
4055
6781
|
}
|
|
4056
|
-
function
|
|
6782
|
+
function logging2(out, ctx) {
|
|
4057
6783
|
const logging = ctx.device.system?.logging;
|
|
4058
6784
|
const local = logging?.local;
|
|
4059
6785
|
out.add("/system logging action", command("set 1", {
|
|
@@ -4074,7 +6800,7 @@ function logging(out, ctx) {
|
|
|
4074
6800
|
out.add("/system logging", command("add", { action, topics: topic }));
|
|
4075
6801
|
});
|
|
4076
6802
|
}
|
|
4077
|
-
function
|
|
6803
|
+
function flowExport2(out, ctx) {
|
|
4078
6804
|
const flow = ctx.device.flow_export;
|
|
4079
6805
|
if (!flow) {
|
|
4080
6806
|
out.add("/ip traffic-flow", command("set", { enabled: "no" }));
|
|
@@ -4094,7 +6820,7 @@ function flowExport(out, ctx) {
|
|
|
4094
6820
|
out.add("/ip traffic-flow target", command("add", { "dst-address": collector.address, port: collector.port ?? 2055, version }));
|
|
4095
6821
|
}
|
|
4096
6822
|
}
|
|
4097
|
-
function
|
|
6823
|
+
function render4(device) {
|
|
4098
6824
|
const out = new Output;
|
|
4099
6825
|
const ctx = new Context(device);
|
|
4100
6826
|
renderInterfaces(out, ctx);
|
|
@@ -4108,7 +6834,7 @@ function render3(device) {
|
|
|
4108
6834
|
|
|
4109
6835
|
// src/adapters/devices/routeros/index.ts
|
|
4110
6836
|
var SUPPORTED_MAJOR2 = 7;
|
|
4111
|
-
async function
|
|
6837
|
+
async function login3(device) {
|
|
4112
6838
|
const user = device.users?.[device.connection.user];
|
|
4113
6839
|
if (!user)
|
|
4114
6840
|
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
@@ -4119,7 +6845,7 @@ async function login2(device) {
|
|
|
4119
6845
|
password: resolve(user.password)
|
|
4120
6846
|
});
|
|
4121
6847
|
}
|
|
4122
|
-
async function
|
|
6848
|
+
async function checkVersion3(client, device) {
|
|
4123
6849
|
const version = (await exec(client, ":put [/system resource get version]")).trim();
|
|
4124
6850
|
const major = Number(version.split(".")[0]);
|
|
4125
6851
|
if (major !== SUPPORTED_MAJOR2) {
|
|
@@ -4135,10 +6861,10 @@ var LIST_CERTIFICATES = [
|
|
|
4135
6861
|
':put ("add fingerprint=" . [/certificate get $c fingerprint] . " name=" . [/certificate get $c name] . " private-key=" . $key)',
|
|
4136
6862
|
"}"
|
|
4137
6863
|
].join(" ");
|
|
4138
|
-
async function
|
|
4139
|
-
const client = await
|
|
6864
|
+
async function read4(device) {
|
|
6865
|
+
const client = await login3(device);
|
|
4140
6866
|
try {
|
|
4141
|
-
await
|
|
6867
|
+
await checkVersion3(client, device);
|
|
4142
6868
|
const config = await exec(client, "/export", 300000);
|
|
4143
6869
|
const users = await exec(client, "/user export");
|
|
4144
6870
|
const certificates = (await exec(client, LIST_CERTIFICATES)).trim();
|
|
@@ -4159,7 +6885,7 @@ ${certificates}` : "";
|
|
|
4159
6885
|
client.end();
|
|
4160
6886
|
}
|
|
4161
6887
|
}
|
|
4162
|
-
function
|
|
6888
|
+
function unsupported4(device) {
|
|
4163
6889
|
const found = [];
|
|
4164
6890
|
for (const [name, port] of Object.entries(device.ports ?? {})) {
|
|
4165
6891
|
if (!port)
|
|
@@ -4233,9 +6959,9 @@ async function installCertificate(client, device, name) {
|
|
|
4233
6959
|
}
|
|
4234
6960
|
await exec(client, `/file remove [find name="${name}.crt" or name="${name}.key"]`);
|
|
4235
6961
|
}
|
|
4236
|
-
async function
|
|
4237
|
-
const client = await
|
|
4238
|
-
await
|
|
6962
|
+
async function apply4(device, _network, changes, options) {
|
|
6963
|
+
const client = await login3(device);
|
|
6964
|
+
await checkVersion3(client, device);
|
|
4239
6965
|
const commands = changes.steps.flatMap((step) => step.send.map((command) => ({ title: step.title, command })));
|
|
4240
6966
|
for (const { title, command } of commands) {
|
|
4241
6967
|
if (command.startsWith(INSTALL_CERTIFICATE))
|
|
@@ -4281,7 +7007,7 @@ async function apply3(device, _network, changes, options) {
|
|
|
4281
7007
|
throw new Error("apply failed and the backup is being restored");
|
|
4282
7008
|
}
|
|
4283
7009
|
client.end();
|
|
4284
|
-
const proof = await
|
|
7010
|
+
const proof = await login3(device).catch(() => {
|
|
4285
7011
|
return;
|
|
4286
7012
|
});
|
|
4287
7013
|
if (!proof)
|
|
@@ -4291,10 +7017,10 @@ async function apply3(device, _network, changes, options) {
|
|
|
4291
7017
|
proof.end();
|
|
4292
7018
|
console.log(" logged in again, rollback disarmed");
|
|
4293
7019
|
}
|
|
4294
|
-
async function
|
|
7020
|
+
async function fill3(device, _network, set, family, prefixes) {
|
|
4295
7021
|
const menu = family === "ipv4" ? "/ip firewall address-list" : "/ipv6 firewall address-list";
|
|
4296
7022
|
const batchSize = 60;
|
|
4297
|
-
const client = await
|
|
7023
|
+
const client = await login3(device);
|
|
4298
7024
|
try {
|
|
4299
7025
|
await exec(client, `${menu} remove [find list="${set}" and !dynamic]`);
|
|
4300
7026
|
for (let start = 0;start < prefixes.length; start += batchSize) {
|
|
@@ -4311,12 +7037,12 @@ async function fill2(device, _network, set, family, prefixes) {
|
|
|
4311
7037
|
}
|
|
4312
7038
|
var routeros = {
|
|
4313
7039
|
extension: ".rsc",
|
|
4314
|
-
unsupported:
|
|
4315
|
-
render:
|
|
4316
|
-
read:
|
|
4317
|
-
plan:
|
|
4318
|
-
apply:
|
|
4319
|
-
fill:
|
|
7040
|
+
unsupported: unsupported4,
|
|
7041
|
+
render: render4,
|
|
7042
|
+
read: read4,
|
|
7043
|
+
plan: plan4,
|
|
7044
|
+
apply: apply4,
|
|
7045
|
+
fill: fill3
|
|
4320
7046
|
};
|
|
4321
7047
|
|
|
4322
7048
|
// src/adapters/devices/vrp/index.ts
|
|
@@ -4435,7 +7161,7 @@ class Config {
|
|
|
4435
7161
|
`;
|
|
4436
7162
|
}
|
|
4437
7163
|
}
|
|
4438
|
-
function
|
|
7164
|
+
function render5(device) {
|
|
4439
7165
|
const config = new Config;
|
|
4440
7166
|
const vlans = device.vlans ?? {};
|
|
4441
7167
|
const vlanId = (name) => vlans[name].id;
|
|
@@ -4452,7 +7178,7 @@ function render4(device) {
|
|
|
4452
7178
|
system2(config, device);
|
|
4453
7179
|
vlanBlocks(config, device);
|
|
4454
7180
|
managementAcl(config, device);
|
|
4455
|
-
|
|
7181
|
+
users4(config, device);
|
|
4456
7182
|
vrfs3(config, device);
|
|
4457
7183
|
for (const [name, lag] of Object.entries(interfaces)) {
|
|
4458
7184
|
if (lag.type === "lag")
|
|
@@ -4466,16 +7192,16 @@ function render4(device) {
|
|
|
4466
7192
|
const settings = device.ports?.[port];
|
|
4467
7193
|
config.block(`interface ${nameOf(port)}`, physicalPort2(device, port, settings, vlanId, lagNumbers));
|
|
4468
7194
|
}
|
|
4469
|
-
|
|
4470
|
-
|
|
7195
|
+
staticRoutes4(config, device, nameOf);
|
|
7196
|
+
ospf4(config, device, nameOf);
|
|
4471
7197
|
if (wantsBfd(device))
|
|
4472
7198
|
config.block("bfd", []);
|
|
4473
7199
|
if (device.dhcp_relay && Object.keys(device.dhcp_relay).length) {
|
|
4474
7200
|
config.line("dhcp enable");
|
|
4475
7201
|
config.separator();
|
|
4476
7202
|
}
|
|
4477
|
-
|
|
4478
|
-
|
|
7203
|
+
flowExport3(config, device);
|
|
7204
|
+
snmp4(config, device);
|
|
4479
7205
|
ssh(config, device);
|
|
4480
7206
|
userInterfaces(config, device);
|
|
4481
7207
|
return config.text();
|
|
@@ -4535,7 +7261,7 @@ function managementAcl(config, device) {
|
|
|
4535
7261
|
const rules = allow.map((cidr, index) => `rule ${(index + 1) * 5} permit source ${host2(cidr)} ${wildcard(split(cidr).len)}`);
|
|
4536
7262
|
config.block(`acl number ${MANAGEMENT_ACL2}`, ["description Management access", ...rules, `rule ${(allow.length + 1) * 5} deny`]);
|
|
4537
7263
|
}
|
|
4538
|
-
function
|
|
7264
|
+
function users4(config, device) {
|
|
4539
7265
|
const lines = [];
|
|
4540
7266
|
for (const [name, user] of Object.entries(device.users ?? {})) {
|
|
4541
7267
|
lines.push(`local-user ${name} password irreversible-cipher ${marker(user.password)}`);
|
|
@@ -4660,7 +7386,7 @@ function physicalPort2(device, port, settings, vlanId, lagNumbers) {
|
|
|
4660
7386
|
}
|
|
4661
7387
|
return lines;
|
|
4662
7388
|
}
|
|
4663
|
-
function
|
|
7389
|
+
function staticRoutes4(config, device, nameOf) {
|
|
4664
7390
|
for (const route of device.routing?.static ?? []) {
|
|
4665
7391
|
const { addr, len } = split(route.prefix);
|
|
4666
7392
|
const nextHop = route.blackhole ? "NULL0" : route.via ?? nameOf(route.interface);
|
|
@@ -4674,7 +7400,7 @@ function staticRoutes3(config, device, nameOf) {
|
|
|
4674
7400
|
}
|
|
4675
7401
|
config.separator();
|
|
4676
7402
|
}
|
|
4677
|
-
function
|
|
7403
|
+
function ospf4(config, device, nameOf) {
|
|
4678
7404
|
const ospf = device.routing?.ospf;
|
|
4679
7405
|
if (!ospf)
|
|
4680
7406
|
return;
|
|
@@ -4698,7 +7424,7 @@ function wantsBfd(device) {
|
|
|
4698
7424
|
const areas = Object.values(device.routing?.ospf?.areas ?? {});
|
|
4699
7425
|
return areas.some((area) => Object.values(area.interfaces).some((settings) => settings?.bfd));
|
|
4700
7426
|
}
|
|
4701
|
-
function
|
|
7427
|
+
function flowExport3(config, device) {
|
|
4702
7428
|
const flow = device.flow_export;
|
|
4703
7429
|
if (flow?.protocol !== "sflow")
|
|
4704
7430
|
return;
|
|
@@ -4709,7 +7435,7 @@ function flowExport2(config, device) {
|
|
|
4709
7435
|
});
|
|
4710
7436
|
config.separator();
|
|
4711
7437
|
}
|
|
4712
|
-
function
|
|
7438
|
+
function snmp4(config, device) {
|
|
4713
7439
|
const snmp = device.management?.snmp;
|
|
4714
7440
|
if (!snmp)
|
|
4715
7441
|
return;
|
|
@@ -4763,7 +7489,7 @@ function userInterfaces(config, device) {
|
|
|
4763
7489
|
config.block("user-interface vty 0 4", ["authentication-mode aaa", `protocol inbound ${device.management?.telnet ? "all" : "ssh"}`]);
|
|
4764
7490
|
}
|
|
4765
7491
|
var HOST_KEYS2 = ["ssh-rsa"];
|
|
4766
|
-
async function
|
|
7492
|
+
async function login4(device) {
|
|
4767
7493
|
const user = device.users?.[device.connection.user];
|
|
4768
7494
|
if (!user)
|
|
4769
7495
|
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
@@ -4783,8 +7509,8 @@ async function openShell(client) {
|
|
|
4783
7509
|
await send(channel, "screen-length 0 temporary", 1e4, 1500);
|
|
4784
7510
|
return channel;
|
|
4785
7511
|
}
|
|
4786
|
-
async function
|
|
4787
|
-
const client = await
|
|
7512
|
+
async function read5(device) {
|
|
7513
|
+
const client = await login4(device);
|
|
4788
7514
|
try {
|
|
4789
7515
|
const channel = await openShell(client);
|
|
4790
7516
|
const dump = await send(channel, "display current-configuration", 120000, 3000);
|
|
@@ -4795,14 +7521,14 @@ async function read4(device) {
|
|
|
4795
7521
|
if (start < 0)
|
|
4796
7522
|
throw new Error(`${device.name} returned no configuration`);
|
|
4797
7523
|
const body = lines.slice(start).filter((line) => !/^<.*>$/.test(line.trim()) && !line.startsWith("!Last configuration"));
|
|
4798
|
-
return
|
|
7524
|
+
return scrub3(body.join(`
|
|
4799
7525
|
`).trim() + `
|
|
4800
7526
|
`);
|
|
4801
7527
|
} finally {
|
|
4802
7528
|
client.end();
|
|
4803
7529
|
}
|
|
4804
7530
|
}
|
|
4805
|
-
function
|
|
7531
|
+
function unsupported5(device) {
|
|
4806
7532
|
const found = [];
|
|
4807
7533
|
for (const [name, port] of Object.entries(device.ports ?? {})) {
|
|
4808
7534
|
if (!port)
|
|
@@ -4881,7 +7607,7 @@ function unsupported4(device) {
|
|
|
4881
7607
|
}
|
|
4882
7608
|
return found;
|
|
4883
7609
|
}
|
|
4884
|
-
var
|
|
7610
|
+
var REFUSED4 = /^\s*Error|Unrecognized command|Incomplete command|Wrong parameter|Too many parameters/m;
|
|
4885
7611
|
var INTERACTIVE = /^(snmp-agent usm-user v3 \S+ (?:authentication|privacy)-mode \S+) cipher <secret:([A-Za-z0-9_./:-]+)>$/;
|
|
4886
7612
|
async function sendLine(channel, line) {
|
|
4887
7613
|
const interactive = INTERACTIVE.exec(line.trim());
|
|
@@ -4898,21 +7624,21 @@ async function sendLine(channel, line) {
|
|
|
4898
7624
|
answer += await send(channel, "Y", 20000, 800);
|
|
4899
7625
|
return answer;
|
|
4900
7626
|
}
|
|
4901
|
-
async function
|
|
4902
|
-
const client = await
|
|
7627
|
+
async function apply5(device, _network, plan) {
|
|
7628
|
+
const client = await login4(device);
|
|
4903
7629
|
const channel = await openShell(client);
|
|
4904
7630
|
try {
|
|
4905
7631
|
await send(channel, "system-view", 1e4, 1500);
|
|
4906
7632
|
for (const step of plan.steps) {
|
|
4907
7633
|
for (const line of step.send) {
|
|
4908
7634
|
const answer = await sendLine(channel, line);
|
|
4909
|
-
if (
|
|
7635
|
+
if (REFUSED4.test(answer))
|
|
4910
7636
|
throw new Error(`${step.title}: ${line}
|
|
4911
7637
|
device said: ${answer.trim().slice(0, 300)}`);
|
|
4912
7638
|
}
|
|
4913
7639
|
}
|
|
4914
7640
|
const committed = await send(channel, "commit", 60000, 3000);
|
|
4915
|
-
if (
|
|
7641
|
+
if (REFUSED4.test(committed))
|
|
4916
7642
|
throw new Error(`commit refused: ${committed.trim().slice(0, 300)}`);
|
|
4917
7643
|
await send(channel, "quit", 1e4);
|
|
4918
7644
|
} catch (error) {
|
|
@@ -4923,7 +7649,7 @@ async function apply4(device, _network, plan) {
|
|
|
4923
7649
|
client.end();
|
|
4924
7650
|
throw error;
|
|
4925
7651
|
}
|
|
4926
|
-
const proof = await
|
|
7652
|
+
const proof = await login4(device).catch(() => {
|
|
4927
7653
|
return;
|
|
4928
7654
|
});
|
|
4929
7655
|
if (!proof) {
|
|
@@ -4939,8 +7665,8 @@ async function apply4(device, _network, plan) {
|
|
|
4939
7665
|
client.end();
|
|
4940
7666
|
console.log(" committed, login proved, saved");
|
|
4941
7667
|
}
|
|
4942
|
-
function
|
|
4943
|
-
const groups =
|
|
7668
|
+
function plan5(desired, current) {
|
|
7669
|
+
const groups = compare2(desired, current, dialect2);
|
|
4944
7670
|
vlanListChange(groups, "vlan batch ", batchIds, (ids) => [`vlan batch ${vlanBatch(ids)}`], (ids) => [`vlan batch ${vlanBatch(ids)}`]);
|
|
4945
7671
|
const top = groups.find((group) => group.path.length === 0);
|
|
4946
7672
|
const removedBatch = top?.remove.find((line) => line.startsWith("vlan batch ")) ?? "";
|
|
@@ -4961,15 +7687,15 @@ function plan4(desired, current) {
|
|
|
4961
7687
|
}
|
|
4962
7688
|
var vrp = {
|
|
4963
7689
|
extension: ".cfg",
|
|
4964
|
-
unsupported:
|
|
4965
|
-
render:
|
|
4966
|
-
read:
|
|
4967
|
-
plan:
|
|
4968
|
-
apply:
|
|
7690
|
+
unsupported: unsupported5,
|
|
7691
|
+
render: render5,
|
|
7692
|
+
read: read5,
|
|
7693
|
+
plan: plan5,
|
|
7694
|
+
apply: apply5
|
|
4969
7695
|
};
|
|
4970
7696
|
|
|
4971
7697
|
// src/adapters/devices/index.ts
|
|
4972
|
-
var adapters = { routeros, vrp, "raisecom-ros": raisecom, junos };
|
|
7698
|
+
var adapters = { routeros, vrp, "raisecom-ros": raisecom, junos, fortios };
|
|
4973
7699
|
|
|
4974
7700
|
// src/adapters/registries/index.ts
|
|
4975
7701
|
async function ripeStat(query, family) {
|
|
@@ -4990,7 +7716,7 @@ var registries = { "ripe-stat": ripeStat };
|
|
|
4990
7716
|
// src/adapters/registries/librenms.ts
|
|
4991
7717
|
var AUTH = { sha1: "SHA", sha256: "SHA-256" };
|
|
4992
7718
|
var PRIVACY = { aes128: "AES", des: "DES" };
|
|
4993
|
-
function
|
|
7719
|
+
function settingsOf4(network) {
|
|
4994
7720
|
if (!network.librenms)
|
|
4995
7721
|
throw new Error("the network has no librenms settings");
|
|
4996
7722
|
return {
|
|
@@ -5017,7 +7743,7 @@ function credentials(device) {
|
|
|
5017
7743
|
return;
|
|
5018
7744
|
}
|
|
5019
7745
|
async function planLibreNms(network) {
|
|
5020
|
-
const { base, headers } =
|
|
7746
|
+
const { base, headers } = settingsOf4(network);
|
|
5021
7747
|
const response = await fetch(`${base}/devices?type=all`, { headers, signal: AbortSignal.timeout(30000) });
|
|
5022
7748
|
if (!response.ok)
|
|
5023
7749
|
throw new Error(`LibreNMS answered ${response.status} to the device list`);
|
|
@@ -5031,7 +7757,7 @@ async function planLibreNms(network) {
|
|
|
5031
7757
|
};
|
|
5032
7758
|
}
|
|
5033
7759
|
async function sendLibreNms(network, additions) {
|
|
5034
|
-
const { base, headers } =
|
|
7760
|
+
const { base, headers } = settingsOf4(network);
|
|
5035
7761
|
for (const device of additions) {
|
|
5036
7762
|
const body = { hostname: device.connection.host, display_template: device.name, ...credentials(device) };
|
|
5037
7763
|
const response = await fetch(`${base}/devices`, {
|
|
@@ -5050,10 +7776,10 @@ async function sendLibreNms(network, additions) {
|
|
|
5050
7776
|
|
|
5051
7777
|
// src/adapters/registries/peeringdb.ts
|
|
5052
7778
|
var API = "https://www.peeringdb.com/api";
|
|
5053
|
-
var
|
|
7779
|
+
var addressesOf3 = (iface) => (iface.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
|
|
5054
7780
|
function peersWithRouteServer(device, iface) {
|
|
5055
7781
|
const bgp = device.routing?.bgp;
|
|
5056
|
-
const subnets =
|
|
7782
|
+
const subnets = addressesOf3(iface);
|
|
5057
7783
|
return Object.values(bgp?.neighbors ?? {}).some((neighbor) => {
|
|
5058
7784
|
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
5059
7785
|
const role = neighbor.role ?? group?.role;
|
|
@@ -5067,7 +7793,7 @@ function wanted(network) {
|
|
|
5067
7793
|
const exchange = iface.exchange;
|
|
5068
7794
|
if (!exchange?.peeringdb_ixlan)
|
|
5069
7795
|
continue;
|
|
5070
|
-
const published =
|
|
7796
|
+
const published = addressesOf3(iface).map(host2).filter(isGlobal2);
|
|
5071
7797
|
records.push({
|
|
5072
7798
|
name: exchange.name,
|
|
5073
7799
|
ixlan_id: exchange.peeringdb_ixlan,
|
|
@@ -5983,7 +8709,7 @@ async function load(path) {
|
|
|
5983
8709
|
const { createJiti } = await import("jiti");
|
|
5984
8710
|
return createJiti(import.meta.url).import(path, { default: true });
|
|
5985
8711
|
}
|
|
5986
|
-
var
|
|
8712
|
+
var network3 = await load(configPath);
|
|
5987
8713
|
var flags = rest.filter((arg) => arg.startsWith("--"));
|
|
5988
8714
|
var names = rest.filter((arg) => !arg.startsWith("--"));
|
|
5989
8715
|
var confirmed = flags.includes("--confirm");
|
|
@@ -5997,9 +8723,9 @@ function fail(message) {
|
|
|
5997
8723
|
}
|
|
5998
8724
|
function selected(allByDefault) {
|
|
5999
8725
|
if (!names.length && allByDefault)
|
|
6000
|
-
return [...
|
|
6001
|
-
const known =
|
|
6002
|
-
const byName = (name) =>
|
|
8726
|
+
return [...network3.devices];
|
|
8727
|
+
const known = network3.devices.map((device) => device.name).join(", ");
|
|
8728
|
+
const byName = (name) => network3.devices.find((device) => device.name === name);
|
|
6003
8729
|
return names.map((name) => byName(name) ?? fail(`no device ${name}; known: ${known}`));
|
|
6004
8730
|
}
|
|
6005
8731
|
function one() {
|
|
@@ -6023,7 +8749,7 @@ function writeState(device, text) {
|
|
|
6023
8749
|
}
|
|
6024
8750
|
function guard(devices) {
|
|
6025
8751
|
const relevant = new Set(devices.map((device) => device.name));
|
|
6026
|
-
const findings = validate(
|
|
8752
|
+
const findings = validate(network3).filter((finding) => !finding.device || relevant.has(finding.device));
|
|
6027
8753
|
for (const finding of findings) {
|
|
6028
8754
|
const where = finding.device ? `${finding.device}: ` : "";
|
|
6029
8755
|
const level = finding.level === "error" ? "error" : "warn ";
|
|
@@ -6048,8 +8774,8 @@ function show(device, plan) {
|
|
|
6048
8774
|
return commands + plan.problems.length;
|
|
6049
8775
|
}
|
|
6050
8776
|
function validateCommand() {
|
|
6051
|
-
guard([...
|
|
6052
|
-
console.log(`${
|
|
8777
|
+
guard([...network3.devices]);
|
|
8778
|
+
console.log(`${network3.devices.length} device(s), no errors.`);
|
|
6053
8779
|
}
|
|
6054
8780
|
function buildCommand() {
|
|
6055
8781
|
const device = one();
|
|
@@ -6073,14 +8799,14 @@ function diffCommand() {
|
|
|
6073
8799
|
}
|
|
6074
8800
|
async function snapshotCommand() {
|
|
6075
8801
|
for (const device of selected(true)) {
|
|
6076
|
-
const text = await adapterOf(device).read(device,
|
|
8802
|
+
const text = await adapterOf(device).read(device, network3.devices);
|
|
6077
8803
|
writeState(device, text);
|
|
6078
8804
|
console.log(`${device.name}: ${text.split(`
|
|
6079
8805
|
`).length} lines`);
|
|
6080
8806
|
}
|
|
6081
8807
|
if (names.length || !existsSync3(stateDirectory))
|
|
6082
8808
|
return;
|
|
6083
|
-
const current = new Set(
|
|
8809
|
+
const current = new Set(network3.devices.map((device) => basename2(statePath(device))));
|
|
6084
8810
|
for (const file of readdirSync2(stateDirectory)) {
|
|
6085
8811
|
if (current.has(file))
|
|
6086
8812
|
continue;
|
|
@@ -6092,7 +8818,7 @@ async function applyCommand() {
|
|
|
6092
8818
|
const device = one();
|
|
6093
8819
|
guard([device]);
|
|
6094
8820
|
const adapter = adapterOf(device);
|
|
6095
|
-
const live = await adapter.read(device,
|
|
8821
|
+
const live = await adapter.read(device, network3.devices);
|
|
6096
8822
|
writeState(device, live);
|
|
6097
8823
|
const plan = adapter.plan(adapter.render(device), live, options);
|
|
6098
8824
|
if (!show(device, plan))
|
|
@@ -6105,8 +8831,8 @@ Not sent: resolve the problems first.`);
|
|
|
6105
8831
|
Nothing sent. Re-run with --confirm.`);
|
|
6106
8832
|
return;
|
|
6107
8833
|
}
|
|
6108
|
-
await adapter.apply(device,
|
|
6109
|
-
writeState(device, await adapter.read(device,
|
|
8834
|
+
await adapter.apply(device, network3.devices, plan, options);
|
|
8835
|
+
writeState(device, await adapter.read(device, network3.devices));
|
|
6110
8836
|
console.log("snapshot updated");
|
|
6111
8837
|
}
|
|
6112
8838
|
async function refreshCommand() {
|
|
@@ -6122,7 +8848,7 @@ async function refreshCommand() {
|
|
|
6122
8848
|
}
|
|
6123
8849
|
console.log(`${name}: ${prefixes.length} prefixes from ${registry} ${query}`);
|
|
6124
8850
|
if (confirmed)
|
|
6125
|
-
await fill(device,
|
|
8851
|
+
await fill(device, network3.devices, name, family, prefixes);
|
|
6126
8852
|
}
|
|
6127
8853
|
if (!confirmed)
|
|
6128
8854
|
console.log(`
|
|
@@ -6137,7 +8863,7 @@ function publicKeyOf(privateKey) {
|
|
|
6137
8863
|
}
|
|
6138
8864
|
function wireguardCommand() {
|
|
6139
8865
|
const [deviceName, interfaceName, peerName] = names;
|
|
6140
|
-
const device =
|
|
8866
|
+
const device = network3.devices.find((candidate) => candidate.name === deviceName) ?? fail(HELP);
|
|
6141
8867
|
const iface = device.interfaces?.[interfaceName ?? ""];
|
|
6142
8868
|
if (iface?.type !== "wireguard")
|
|
6143
8869
|
fail(`${deviceName} has no WireGuard interface ${interfaceName}`);
|
|
@@ -6162,7 +8888,7 @@ function wireguardCommand() {
|
|
|
6162
8888
|
`));
|
|
6163
8889
|
}
|
|
6164
8890
|
function communitiesCommand() {
|
|
6165
|
-
const communities =
|
|
8891
|
+
const communities = network3.communities ?? fail("the network declares no communities");
|
|
6166
8892
|
for (const { community, description } of communities) {
|
|
6167
8893
|
if (community.includes(","))
|
|
6168
8894
|
fail(`community ${community} holds a comma`);
|
|
@@ -6170,7 +8896,7 @@ function communitiesCommand() {
|
|
|
6170
8896
|
}
|
|
6171
8897
|
}
|
|
6172
8898
|
async function peeringdbCommand() {
|
|
6173
|
-
const plan = await planPeeringDb(
|
|
8899
|
+
const plan = await planPeeringDb(network3);
|
|
6174
8900
|
for (const name of plan.unknown) {
|
|
6175
8901
|
console.log(` PeeringDB lists ${name}, which the config does not. Remove it by hand if the network left.`);
|
|
6176
8902
|
}
|
|
@@ -6185,10 +8911,10 @@ async function peeringdbCommand() {
|
|
|
6185
8911
|
Nothing sent. Re-run with --confirm.`);
|
|
6186
8912
|
return;
|
|
6187
8913
|
}
|
|
6188
|
-
await sendPeeringDb(
|
|
8914
|
+
await sendPeeringDb(network3, plan.changes);
|
|
6189
8915
|
}
|
|
6190
8916
|
async function librenmsCommand() {
|
|
6191
|
-
const plan = await planLibreNms(
|
|
8917
|
+
const plan = await planLibreNms(network3);
|
|
6192
8918
|
for (const hostname of plan.unknown)
|
|
6193
8919
|
console.log(` LibreNMS monitors ${hostname}, which the config does not. Remove it by hand if it is gone.`);
|
|
6194
8920
|
for (const device of plan.withoutSnmp)
|
|
@@ -6204,17 +8930,17 @@ async function librenmsCommand() {
|
|
|
6204
8930
|
Nothing sent. Re-run with --confirm.`);
|
|
6205
8931
|
return;
|
|
6206
8932
|
}
|
|
6207
|
-
await sendLibreNms(
|
|
8933
|
+
await sendLibreNms(network3, plan.additions);
|
|
6208
8934
|
}
|
|
6209
8935
|
function prometheusCommand() {
|
|
6210
8936
|
if (flags.includes("--auths")) {
|
|
6211
|
-
const { yaml, notes } = snmpExporterAuths(
|
|
8937
|
+
const { yaml, notes } = snmpExporterAuths(network3);
|
|
6212
8938
|
for (const note of notes)
|
|
6213
8939
|
console.error(note);
|
|
6214
8940
|
process.stdout.write(yaml);
|
|
6215
8941
|
return;
|
|
6216
8942
|
}
|
|
6217
|
-
const { targets, withoutSnmp } = prometheusTargets(
|
|
8943
|
+
const { targets, withoutSnmp } = prometheusTargets(network3);
|
|
6218
8944
|
for (const device of withoutSnmp)
|
|
6219
8945
|
console.error(`${device.name} has no SNMP in its config; left out`);
|
|
6220
8946
|
console.log(JSON.stringify(targets, null, 4));
|
|
@@ -6231,7 +8957,7 @@ function secretsIn(value, found = new Set) {
|
|
|
6231
8957
|
return found;
|
|
6232
8958
|
}
|
|
6233
8959
|
function secretsCommand() {
|
|
6234
|
-
const references = [...secretsIn([
|
|
8960
|
+
const references = [...secretsIn([network3.devices, network3.peeringdb, network3.librenms])].sort();
|
|
6235
8961
|
let missing = 0;
|
|
6236
8962
|
for (const reference of references) {
|
|
6237
8963
|
const name = reference.replace(/^file:/, "");
|
|
@@ -6265,12 +8991,12 @@ function driftOf(device) {
|
|
|
6265
8991
|
}
|
|
6266
8992
|
}
|
|
6267
8993
|
function annotatedGraph(layers, deviceNames) {
|
|
6268
|
-
const graph = only(graphOf(
|
|
8994
|
+
const graph = only(graphOf(network3.devices), layers, deviceNames);
|
|
6269
8995
|
const shown = new Set(graph.nodes.map((node) => node.label));
|
|
6270
8996
|
return {
|
|
6271
8997
|
...graph,
|
|
6272
|
-
findings: validate(
|
|
6273
|
-
drift: Object.fromEntries(
|
|
8998
|
+
findings: validate(network3).filter((finding) => !finding.device || shown.has(finding.device)),
|
|
8999
|
+
drift: Object.fromEntries(network3.devices.filter((device) => shown.has(device.name)).map((device) => [device.name, driftOf(device)]))
|
|
6274
9000
|
};
|
|
6275
9001
|
}
|
|
6276
9002
|
function graphCommand() {
|
|
@@ -6306,7 +9032,7 @@ function addressesCommand() {
|
|
|
6306
9032
|
const length = lengthText === undefined ? undefined : Number(lengthText);
|
|
6307
9033
|
if (length !== undefined && !(Number.isInteger(length) && length >= 0 && length <= 128))
|
|
6308
9034
|
fail("--length takes a prefix length, such as 24");
|
|
6309
|
-
const report = addressesOf(
|
|
9035
|
+
const report = addressesOf(network3.devices, within);
|
|
6310
9036
|
if (flagValue("format") === "json") {
|
|
6311
9037
|
console.log(JSON.stringify(report, null, 4));
|
|
6312
9038
|
return;
|
|
@@ -6341,10 +9067,10 @@ function renameCommand() {
|
|
|
6341
9067
|
const [from, to] = names;
|
|
6342
9068
|
if (!from || !to)
|
|
6343
9069
|
fail("usage: circuit rename <device> <new-name> [--file] [--confirm]");
|
|
6344
|
-
const device =
|
|
9070
|
+
const device = network3.devices.find((candidate) => candidate.name === from);
|
|
6345
9071
|
if (!device)
|
|
6346
|
-
fail(`no device ${from}; known: ${
|
|
6347
|
-
if (
|
|
9072
|
+
fail(`no device ${from}; known: ${network3.devices.map((candidate) => candidate.name).join(", ")}`);
|
|
9073
|
+
if (network3.devices.some((candidate) => candidate.name === to))
|
|
6348
9074
|
fail(`${to} is already a device`);
|
|
6349
9075
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(to))
|
|
6350
9076
|
fail(`${to} is not a hostname: letters, digits, dots, dashes and underscores`);
|
|
@@ -6381,7 +9107,7 @@ Renamed. Run circuit validate, then circuit apply ${to}: the device keeps its ol
|
|
|
6381
9107
|
async function studioCommand() {
|
|
6382
9108
|
if (flags.includes("--json")) {
|
|
6383
9109
|
const sources = projectSources(root2);
|
|
6384
|
-
const data = studioData(
|
|
9110
|
+
const data = studioData(network3.devices, annotatedGraph(LAYERS, []), {
|
|
6385
9111
|
render: (device) => adapterOf(device).render(device),
|
|
6386
9112
|
snapshot: (device) => existsSync3(statePath(device)) ? readFileSync4(statePath(device), "utf8") : undefined,
|
|
6387
9113
|
files: (device) => ({
|
|
@@ -6395,7 +9121,7 @@ async function studioCommand() {
|
|
|
6395
9121
|
const port = Number(flagValue("port") ?? 4600);
|
|
6396
9122
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
6397
9123
|
fail("--port takes a number from 1 to 65535");
|
|
6398
|
-
const { isLoopback, studio } = await import("../chunks/server-
|
|
9124
|
+
const { isLoopback, studio } = await import("../chunks/server-1ftkqkx5.js");
|
|
6399
9125
|
const host = flagValue("host") ?? "127.0.0.1";
|
|
6400
9126
|
const allowHosts = (flagValue("allow-host") ?? "").split(",").filter(Boolean);
|
|
6401
9127
|
if (!isLoopback(host) && !allowHosts.length) {
|