@takodotid/circuit 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/chunks/{index-n4zpd25n.js → index-wm9ypkjq.js} +14 -6
- package/dist/chunks/{server-ckdy25w2.js → server-n2q4tzy4.js} +1 -1
- package/dist/cli/index.js +1733 -162
- package/dist/presets/index.js +5 -0
- package/dist/types/adapters/devices/catalog.d.ts +14 -0
- package/dist/types/adapters/devices/junos/bgp.d.ts +11 -0
- package/dist/types/adapters/devices/junos/index.d.ts +39 -0
- package/dist/types/adapters/devices/junos/models.d.ts +14 -0
- package/dist/types/adapters/devices/junos/render.d.ts +52 -0
- package/dist/types/core/graph.d.ts +2 -0
- package/dist/types/presets/index.d.ts +1 -1
- package/package.json +4 -2
- package/src/adapters/devices/catalog.ts +4 -2
- package/src/adapters/devices/index.ts +2 -1
- package/src/adapters/devices/junos/TRAPS.md +31 -0
- package/src/adapters/devices/junos/bgp.ts +263 -0
- package/src/adapters/devices/junos/index.ts +784 -0
- package/src/adapters/devices/junos/models.ts +7 -0
- package/src/adapters/devices/junos/render.ts +868 -0
- package/src/core/across.ts +6 -3
- package/src/core/graph.ts +3 -1
- package/src/core/validate.ts +4 -3
- package/src/presets/index.ts +7 -1
- package/templates/colocation/checks.ts +8 -1
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-wm9ypkjq.js";
|
|
15
15
|
import {
|
|
16
16
|
familyOf2,
|
|
17
17
|
split,
|
|
@@ -110,20 +110,6 @@ var MARKER = /<secret:([A-Za-z0-9_./:-]+)>/g;
|
|
|
110
110
|
var substitute = (text) => text.replace(MARKER, (_, reference) => valueOf(reference));
|
|
111
111
|
var valueOfReference = valueOf;
|
|
112
112
|
|
|
113
|
-
// src/core/time.ts
|
|
114
|
-
function offset(zone) {
|
|
115
|
-
const name = new Intl.DateTimeFormat("en-US", {
|
|
116
|
-
timeZone: zone,
|
|
117
|
-
timeZoneName: "longOffset"
|
|
118
|
-
}).formatToParts(new Date).find((entry) => entry.type === "timeZoneName").value;
|
|
119
|
-
const found = /GMT([+-])(\d{2}):(\d{2})/.exec(name);
|
|
120
|
-
if (!found)
|
|
121
|
-
return 0;
|
|
122
|
-
const [, sign, hours, minutes] = found;
|
|
123
|
-
const sizeInMinutes = Number(hours) * 60 + Number(minutes);
|
|
124
|
-
return sign === "-" ? -sizeInMinutes : sizeInMinutes;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
113
|
// src/transport/ssh.ts
|
|
128
114
|
import { Client } from "ssh2";
|
|
129
115
|
function connect(login) {
|
|
@@ -264,6 +250,1588 @@ function switchUnsupported(device) {
|
|
|
264
250
|
return found;
|
|
265
251
|
}
|
|
266
252
|
|
|
253
|
+
// src/adapters/devices/junos/render.ts
|
|
254
|
+
var USER_CLASS = { admin: "super-user", operator: "operator", "read-only": "read-only" };
|
|
255
|
+
var KEY_STATEMENT = {
|
|
256
|
+
"ssh-ed25519": "ssh-ed25519",
|
|
257
|
+
"ssh-rsa": "ssh-rsa",
|
|
258
|
+
"ecdsa-sha2-nistp256": "ssh-ecdsa",
|
|
259
|
+
"ecdsa-sha2-nistp384": "ssh-ecdsa",
|
|
260
|
+
"ecdsa-sha2-nistp521": "ssh-ecdsa"
|
|
261
|
+
};
|
|
262
|
+
var SNMP_AUTH = { sha1: "authentication-sha", sha256: "authentication-sha256" };
|
|
263
|
+
var SNMP_PRIVACY = { aes128: "privacy-aes128", des: "privacy-des" };
|
|
264
|
+
var SNMP_GROUP = "circuit";
|
|
265
|
+
var LEVEL = {
|
|
266
|
+
emergency: "emergency",
|
|
267
|
+
alert: "alert",
|
|
268
|
+
critical: "critical",
|
|
269
|
+
error: "error",
|
|
270
|
+
warning: "warning",
|
|
271
|
+
notice: "notice",
|
|
272
|
+
info: "info",
|
|
273
|
+
debug: "any"
|
|
274
|
+
};
|
|
275
|
+
var SERVICES_FILTER = { inet: "circuit-services", inet6: "circuit-services-v6" };
|
|
276
|
+
var OSPF_EXPORT = "circuit-ospf-redistribute";
|
|
277
|
+
var SSH_CIPHERS = ["aes256-gcm@openssh.com", "aes128-gcm@openssh.com", "aes256-ctr", "aes192-ctr", "aes128-ctr"];
|
|
278
|
+
var SSH_MACS = ["hmac-sha2-512", "hmac-sha2-256"];
|
|
279
|
+
var SSH_KEY_EXCHANGES = ["curve25519-sha256", "ecdh-sha2-nistp521", "ecdh-sha2-nistp384", "ecdh-sha2-nistp256", "group-exchange-sha2"];
|
|
280
|
+
var SERVICE_PORTS = {
|
|
281
|
+
ssh: ["tcp", 22],
|
|
282
|
+
telnet: ["tcp", 23],
|
|
283
|
+
ftp: ["tcp", 21],
|
|
284
|
+
snmp: ["udp", 161],
|
|
285
|
+
http: ["tcp", 80],
|
|
286
|
+
api: ["tcp", 3000],
|
|
287
|
+
native: ["tcp", 830]
|
|
288
|
+
};
|
|
289
|
+
var BFD_INTERVAL = 300;
|
|
290
|
+
function quote(value) {
|
|
291
|
+
if (/^[\w.:/@-]+$/.test(value))
|
|
292
|
+
return value;
|
|
293
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
294
|
+
return `"${escaped}"`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
class Statements {
|
|
298
|
+
lines = [];
|
|
299
|
+
notes = [];
|
|
300
|
+
set(...words) {
|
|
301
|
+
this.lines.push(`set ${words.join(" ")}`);
|
|
302
|
+
}
|
|
303
|
+
note(comment) {
|
|
304
|
+
this.notes.push(comment);
|
|
305
|
+
}
|
|
306
|
+
text() {
|
|
307
|
+
const unique = [...new Set(this.lines)];
|
|
308
|
+
const extended = new Set;
|
|
309
|
+
for (const line of unique) {
|
|
310
|
+
const words = line.split(" ");
|
|
311
|
+
for (let length = 2;length < words.length; length++)
|
|
312
|
+
extended.add(words.slice(0, length).join(" "));
|
|
313
|
+
}
|
|
314
|
+
return [...unique.filter((line) => !extended.has(line)), ...this.notes].join(`
|
|
315
|
+
`) + `
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
class Names {
|
|
321
|
+
device;
|
|
322
|
+
lags;
|
|
323
|
+
loopbacks = new Map;
|
|
324
|
+
tunnels = new Map;
|
|
325
|
+
constructor(device) {
|
|
326
|
+
this.device = device;
|
|
327
|
+
this.lags = lagIds(device);
|
|
328
|
+
const loopbacks = Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "loopback");
|
|
329
|
+
let unit = 0;
|
|
330
|
+
for (const [name, iface] of loopbacks)
|
|
331
|
+
if (!iface.vrf)
|
|
332
|
+
this.loopbacks.set(name, 0);
|
|
333
|
+
for (const [name, iface] of loopbacks)
|
|
334
|
+
if (iface.vrf)
|
|
335
|
+
this.loopbacks.set(name, ++unit);
|
|
336
|
+
const tunnels = Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "gre");
|
|
337
|
+
tunnels.forEach(([name], index) => this.tunnels.set(name, index));
|
|
338
|
+
}
|
|
339
|
+
port(port) {
|
|
340
|
+
const name = spell(this.device.platform, this.device.model, port);
|
|
341
|
+
const settings = this.device.ports?.[port];
|
|
342
|
+
return settings?.speed === "1g" ? name.replace(/^xe-/, "ge-") : name;
|
|
343
|
+
}
|
|
344
|
+
physical(name) {
|
|
345
|
+
if (this.device.interfaces?.[name]?.type === "lag")
|
|
346
|
+
return `ae${this.lags.get(name)}`;
|
|
347
|
+
return this.port(name);
|
|
348
|
+
}
|
|
349
|
+
unit(name) {
|
|
350
|
+
const iface = this.device.interfaces?.[name];
|
|
351
|
+
if (iface?.type === "vlan")
|
|
352
|
+
return `irb.${this.device.vlans[iface.vlan].id}`;
|
|
353
|
+
if (iface?.type === "loopback")
|
|
354
|
+
return `lo0.${this.loopbacks.get(name)}`;
|
|
355
|
+
if (iface?.type === "gre")
|
|
356
|
+
return `gr-0/0/0.${this.tunnels.get(name)}`;
|
|
357
|
+
return `${this.physical(name)}.0`;
|
|
358
|
+
}
|
|
359
|
+
members(name) {
|
|
360
|
+
if (this.device.interfaces?.[name]?.type !== "lag")
|
|
361
|
+
return [this.port(name)];
|
|
362
|
+
const ports = Object.entries(this.device.ports ?? {}).filter(([, settings]) => settings?.lag === name);
|
|
363
|
+
return ports.map(([port]) => this.port(port));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function vrrpAddress(addresses, group) {
|
|
367
|
+
return addresses.find((address) => familyOf2(address) === "ipv4" && contains2(address, group.address));
|
|
368
|
+
}
|
|
369
|
+
var STORM_KINDS = [
|
|
370
|
+
["broadcast", "no-broadcast"],
|
|
371
|
+
["multicast", "no-multicast"],
|
|
372
|
+
["unknown_unicast", "no-unknown-unicast"]
|
|
373
|
+
];
|
|
374
|
+
function stormProfile(settings) {
|
|
375
|
+
const storm = settings.storm_control;
|
|
376
|
+
if (!storm)
|
|
377
|
+
return;
|
|
378
|
+
const limited = STORM_KINDS.filter(([kind]) => storm[kind]);
|
|
379
|
+
const first = limited[0] && storm[limited[0][0]];
|
|
380
|
+
if (!first || !("percent" in first))
|
|
381
|
+
return;
|
|
382
|
+
const kinds = limited.map(([kind]) => kind.replace("_", "-"));
|
|
383
|
+
const left = STORM_KINDS.filter(([kind]) => !storm[kind]).map(([, statement]) => statement);
|
|
384
|
+
return { name: `${kinds.join("-")}-${first.percent}-percent`, percent: first.percent, left };
|
|
385
|
+
}
|
|
386
|
+
function render(device) {
|
|
387
|
+
const out = new Statements;
|
|
388
|
+
const names = new Names(device);
|
|
389
|
+
system(out, device);
|
|
390
|
+
users(out, device);
|
|
391
|
+
services(out, device);
|
|
392
|
+
snmp(out, device);
|
|
393
|
+
vlans(out, device, names);
|
|
394
|
+
ports(out, device, names);
|
|
395
|
+
lags(out, device, names);
|
|
396
|
+
routedInterfaces(out, device, names);
|
|
397
|
+
stormProfiles(out, device);
|
|
398
|
+
servicesFilter(out, device, names);
|
|
399
|
+
vrfs(out, device, names);
|
|
400
|
+
staticRoutes(out, device, names);
|
|
401
|
+
spanningTree(out, device, names);
|
|
402
|
+
lldp(out, device, names);
|
|
403
|
+
ospf(out, device, names);
|
|
404
|
+
dhcpRelay(out, device, names);
|
|
405
|
+
dhcpServers(out, device, names);
|
|
406
|
+
routerAdvertisements(out, device, names);
|
|
407
|
+
acls(out, device, names);
|
|
408
|
+
renderBgp(out, device);
|
|
409
|
+
sflow(out, device, names);
|
|
410
|
+
return out.text();
|
|
411
|
+
}
|
|
412
|
+
function system(out, device) {
|
|
413
|
+
out.set("system host-name", quote(device.name));
|
|
414
|
+
const system = device.system ?? {};
|
|
415
|
+
if (system.timezone)
|
|
416
|
+
out.set("system time-zone", system.timezone);
|
|
417
|
+
if (system.banner)
|
|
418
|
+
out.set("system login message", quote(system.banner));
|
|
419
|
+
for (const server of system.dns?.servers ?? [])
|
|
420
|
+
out.set("system name-server", server);
|
|
421
|
+
for (const server of system.ntp?.servers ?? [])
|
|
422
|
+
out.set("system ntp server", server);
|
|
423
|
+
if (!system.ip?.icmp_redirects) {
|
|
424
|
+
out.set("system no-redirects");
|
|
425
|
+
out.set("system no-redirects-ipv6");
|
|
426
|
+
}
|
|
427
|
+
const local = system.logging?.local;
|
|
428
|
+
if (local) {
|
|
429
|
+
out.set("system syslog file messages any", LEVEL[local.level]);
|
|
430
|
+
if (local.files !== undefined)
|
|
431
|
+
out.set("system syslog file messages archive files", local.files);
|
|
432
|
+
}
|
|
433
|
+
for (const remote of system.logging?.remote ?? []) {
|
|
434
|
+
out.set("system syslog host", remote.address, "any", LEVEL[remote.level]);
|
|
435
|
+
if (remote.port !== undefined && remote.port !== 514)
|
|
436
|
+
out.set("system syslog host", remote.address, "port", remote.port);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function users(out, device) {
|
|
440
|
+
for (const [name, user] of Object.entries(device.users ?? {})) {
|
|
441
|
+
const login = name === "root" ? "system root-authentication" : `system login user ${name} authentication`;
|
|
442
|
+
if (name !== "root") {
|
|
443
|
+
out.set(`system login user ${name} class`, USER_CLASS[user.role]);
|
|
444
|
+
if (user.description)
|
|
445
|
+
out.set(`system login user ${name} full-name`, quote(user.description));
|
|
446
|
+
}
|
|
447
|
+
out.set(login, "plain-text-password-value", marker(user.password));
|
|
448
|
+
for (const key of user.ssh_keys ?? [])
|
|
449
|
+
out.set(login, KEY_STATEMENT[key.split(" ")[0]], quote(key));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function services(out, device) {
|
|
453
|
+
const management = device.management ?? {};
|
|
454
|
+
const ssh = management.ssh;
|
|
455
|
+
if (ssh) {
|
|
456
|
+
out.set("system services ssh");
|
|
457
|
+
if (ssh.port !== undefined && ssh.port !== 22)
|
|
458
|
+
out.set("system services ssh port", ssh.port);
|
|
459
|
+
if (!ssh.weak_crypto) {
|
|
460
|
+
for (const cipher of SSH_CIPHERS)
|
|
461
|
+
out.set("system services ssh ciphers", cipher);
|
|
462
|
+
for (const mac of SSH_MACS)
|
|
463
|
+
out.set("system services ssh macs", mac);
|
|
464
|
+
for (const exchange of SSH_KEY_EXCHANGES)
|
|
465
|
+
out.set("system services ssh key-exchange", exchange);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (management.telnet)
|
|
469
|
+
out.set("system services telnet");
|
|
470
|
+
if (management.ftp)
|
|
471
|
+
out.set("system services ftp");
|
|
472
|
+
const others = [
|
|
473
|
+
["http", "system services web-management http"],
|
|
474
|
+
["api", "system services rest http"],
|
|
475
|
+
["native", "system services netconf ssh"]
|
|
476
|
+
];
|
|
477
|
+
for (const [service, statement] of others) {
|
|
478
|
+
const settings = management[service];
|
|
479
|
+
if (!settings)
|
|
480
|
+
continue;
|
|
481
|
+
out.set(statement);
|
|
482
|
+
if (settings.port !== undefined && settings.port !== SERVICE_PORTS[service][1])
|
|
483
|
+
out.set(statement, "port", settings.port);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function snmp(out, device) {
|
|
487
|
+
const snmp = device.management?.snmp;
|
|
488
|
+
if (!snmp)
|
|
489
|
+
return;
|
|
490
|
+
if (snmp.contact)
|
|
491
|
+
out.set("snmp contact", quote(snmp.contact));
|
|
492
|
+
if (snmp.location)
|
|
493
|
+
out.set("snmp location", quote(snmp.location));
|
|
494
|
+
if (snmp.community)
|
|
495
|
+
out.set("snmp community", marker(snmp.community), "authorization read-only");
|
|
496
|
+
if (!snmp.users)
|
|
497
|
+
return;
|
|
498
|
+
for (const [name, user] of Object.entries(snmp.users)) {
|
|
499
|
+
const usm = `snmp v3 usm local-engine user ${name}`;
|
|
500
|
+
out.set(usm, SNMP_AUTH[user.auth], "authentication-password", marker(user.auth_password));
|
|
501
|
+
out.set(usm, SNMP_PRIVACY[user.privacy], "privacy-password", marker(user.privacy_password));
|
|
502
|
+
out.set("snmp v3 vacm security-to-group security-model usm security-name", name, "group", SNMP_GROUP);
|
|
503
|
+
}
|
|
504
|
+
out.set("snmp view all oid .1 include");
|
|
505
|
+
out.set(`snmp v3 vacm access group ${SNMP_GROUP} default-context-prefix security-model usm security-level privacy read-view all`);
|
|
506
|
+
}
|
|
507
|
+
function vlans(out, device, names) {
|
|
508
|
+
for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
|
|
509
|
+
out.set(`vlans ${name} vlan-id`, vlan.id);
|
|
510
|
+
if (vlan.description)
|
|
511
|
+
out.set(`vlans ${name} description`, quote(vlan.description));
|
|
512
|
+
}
|
|
513
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
514
|
+
if (iface.type === "vlan")
|
|
515
|
+
out.set(`vlans ${iface.vlan} l3-interface`, names.unit(name));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function switching(out, device, physical, settings) {
|
|
519
|
+
const family = `interfaces ${physical} unit 0 family ethernet-switching`;
|
|
520
|
+
if (settings.access_vlan) {
|
|
521
|
+
out.set(family, "interface-mode access");
|
|
522
|
+
out.set(family, "vlan members", settings.access_vlan);
|
|
523
|
+
}
|
|
524
|
+
if (settings.trunk_vlans) {
|
|
525
|
+
out.set(family, "interface-mode trunk");
|
|
526
|
+
const members = new Set([...settings.trunk_vlans, ...settings.native_vlan ? [settings.native_vlan] : []]);
|
|
527
|
+
for (const vlan of members)
|
|
528
|
+
out.set(family, "vlan members", vlan);
|
|
529
|
+
if (settings.native_vlan)
|
|
530
|
+
out.set(`interfaces ${physical} native-vlan-id`, device.vlans[settings.native_vlan].id);
|
|
531
|
+
}
|
|
532
|
+
const storm = stormProfile(settings);
|
|
533
|
+
if (storm)
|
|
534
|
+
out.set(family, "storm-control", storm.name);
|
|
535
|
+
}
|
|
536
|
+
function addressing(out, unit, settings) {
|
|
537
|
+
const [physical, number] = unit.split(".");
|
|
538
|
+
const prefix = `interfaces ${physical} unit ${number}`;
|
|
539
|
+
const addresses = (settings.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
|
|
540
|
+
for (const address of addresses)
|
|
541
|
+
out.set(`${prefix} family ${familyOf2(address) === "ipv4" ? "inet" : "inet6"} address`, address);
|
|
542
|
+
for (const group of settings.vrrp ?? []) {
|
|
543
|
+
const address = vrrpAddress(addresses, group);
|
|
544
|
+
if (!address)
|
|
545
|
+
continue;
|
|
546
|
+
const statement = `${prefix} family inet address ${address} vrrp-group ${group.id}`;
|
|
547
|
+
out.set(statement, "virtual-address", group.address);
|
|
548
|
+
if (group.priority !== undefined)
|
|
549
|
+
out.set(statement, "priority", group.priority);
|
|
550
|
+
if (!group.preempt)
|
|
551
|
+
out.set(statement, "no-preempt");
|
|
552
|
+
if (group.interval !== undefined && group.interval !== 1)
|
|
553
|
+
out.set(statement, "advertise-interval", group.interval);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function ports(out, device, names) {
|
|
557
|
+
for (const port of portsOf(device.platform, device.model)) {
|
|
558
|
+
const settings = device.ports?.[port];
|
|
559
|
+
const name = names.port(port);
|
|
560
|
+
if (name.startsWith("xe-"))
|
|
561
|
+
out.set(`interfaces ${name.replace("xe-", "ge-")} disable`);
|
|
562
|
+
if (name.startsWith("ge-"))
|
|
563
|
+
out.set(`interfaces ${name.replace("ge-", "xe-")} disable`);
|
|
564
|
+
if (!settings) {
|
|
565
|
+
out.set(`interfaces ${name} disable`);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (settings.description)
|
|
569
|
+
out.set(`interfaces ${name} description`, quote(settings.description));
|
|
570
|
+
if (settings.lag) {
|
|
571
|
+
out.set(`interfaces ${name} ether-options 802.3ad`, `ae${names.lags.get(settings.lag)}`);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const switched = !!(settings.access_vlan || settings.trunk_vlans);
|
|
575
|
+
if (settings.mtu !== undefined)
|
|
576
|
+
out.set(`interfaces ${name} mtu`, frameSize(settings.mtu, switched));
|
|
577
|
+
switching(out, device, name, settings);
|
|
578
|
+
addressing(out, `${name}.0`, settings);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function lags(out, device, names) {
|
|
582
|
+
let highest = -1;
|
|
583
|
+
for (const [name, lag] of Object.entries(device.interfaces ?? {})) {
|
|
584
|
+
if (lag.type !== "lag")
|
|
585
|
+
continue;
|
|
586
|
+
const physical = names.physical(name);
|
|
587
|
+
highest = Math.max(highest, names.lags.get(name));
|
|
588
|
+
if (lag.description)
|
|
589
|
+
out.set(`interfaces ${physical} description`, quote(lag.description));
|
|
590
|
+
if (lag.mode !== "static")
|
|
591
|
+
out.set(`interfaces ${physical} aggregated-ether-options lacp active`);
|
|
592
|
+
if (lag.mtu !== undefined)
|
|
593
|
+
out.set(`interfaces ${physical} mtu`, frameSize(lag.mtu, !!(lag.access_vlan || lag.trunk_vlans)));
|
|
594
|
+
switching(out, device, physical, lag);
|
|
595
|
+
addressing(out, `${physical}.0`, lag);
|
|
596
|
+
}
|
|
597
|
+
if (highest >= 0)
|
|
598
|
+
out.set("chassis aggregated-devices ethernet device-count", highest + 1);
|
|
599
|
+
}
|
|
600
|
+
function routedInterfaces(out, device, names) {
|
|
601
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
602
|
+
if (iface.type !== "vlan" && iface.type !== "loopback" && iface.type !== "gre")
|
|
603
|
+
continue;
|
|
604
|
+
const unit = names.unit(name);
|
|
605
|
+
const [physical, number] = unit.split(".");
|
|
606
|
+
const prefix = `interfaces ${physical} unit ${number}`;
|
|
607
|
+
if (iface.description)
|
|
608
|
+
out.set(`${prefix} description`, quote(iface.description));
|
|
609
|
+
if (iface.type === "gre") {
|
|
610
|
+
out.set(`${prefix} tunnel source`, iface.local);
|
|
611
|
+
out.set(`${prefix} tunnel destination`, iface.remote);
|
|
612
|
+
}
|
|
613
|
+
addressing(out, unit, iface);
|
|
614
|
+
if (iface.mtu === undefined)
|
|
615
|
+
continue;
|
|
616
|
+
const addresses = (iface.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
|
|
617
|
+
const families = new Set(addresses.map((address) => familyOf2(address) === "ipv4" ? "inet" : "inet6"));
|
|
618
|
+
for (const family of families)
|
|
619
|
+
out.set(`${prefix} family ${family} mtu`, iface.mtu);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function stormProfiles(out, device) {
|
|
623
|
+
const switched = [
|
|
624
|
+
...Object.values(device.ports ?? {}).filter((settings) => !!settings && !settings.lag),
|
|
625
|
+
...Object.values(device.interfaces ?? {}).filter((iface) => iface.type === "lag")
|
|
626
|
+
];
|
|
627
|
+
for (const settings of switched) {
|
|
628
|
+
const storm = stormProfile(settings);
|
|
629
|
+
if (!storm)
|
|
630
|
+
continue;
|
|
631
|
+
const profile = `forwarding-options storm-control-profiles ${storm.name} all`;
|
|
632
|
+
out.set(profile, "bandwidth-percentage", storm.percent);
|
|
633
|
+
for (const statement of storm.left)
|
|
634
|
+
out.set(profile, statement);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function servicesFilter(out, device, names) {
|
|
638
|
+
const management = device.management ?? {};
|
|
639
|
+
const guarded = [];
|
|
640
|
+
for (const service of Object.keys(SERVICE_PORTS)) {
|
|
641
|
+
const settings = management[service];
|
|
642
|
+
const sources = settings?.allow ?? management.allow;
|
|
643
|
+
if (settings && sources?.length)
|
|
644
|
+
guarded.push({ service, sources });
|
|
645
|
+
}
|
|
646
|
+
const ntp = device.system?.ntp;
|
|
647
|
+
const keepsTimeQuietly = !!ntp?.servers.length && !ntp.serve;
|
|
648
|
+
if (!guarded.length && !keepsTimeQuietly)
|
|
649
|
+
return;
|
|
650
|
+
const families = [
|
|
651
|
+
["inet", "ipv4", "protocol"],
|
|
652
|
+
["inet6", "ipv6", "next-header"]
|
|
653
|
+
];
|
|
654
|
+
for (const [family, addressFamily, protocolWord] of families) {
|
|
655
|
+
const filter = `firewall family ${family} filter ${SERVICES_FILTER[family]}`;
|
|
656
|
+
const ofFamily = (prefix) => familyOf2(host2(prefix)) === addressFamily;
|
|
657
|
+
for (const { service, sources } of guarded) {
|
|
658
|
+
const [protocol, standardPort] = SERVICE_PORTS[service];
|
|
659
|
+
const port = management[service]?.port ?? standardPort;
|
|
660
|
+
const term = `${filter} term ${service}`;
|
|
661
|
+
for (const source of sources.filter(ofFamily))
|
|
662
|
+
out.set(term, "from source-address", source);
|
|
663
|
+
out.set(term, "from", protocolWord, protocol);
|
|
664
|
+
out.set(term, "from destination-port", port);
|
|
665
|
+
out.set(term, "then accept");
|
|
666
|
+
const others = `${filter} term ${service}-others`;
|
|
667
|
+
out.set(others, "from", protocolWord, protocol);
|
|
668
|
+
out.set(others, "from destination-port", port);
|
|
669
|
+
out.set(others, "then discard");
|
|
670
|
+
}
|
|
671
|
+
if (keepsTimeQuietly) {
|
|
672
|
+
const servers = ntp.servers.filter(ofFamily);
|
|
673
|
+
if (servers.length) {
|
|
674
|
+
const length = addressFamily === "ipv4" ? 32 : 128;
|
|
675
|
+
for (const server of servers)
|
|
676
|
+
out.set(`${filter} term ntp from source-address`, `${server}/${length}`);
|
|
677
|
+
out.set(`${filter} term ntp from`, protocolWord, "udp");
|
|
678
|
+
out.set(`${filter} term ntp from destination-port 123`);
|
|
679
|
+
out.set(`${filter} term ntp then accept`);
|
|
680
|
+
}
|
|
681
|
+
out.set(`${filter} term ntp-others from`, protocolWord, "udp");
|
|
682
|
+
out.set(`${filter} term ntp-others from destination-port 123`);
|
|
683
|
+
out.set(`${filter} term ntp-others then discard`);
|
|
684
|
+
}
|
|
685
|
+
out.set(`${filter} term everything-else then accept`);
|
|
686
|
+
}
|
|
687
|
+
const anchors = new Set(["lo0.0", ...[...names.loopbacks.values()].map((unit) => `lo0.${unit}`)]);
|
|
688
|
+
for (const [port, settings] of Object.entries(device.ports ?? {})) {
|
|
689
|
+
const name = names.port(port);
|
|
690
|
+
if (settings?.addresses?.length && name.startsWith("em"))
|
|
691
|
+
anchors.add(`${name}.0`);
|
|
692
|
+
}
|
|
693
|
+
for (const anchor of anchors) {
|
|
694
|
+
const [physical, unit] = anchor.split(".");
|
|
695
|
+
out.set(`interfaces ${physical} unit ${unit} family inet filter input ${SERVICES_FILTER.inet}`);
|
|
696
|
+
out.set(`interfaces ${physical} unit ${unit} family inet6 filter input ${SERVICES_FILTER.inet6}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function vrfs(out, device, names) {
|
|
700
|
+
for (const [name, vrf] of Object.entries(device.vrfs ?? {})) {
|
|
701
|
+
out.set(`routing-instances ${name} instance-type virtual-router`);
|
|
702
|
+
if (vrf.description)
|
|
703
|
+
out.set(`routing-instances ${name} description`, quote(vrf.description));
|
|
704
|
+
}
|
|
705
|
+
const routed = [...Object.entries(device.ports ?? {}), ...Object.entries(device.interfaces ?? {})];
|
|
706
|
+
for (const [name, settings] of routed) {
|
|
707
|
+
if (settings?.vrf)
|
|
708
|
+
out.set(`routing-instances ${settings.vrf} interface`, names.unit(name));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function staticRoutes(out, device, names) {
|
|
712
|
+
if (device.routing?.router_id)
|
|
713
|
+
out.set("routing-options router-id", device.routing.router_id);
|
|
714
|
+
for (const route of device.routing?.static ?? []) {
|
|
715
|
+
const ipv6 = familyOf2(split(route.prefix).addr) === "ipv6";
|
|
716
|
+
const table = ipv6 ? ` rib ${route.vrf ? `${route.vrf}.` : ""}inet6.0` : "";
|
|
717
|
+
const options = route.vrf ? `routing-instances ${route.vrf} routing-options${table}` : `routing-options${table}`;
|
|
718
|
+
const statement = `${options} static route ${route.prefix}`;
|
|
719
|
+
if (route.blackhole) {
|
|
720
|
+
out.set(statement, "discard");
|
|
721
|
+
if (route.distance !== undefined)
|
|
722
|
+
out.set(statement, "preference", route.distance);
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const nextHop = route.via ?? names.unit(route.interface);
|
|
726
|
+
if (route.distance === undefined)
|
|
727
|
+
out.set(statement, "next-hop", nextHop);
|
|
728
|
+
else
|
|
729
|
+
out.set(statement, "qualified-next-hop", nextHop, "preference", route.distance);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function spanningTree(out, device, names) {
|
|
733
|
+
const stp = device.stp;
|
|
734
|
+
if (!stp)
|
|
735
|
+
return;
|
|
736
|
+
const protocol = `protocols ${stp.mode === "mstp" ? "mstp" : "rstp"}`;
|
|
737
|
+
if (stp.mode === "stp")
|
|
738
|
+
out.set(protocol, "force-version stp");
|
|
739
|
+
if (stp.priority !== undefined)
|
|
740
|
+
out.set(protocol, "bridge-priority", stp.priority === 0 ? "0" : `${stp.priority / 1024}k`);
|
|
741
|
+
const switched = [];
|
|
742
|
+
for (const [port, settings] of Object.entries(device.ports ?? {})) {
|
|
743
|
+
if (settings && !settings.lag && (settings.access_vlan || settings.trunk_vlans))
|
|
744
|
+
switched.push([names.port(port), settings]);
|
|
745
|
+
}
|
|
746
|
+
for (const [name, lag] of Object.entries(device.interfaces ?? {})) {
|
|
747
|
+
if (lag.type === "lag" && (lag.access_vlan || lag.trunk_vlans))
|
|
748
|
+
switched.push([names.physical(name), lag]);
|
|
749
|
+
}
|
|
750
|
+
for (const [name, settings] of switched) {
|
|
751
|
+
const statement = `${protocol} interface ${name}`;
|
|
752
|
+
out.set(statement);
|
|
753
|
+
if (settings.stp === false)
|
|
754
|
+
out.set(statement, "disable");
|
|
755
|
+
else if (typeof settings.stp === "object") {
|
|
756
|
+
if (settings.stp.edge)
|
|
757
|
+
out.set(statement, "edge");
|
|
758
|
+
if (settings.stp.cost !== undefined)
|
|
759
|
+
out.set(statement, "cost", settings.stp.cost);
|
|
760
|
+
if (settings.stp.priority !== undefined)
|
|
761
|
+
out.set(statement, "priority", settings.stp.priority);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function lldp(out, device, names) {
|
|
766
|
+
const lldp = device.lldp;
|
|
767
|
+
if (lldp === true)
|
|
768
|
+
out.set("protocols lldp interface all");
|
|
769
|
+
else if (lldp)
|
|
770
|
+
for (const name of lldp.interfaces)
|
|
771
|
+
for (const port of names.members(name))
|
|
772
|
+
out.set("protocols lldp interface", port);
|
|
773
|
+
for (const [port, settings] of Object.entries(device.ports ?? {})) {
|
|
774
|
+
if (settings?.lldp === false)
|
|
775
|
+
out.set("protocols lldp interface", names.port(port), "disable");
|
|
776
|
+
else if (settings?.lldp === true)
|
|
777
|
+
out.set("protocols lldp interface", names.port(port));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function ospf(out, device, names) {
|
|
781
|
+
const ospf = device.routing?.ospf;
|
|
782
|
+
if (!ospf)
|
|
783
|
+
return;
|
|
784
|
+
const protocols = (ospf.families ?? ["ipv4"]).map((family) => family === "ipv4" ? "ospf" : "ospf3");
|
|
785
|
+
for (const protocol of protocols) {
|
|
786
|
+
for (const [areaId, area] of Object.entries(ospf.areas)) {
|
|
787
|
+
for (const [iface, settings] of Object.entries(area.interfaces)) {
|
|
788
|
+
const statement = `protocols ${protocol} area ${areaId} interface ${names.unit(iface)}`;
|
|
789
|
+
out.set(statement);
|
|
790
|
+
if (settings?.cost !== undefined)
|
|
791
|
+
out.set(statement, "metric", settings.cost);
|
|
792
|
+
if (settings?.passive)
|
|
793
|
+
out.set(statement, "passive");
|
|
794
|
+
if (settings?.network === "point-to-point")
|
|
795
|
+
out.set(statement, "interface-type p2p");
|
|
796
|
+
if (settings?.bfd)
|
|
797
|
+
out.set(statement, "bfd-liveness-detection minimum-interval", BFD_INTERVAL);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (ospf.redistribute?.length)
|
|
801
|
+
out.set(`protocols ${protocol} export`, OSPF_EXPORT);
|
|
802
|
+
}
|
|
803
|
+
for (const source of ospf.redistribute ?? []) {
|
|
804
|
+
const term = `policy-options policy-statement ${OSPF_EXPORT} term ${source}`;
|
|
805
|
+
out.set(term, "from protocol", source === "connected" ? "direct" : "static");
|
|
806
|
+
out.set(term, "then accept");
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function dhcpRelay(out, device, names) {
|
|
810
|
+
for (const [name, relay] of Object.entries(device.dhcp_relay ?? {})) {
|
|
811
|
+
const vrf = device.interfaces?.[relay.interface]?.vrf ?? device.ports?.[relay.interface]?.vrf;
|
|
812
|
+
const relays = vrf ? `routing-instances ${vrf} forwarding-options dhcp-relay` : "forwarding-options dhcp-relay";
|
|
813
|
+
for (const server of relay.servers)
|
|
814
|
+
out.set(`${relays} server-group ${name}`, server);
|
|
815
|
+
out.set(`${relays} group ${name} active-server-group`, name);
|
|
816
|
+
out.set(`${relays} group ${name} interface`, names.unit(relay.interface));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function sflow(out, device, names) {
|
|
820
|
+
const flow = device.flow_export;
|
|
821
|
+
if (flow?.protocol !== "sflow")
|
|
822
|
+
return;
|
|
823
|
+
out.set("protocols sflow agent-id", device.connection.host);
|
|
824
|
+
if (flow.sampling !== undefined)
|
|
825
|
+
out.set("protocols sflow sample-rate ingress", flow.sampling);
|
|
826
|
+
for (const collector of flow.collectors) {
|
|
827
|
+
out.set("protocols sflow collector", collector.address);
|
|
828
|
+
if (collector.port !== undefined && collector.port !== 6343) {
|
|
829
|
+
out.set("protocols sflow collector", collector.address, "udp-port", collector.port);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const declared = Object.entries(device.ports ?? {}).filter(([, settings]) => settings && !settings.lag);
|
|
833
|
+
const observed = flow.interfaces ?? declared.map(([port]) => port);
|
|
834
|
+
for (const name of observed) {
|
|
835
|
+
for (const port of names.members(name))
|
|
836
|
+
if (!port.startsWith("em"))
|
|
837
|
+
out.set("protocols sflow interfaces", `${port}.0`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
var frameSize = (mtu, switched) => mtu + (switched ? 18 : 14);
|
|
841
|
+
var bandwidth = (rate) => rate.toLowerCase();
|
|
842
|
+
function burstOf(rate) {
|
|
843
|
+
const scale = { k: 1000, m: 1e6, g: 1e9 }[rate.slice(-1).toLowerCase()];
|
|
844
|
+
return Math.max(1500, Math.round(Number(rate.slice(0, -1)) * scale / 800));
|
|
845
|
+
}
|
|
846
|
+
var eachPort = (ports) => [ports].flat().map((port) => typeof port === "number" ? String(port) : `${port.min}-${port.max}`);
|
|
847
|
+
function aclFamily(rule) {
|
|
848
|
+
const match = rule.match;
|
|
849
|
+
if (match?.family)
|
|
850
|
+
return match.family;
|
|
851
|
+
const address = match?.src ?? match?.dst;
|
|
852
|
+
return address ? familyOf2(host2(address)) : undefined;
|
|
853
|
+
}
|
|
854
|
+
var nextHeader = (protocol) => protocol === "icmp" ? "icmp6" : protocol;
|
|
855
|
+
var ruleName = (label, description) => quote(description ? `${label} ${description}` : label);
|
|
856
|
+
function acls(out, device, names) {
|
|
857
|
+
const switched = new Set;
|
|
858
|
+
const routed = new Set;
|
|
859
|
+
for (const [port, settings] of Object.entries(device.ports ?? {})) {
|
|
860
|
+
if (!settings?.acl)
|
|
861
|
+
continue;
|
|
862
|
+
const physical = names.port(port);
|
|
863
|
+
if (settings.access_vlan || settings.trunk_vlans) {
|
|
864
|
+
switched.add(settings.acl);
|
|
865
|
+
out.set(`interfaces ${physical} unit 0 family ethernet-switching filter input`, settings.acl);
|
|
866
|
+
} else {
|
|
867
|
+
routed.add(settings.acl);
|
|
868
|
+
out.set(`interfaces ${physical} unit 0 family inet filter input`, `${settings.acl}-v4`);
|
|
869
|
+
out.set(`interfaces ${physical} unit 0 family inet6 filter input`, `${settings.acl}-v6`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
for (const acl of switched)
|
|
873
|
+
switchingFilter(out, device, acl);
|
|
874
|
+
for (const acl of routed) {
|
|
875
|
+
routedFilter(out, device, acl, "ipv4");
|
|
876
|
+
routedFilter(out, device, acl, "ipv6");
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function aclThen(out, term, policer, rule) {
|
|
880
|
+
if (rule.action === "drop") {
|
|
881
|
+
out.set(term, "then discard");
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (rule.rate) {
|
|
885
|
+
out.set(`firewall policer ${policer} if-exceeding bandwidth-limit`, bandwidth(rule.rate));
|
|
886
|
+
out.set(`firewall policer ${policer} if-exceeding burst-size-limit`, burstOf(rule.rate));
|
|
887
|
+
out.set(`firewall policer ${policer} then discard`);
|
|
888
|
+
out.set(term, "then policer", policer);
|
|
889
|
+
}
|
|
890
|
+
out.set(term, "then accept");
|
|
891
|
+
}
|
|
892
|
+
function switchingFilter(out, device, acl) {
|
|
893
|
+
const filter = `firewall family ethernet-switching filter ${acl}`;
|
|
894
|
+
(device.acls?.[acl] ?? []).forEach((rule, index) => {
|
|
895
|
+
const match = rule.match ?? {};
|
|
896
|
+
const addressed = match.src || match.dst || match.protocol || match.src_port !== undefined || match.dst_port !== undefined;
|
|
897
|
+
const family = aclFamily(rule);
|
|
898
|
+
const families = addressed || family ? family ? [family] : ["ipv4", "ipv6"] : [undefined];
|
|
899
|
+
for (const each of families) {
|
|
900
|
+
const label = families.length > 1 ? `${index + 1}-${each === "ipv4" ? "v4" : "v6"}` : `${index + 1}`;
|
|
901
|
+
const term = `${filter} term ${ruleName(label, rule.description)}`;
|
|
902
|
+
if (each) {
|
|
903
|
+
const version = `from ip-version ${each}`;
|
|
904
|
+
const v4 = each === "ipv4";
|
|
905
|
+
if (match.src)
|
|
906
|
+
out.set(term, version, v4 ? "ip-source-address" : "ip6-source-address", match.src);
|
|
907
|
+
if (match.dst)
|
|
908
|
+
out.set(term, version, v4 ? "ip-destination-address" : "ip6-destination-address", match.dst);
|
|
909
|
+
if (match.protocol)
|
|
910
|
+
out.set(term, version, v4 ? "ip-protocol" : "next-header", v4 ? match.protocol : nextHeader(match.protocol));
|
|
911
|
+
for (const port of match.src_port !== undefined ? eachPort(match.src_port) : [])
|
|
912
|
+
out.set(term, version, "source-port", port);
|
|
913
|
+
for (const port of match.dst_port !== undefined ? eachPort(match.dst_port) : [])
|
|
914
|
+
out.set(term, version, "destination-port", port);
|
|
915
|
+
if (!addressed)
|
|
916
|
+
out.set(term, version);
|
|
917
|
+
}
|
|
918
|
+
aclThen(out, term, `${acl}-${label}`, rule);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
out.set(filter, "term circuit-default then accept");
|
|
922
|
+
}
|
|
923
|
+
function routedFilter(out, device, acl, family) {
|
|
924
|
+
const name = `${acl}-${family === "ipv4" ? "v4" : "v6"}`;
|
|
925
|
+
const filter = `firewall family ${family === "ipv4" ? "inet" : "inet6"} filter ${name}`;
|
|
926
|
+
(device.acls?.[acl] ?? []).forEach((rule, index) => {
|
|
927
|
+
const ruleFamily = aclFamily(rule);
|
|
928
|
+
if (ruleFamily && ruleFamily !== family)
|
|
929
|
+
return;
|
|
930
|
+
const match = rule.match ?? {};
|
|
931
|
+
const label = `${index + 1}`;
|
|
932
|
+
const term = `${filter} term ${ruleName(label, rule.description)}`;
|
|
933
|
+
const protocolWord = family === "ipv4" ? "protocol" : "next-header";
|
|
934
|
+
if (match.src)
|
|
935
|
+
out.set(term, "from source-address", match.src);
|
|
936
|
+
if (match.dst)
|
|
937
|
+
out.set(term, "from destination-address", match.dst);
|
|
938
|
+
if (match.protocol)
|
|
939
|
+
out.set(term, "from", protocolWord, family === "ipv4" ? match.protocol : nextHeader(match.protocol));
|
|
940
|
+
for (const port of match.src_port !== undefined ? eachPort(match.src_port) : [])
|
|
941
|
+
out.set(term, "from source-port", port);
|
|
942
|
+
for (const port of match.dst_port !== undefined ? eachPort(match.dst_port) : [])
|
|
943
|
+
out.set(term, "from destination-port", port);
|
|
944
|
+
aclThen(out, term, `${name}-${label}`, rule);
|
|
945
|
+
});
|
|
946
|
+
out.set(filter, "term circuit-default then accept");
|
|
947
|
+
}
|
|
948
|
+
function dhcpServers(out, device, names) {
|
|
949
|
+
for (const [name, server] of Object.entries(device.dhcp ?? {})) {
|
|
950
|
+
out.set(`system services dhcp-local-server group ${name} interface`, names.unit(server.interface));
|
|
951
|
+
const pool = `access address-assignment pool ${name} family inet`;
|
|
952
|
+
const [low, high] = server.pool;
|
|
953
|
+
out.set(pool, "network", server.network);
|
|
954
|
+
out.set(pool, `range ${name} low`, low);
|
|
955
|
+
out.set(pool, `range ${name} high`, high);
|
|
956
|
+
out.set(pool, "dhcp-attributes router", server.gateway);
|
|
957
|
+
for (const resolver of server.dns ?? [])
|
|
958
|
+
out.set(pool, "dhcp-attributes name-server", resolver);
|
|
959
|
+
if (server.lease_time !== undefined)
|
|
960
|
+
out.set(pool, "dhcp-attributes maximum-lease-time", server.lease_time);
|
|
961
|
+
for (const reservation of server.reservations ?? []) {
|
|
962
|
+
const host = `${pool} host ${reservation.address.replace(/\./g, "-")}`;
|
|
963
|
+
out.set(host, "hardware-address", reservation.mac.toLowerCase());
|
|
964
|
+
out.set(host, "ip-address", reservation.address);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function routerAdvertisements(out, device, names) {
|
|
969
|
+
const routed = [...Object.entries(device.ports ?? {}), ...Object.entries(device.interfaces ?? {})];
|
|
970
|
+
for (const [name, settings] of routed) {
|
|
971
|
+
if (!settings?.ipv6_ra)
|
|
972
|
+
continue;
|
|
973
|
+
const addresses = (settings.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
|
|
974
|
+
for (const address of addresses.filter((address) => familyOf2(address) === "ipv6")) {
|
|
975
|
+
out.set(`protocols router-advertisement interface ${names.unit(name)} prefix`, network(address));
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/adapters/devices/junos/bgp.ts
|
|
981
|
+
var REJECT_ALL = "circuit-reject-all";
|
|
982
|
+
var ORIGIN = "circuit-bgp-origin";
|
|
983
|
+
var RPKI_GROUP = "circuit";
|
|
984
|
+
var GENERATED = "# generated ";
|
|
985
|
+
var FAMILY = { ipv4: "inet", ipv6: "inet6" };
|
|
986
|
+
var VALIDATION = { valid: "valid", invalid: "invalid", "not-found": "unknown" };
|
|
987
|
+
function settingsOf(device, neighbor) {
|
|
988
|
+
const group = neighbor.group ? device.routing?.bgp?.groups?.[neighbor.group] : undefined;
|
|
989
|
+
return { ...group, ...neighbor };
|
|
990
|
+
}
|
|
991
|
+
var slug = (value) => value.replace(/\*/g, "any").replace(/[^A-Za-z0-9]+/g, "-");
|
|
992
|
+
var member = (community, large) => large ? `large:${community}` : community;
|
|
993
|
+
function renderBgp(out, device) {
|
|
994
|
+
prefixSets(out, device);
|
|
995
|
+
rpki(out, device);
|
|
996
|
+
const bgp = device.routing?.bgp;
|
|
997
|
+
if (!bgp)
|
|
998
|
+
return;
|
|
999
|
+
out.set("routing-options autonomous-system", bgp.asn);
|
|
1000
|
+
const used = new Set;
|
|
1001
|
+
let rejectAll = false;
|
|
1002
|
+
let exports = false;
|
|
1003
|
+
for (const [name, neighbor] of Object.entries(bgp.neighbors ?? {})) {
|
|
1004
|
+
const settings = settingsOf(device, neighbor);
|
|
1005
|
+
const group = `protocols bgp group ${name}`;
|
|
1006
|
+
out.set(group, "type", settings.remote_as === bgp.asn ? "internal" : "external");
|
|
1007
|
+
out.set(group, "peer-as", settings.remote_as);
|
|
1008
|
+
if (settings.description)
|
|
1009
|
+
out.set(group, "description", quote(settings.description));
|
|
1010
|
+
if (settings.local_address)
|
|
1011
|
+
out.set(group, "local-address", settings.local_address);
|
|
1012
|
+
if (settings.multihop)
|
|
1013
|
+
out.set(group, "multihop");
|
|
1014
|
+
if (settings.passive)
|
|
1015
|
+
out.set(group, "passive");
|
|
1016
|
+
if (settings.hold_time !== undefined)
|
|
1017
|
+
out.set(group, "hold-time", settings.hold_time);
|
|
1018
|
+
if (settings.password)
|
|
1019
|
+
out.set(group, "authentication-key", marker(settings.password));
|
|
1020
|
+
if (settings.bfd)
|
|
1021
|
+
out.set(group, "bfd-liveness-detection minimum-interval", BFD_INTERVAL);
|
|
1022
|
+
out.set(group, "import", settings.import ?? REJECT_ALL);
|
|
1023
|
+
if (settings.export) {
|
|
1024
|
+
exports = true;
|
|
1025
|
+
out.set(group, "export", ORIGIN);
|
|
1026
|
+
out.set(group, "export", settings.export);
|
|
1027
|
+
} else
|
|
1028
|
+
out.set(group, "export", REJECT_ALL);
|
|
1029
|
+
for (const policy of [settings.import, settings.export]) {
|
|
1030
|
+
if (policy)
|
|
1031
|
+
used.add(policy);
|
|
1032
|
+
else
|
|
1033
|
+
rejectAll = true;
|
|
1034
|
+
}
|
|
1035
|
+
for (const family of settings.families ?? [familyOf2(neighbor.address)]) {
|
|
1036
|
+
const nlri = `${group} family ${FAMILY[family]} unicast`;
|
|
1037
|
+
out.set(nlri);
|
|
1038
|
+
if (settings.max_prefixes !== undefined) {
|
|
1039
|
+
out.set(nlri, "prefix-limit maximum", settings.max_prefixes);
|
|
1040
|
+
out.set(nlri, "prefix-limit teardown idle-timeout forever");
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
out.set(group, "neighbor", neighbor.address);
|
|
1044
|
+
}
|
|
1045
|
+
if (rejectAll)
|
|
1046
|
+
out.set(`policy-options policy-statement ${REJECT_ALL} then reject`);
|
|
1047
|
+
if (exports)
|
|
1048
|
+
origin(out, device);
|
|
1049
|
+
for (const policy of [...used].sort())
|
|
1050
|
+
policyStatement(out, device, policy);
|
|
1051
|
+
}
|
|
1052
|
+
function origin(out, device) {
|
|
1053
|
+
const statement = `policy-options policy-statement ${ORIGIN}`;
|
|
1054
|
+
out.set(statement, "term bgp from protocol bgp");
|
|
1055
|
+
out.set(statement, "term bgp then next policy");
|
|
1056
|
+
const networks = device.routing?.bgp?.networks ?? [];
|
|
1057
|
+
for (const prefix of networks)
|
|
1058
|
+
out.set(statement, "term networks from route-filter", prefix, "exact");
|
|
1059
|
+
if (networks.length)
|
|
1060
|
+
out.set(statement, "term networks then next policy");
|
|
1061
|
+
out.set(statement, "term others then reject");
|
|
1062
|
+
}
|
|
1063
|
+
function prefixSets(out, device) {
|
|
1064
|
+
for (const [name, set] of Object.entries(device.prefix_sets ?? {})) {
|
|
1065
|
+
const list = `policy-options prefix-list ${name}`;
|
|
1066
|
+
if ("prefixes" in set && set.prefixes.length)
|
|
1067
|
+
for (const prefix of set.prefixes)
|
|
1068
|
+
out.set(list, prefix);
|
|
1069
|
+
else
|
|
1070
|
+
out.set(list);
|
|
1071
|
+
if ("source" in set)
|
|
1072
|
+
out.note(`${GENERATED}${list}`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function rpki(out, device) {
|
|
1076
|
+
for (const server of device.routing?.rpki ?? []) {
|
|
1077
|
+
out.set(`routing-options validation group ${RPKI_GROUP} session`, server.address, "port", server.port);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function familyOfRule(device, rule) {
|
|
1081
|
+
const match = rule.match;
|
|
1082
|
+
if (match?.family)
|
|
1083
|
+
return match.family;
|
|
1084
|
+
if (match?.prefix)
|
|
1085
|
+
return familyOf2(host2(match.prefix));
|
|
1086
|
+
const set = match?.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
|
|
1087
|
+
if (set && "source" in set)
|
|
1088
|
+
return set.source.family;
|
|
1089
|
+
if (set?.prefixes[0])
|
|
1090
|
+
return familyOf2(host2(set.prefixes[0]));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
function flatten(device, policy, label, seen) {
|
|
1094
|
+
const rules = device.policies?.[policy] ?? [];
|
|
1095
|
+
const flat = [];
|
|
1096
|
+
rules.forEach((rule, index) => {
|
|
1097
|
+
const here = label ? `${label}.${index + 1}` : `${index + 1}`;
|
|
1098
|
+
if (rule.call && !seen.includes(rule.call))
|
|
1099
|
+
flat.push(...flatten(device, rule.call, here, [...seen, rule.call]));
|
|
1100
|
+
if (rule.match || rule.set || rule.action)
|
|
1101
|
+
flat.push({ label: here, rule });
|
|
1102
|
+
});
|
|
1103
|
+
return flat;
|
|
1104
|
+
}
|
|
1105
|
+
var termName = (label, rule) => quote(rule.description ? `${label} ${rule.description}` : label);
|
|
1106
|
+
function policyStatement(out, device, policy) {
|
|
1107
|
+
const statement = `policy-options policy-statement ${policy}`;
|
|
1108
|
+
const flat = flatten(device, policy, "", [policy]);
|
|
1109
|
+
for (const { label, rule } of flat) {
|
|
1110
|
+
const family = familyOfRule(device, rule);
|
|
1111
|
+
const split = !family && (rule.set?.next_hop || rule.match?.prefix_length) ? ["ipv4", "ipv6"] : [family];
|
|
1112
|
+
for (const part of split) {
|
|
1113
|
+
const name = split.length > 1 ? termName(`${label}-${part === "ipv4" ? "v4" : "v6"}`, rule) : termName(label, rule);
|
|
1114
|
+
term(out, device, `${statement} term ${name}`, rule, part, split.length > 1);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const last = flat.at(-1)?.rule;
|
|
1118
|
+
const decides = last !== undefined && !last.match && last.action !== undefined;
|
|
1119
|
+
if (!decides)
|
|
1120
|
+
out.set(statement, "term circuit-default then reject");
|
|
1121
|
+
}
|
|
1122
|
+
function term(out, device, term, rule, family, splitFamily) {
|
|
1123
|
+
const match = rule.match ?? {};
|
|
1124
|
+
const set = rule.set ?? {};
|
|
1125
|
+
const from = [];
|
|
1126
|
+
const then = [];
|
|
1127
|
+
if (match.family || splitFamily)
|
|
1128
|
+
from.push(["family", FAMILY[match.family ?? family]]);
|
|
1129
|
+
if (match.prefix)
|
|
1130
|
+
from.push(["route-filter", match.prefix, "exact"]);
|
|
1131
|
+
if (match.prefix_set)
|
|
1132
|
+
from.push(["prefix-list-filter", match.prefix_set, "orlonger"]);
|
|
1133
|
+
if (match.prefix_length) {
|
|
1134
|
+
const families = family ? [family] : ["ipv4", "ipv6"];
|
|
1135
|
+
for (const each of families) {
|
|
1136
|
+
const longest = each === "ipv4" ? 32 : 128;
|
|
1137
|
+
const range = `/${match.prefix_length.min ?? 0}-/${match.prefix_length.max ?? longest}`;
|
|
1138
|
+
from.push(["route-filter", each === "ipv4" ? "0.0.0.0/0" : "::/0", "prefix-length-range", range]);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (match.as_path_length) {
|
|
1142
|
+
const { min = 0, max } = match.as_path_length;
|
|
1143
|
+
const name = `circuit-path-length-${min}-${max ?? "any"}`;
|
|
1144
|
+
out.set(`policy-options as-path ${name}`, quote(`.{${min},${max ?? ""}}`));
|
|
1145
|
+
from.push(["as-path", name]);
|
|
1146
|
+
}
|
|
1147
|
+
if (match.rpki)
|
|
1148
|
+
from.push(["validation-database", VALIDATION[match.rpki]]);
|
|
1149
|
+
const wanted = [
|
|
1150
|
+
...match.community ? [member(match.community, false)] : [],
|
|
1151
|
+
...match.large_community ? [member(match.large_community, true)] : []
|
|
1152
|
+
];
|
|
1153
|
+
if (wanted.length)
|
|
1154
|
+
from.push(["community", community(out, "match", wanted)]);
|
|
1155
|
+
if (set.local_pref !== undefined)
|
|
1156
|
+
then.push(["local-preference", set.local_pref]);
|
|
1157
|
+
if (set.med !== undefined)
|
|
1158
|
+
then.push(["metric", set.med]);
|
|
1159
|
+
if (set.prepend !== undefined) {
|
|
1160
|
+
const asn = device.routing.bgp.asn;
|
|
1161
|
+
then.push(["as-path-prepend", quote(Array(set.prepend).fill(asn).join(" "))]);
|
|
1162
|
+
}
|
|
1163
|
+
const removed = [
|
|
1164
|
+
...(set.communities ? ["*:*"] : set.remove_communities ?? []).map((pattern) => member(pattern, false)),
|
|
1165
|
+
...(set.large_communities ? ["*:*:*"] : set.remove_large_communities ?? []).map((pattern) => member(pattern, true))
|
|
1166
|
+
];
|
|
1167
|
+
const added = [
|
|
1168
|
+
...(set.communities ?? set.add_communities ?? []).map((value) => member(value, false)),
|
|
1169
|
+
...(set.large_communities ?? set.add_large_communities ?? []).map((value) => member(value, true))
|
|
1170
|
+
];
|
|
1171
|
+
if (set.communities && set.add_communities)
|
|
1172
|
+
added.push(...set.add_communities.map((value) => member(value, false)));
|
|
1173
|
+
if (set.large_communities && set.add_large_communities)
|
|
1174
|
+
added.push(...set.add_large_communities.map((value) => member(value, true)));
|
|
1175
|
+
for (const pattern of removed)
|
|
1176
|
+
then.push(["community delete", community(out, "remove", [pattern])]);
|
|
1177
|
+
if (added.length)
|
|
1178
|
+
then.push(["community add", community(out, "add", added)]);
|
|
1179
|
+
if (set.next_hop && (!family || familyOf2(set.next_hop) === family))
|
|
1180
|
+
then.push(["next-hop", set.next_hop]);
|
|
1181
|
+
if (set.blackhole)
|
|
1182
|
+
then.push(["next-hop discard"]);
|
|
1183
|
+
if (rule.action)
|
|
1184
|
+
then.push([rule.action]);
|
|
1185
|
+
for (const words of from)
|
|
1186
|
+
out.set(term, "from", ...words);
|
|
1187
|
+
for (const words of then)
|
|
1188
|
+
out.set(term, "then", ...words);
|
|
1189
|
+
if (!from.length && !then.length)
|
|
1190
|
+
out.set(term);
|
|
1191
|
+
}
|
|
1192
|
+
function community(out, purpose, members) {
|
|
1193
|
+
const name = `circuit-${purpose}-${slug(members.join(" "))}`;
|
|
1194
|
+
for (const value of members)
|
|
1195
|
+
out.set(`policy-options community ${name} members`, quote(value));
|
|
1196
|
+
return name;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/adapters/devices/junos/index.ts
|
|
1200
|
+
var SUPPORTED_MAJOR = 21;
|
|
1201
|
+
var DEVICE_FACTS = [/^set version /, /^set system login user \S+ uid \d+$/];
|
|
1202
|
+
var SYSTEM_ACCOUNTS = new Set(["bind", "daemon", "kmem", "nobody", "operator", "sshd", "tty", "uucp"]);
|
|
1203
|
+
function wordsOf(statement) {
|
|
1204
|
+
return statement.match(/"(?:[^"\\]|\\.)*"|\S+/g) ?? [];
|
|
1205
|
+
}
|
|
1206
|
+
var bare = (word) => word.startsWith('"') ? word.slice(1, -1).replace(/\\(.)/g, "$1") : word;
|
|
1207
|
+
var SECRETS = [
|
|
1208
|
+
[/ (?:encrypted-password|plain-text-password-value) (?:"[^"]*"|\S+)/, " password <secret>"],
|
|
1209
|
+
[/ (?:authentication-key|authentication-password) (?:"[^"]*"|\S+)/, " authentication-key <secret>"],
|
|
1210
|
+
[/ (?:privacy-key|privacy-password) (?:"[^"]*"|\S+)/, " privacy-key <secret>"],
|
|
1211
|
+
[/^set snmp community (?:"[^"]*"|\S+)/, "set snmp community <secret>"]
|
|
1212
|
+
];
|
|
1213
|
+
function keyOf2(statement) {
|
|
1214
|
+
let canonical = statement;
|
|
1215
|
+
for (const [pattern, replacement] of SECRETS)
|
|
1216
|
+
canonical = canonical.replace(pattern, replacement);
|
|
1217
|
+
return wordsOf(canonical).slice(1).map(bare);
|
|
1218
|
+
}
|
|
1219
|
+
function scrub(text) {
|
|
1220
|
+
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
|
+
}
|
|
1222
|
+
function clean(output) {
|
|
1223
|
+
const statements = output.replace(/\r/g, "").split(`
|
|
1224
|
+
`).map((line) => line.trimEnd()).filter((line) => /^(set|deactivate) /.test(line)).filter((line) => !DEVICE_FACTS.some((fact) => fact.test(line)));
|
|
1225
|
+
return scrub(statements.join(`
|
|
1226
|
+
`) + `
|
|
1227
|
+
`);
|
|
1228
|
+
}
|
|
1229
|
+
var NAMED_LISTS = new Set([
|
|
1230
|
+
"address",
|
|
1231
|
+
"area",
|
|
1232
|
+
"as-path",
|
|
1233
|
+
"class",
|
|
1234
|
+
"collector",
|
|
1235
|
+
"community",
|
|
1236
|
+
"family",
|
|
1237
|
+
"file",
|
|
1238
|
+
"filter",
|
|
1239
|
+
"group",
|
|
1240
|
+
"host",
|
|
1241
|
+
"interface",
|
|
1242
|
+
"interfaces",
|
|
1243
|
+
"neighbor",
|
|
1244
|
+
"oid",
|
|
1245
|
+
"policer",
|
|
1246
|
+
"policy-statement",
|
|
1247
|
+
"pool",
|
|
1248
|
+
"prefix",
|
|
1249
|
+
"prefix-list",
|
|
1250
|
+
"qualified-next-hop",
|
|
1251
|
+
"range",
|
|
1252
|
+
"rib",
|
|
1253
|
+
"route",
|
|
1254
|
+
"security-level",
|
|
1255
|
+
"security-model",
|
|
1256
|
+
"security-name",
|
|
1257
|
+
"server",
|
|
1258
|
+
"server-group",
|
|
1259
|
+
"session",
|
|
1260
|
+
"storm-control-profiles",
|
|
1261
|
+
"term",
|
|
1262
|
+
"trap-group",
|
|
1263
|
+
"unit",
|
|
1264
|
+
"user",
|
|
1265
|
+
"view",
|
|
1266
|
+
"vlan",
|
|
1267
|
+
"vrrp-group"
|
|
1268
|
+
]);
|
|
1269
|
+
function canStopAt(path, index) {
|
|
1270
|
+
if (index === 0 || !NAMED_LISTS.has(path[index]))
|
|
1271
|
+
return true;
|
|
1272
|
+
return path[index] === "vlan" && path[index + 1] === "members";
|
|
1273
|
+
}
|
|
1274
|
+
var sectionOf = (path) => path.slice(0, 2).join(" ");
|
|
1275
|
+
function orderedBlockOf(path) {
|
|
1276
|
+
if (path[0] === "policy-options" && path[1] === "policy-statement" && path[3] === "term") {
|
|
1277
|
+
return { block: path.slice(0, 3), item: path[4] };
|
|
1278
|
+
}
|
|
1279
|
+
if (path[0] === "firewall" && path[1] === "family" && path[3] === "filter" && path[5] === "term") {
|
|
1280
|
+
return { block: path.slice(0, 5), item: path[6] };
|
|
1281
|
+
}
|
|
1282
|
+
const chain = path.length - 2;
|
|
1283
|
+
if (path[0] === "protocols" && (path[chain] === "import" || path[chain] === "export")) {
|
|
1284
|
+
return { block: path.slice(0, chain + 1), item: path[chain + 1] };
|
|
1285
|
+
}
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
var startsWith = (path, prefix) => prefix.every((word, index) => path[index] === word);
|
|
1289
|
+
function plan(desired, current, options) {
|
|
1290
|
+
const lines = (text, verb) => text.split(`
|
|
1291
|
+
`).filter((line) => line.startsWith(`${verb} `));
|
|
1292
|
+
const wanted = lines(desired, "set");
|
|
1293
|
+
const deactivated = lines(current, "deactivate");
|
|
1294
|
+
const generated = desired.split(`
|
|
1295
|
+
`).filter((line) => line.startsWith(GENERATED)).map((line) => wordsOf(line.slice(GENERATED.length)).map(bare));
|
|
1296
|
+
const printed = lines(current, "set").filter((line) => !DEVICE_FACTS.some((fact) => fact.test(line)));
|
|
1297
|
+
const isGenerated = (line) => generated.some((path) => keyOf2(line).length > path.length && startsWith(keyOf2(line), path));
|
|
1298
|
+
const existing = printed.filter((line) => !isGenerated(line));
|
|
1299
|
+
const join = (path) => path.join("\x00");
|
|
1300
|
+
const wantedPaths = wanted.map(keyOf2);
|
|
1301
|
+
const wantedKeys = new Set(wantedPaths.map(join));
|
|
1302
|
+
const existingKeys = new Set(existing.map((line) => join(keyOf2(line))));
|
|
1303
|
+
const wantedUnder = (path) => wantedPaths.filter((other) => startsWith(other, path));
|
|
1304
|
+
const sections = new Map;
|
|
1305
|
+
const sectionFor = (path) => {
|
|
1306
|
+
const name = sectionOf(path);
|
|
1307
|
+
if (!sections.has(name))
|
|
1308
|
+
sections.set(name, { show: [], activate: [], remove: [], add: [] });
|
|
1309
|
+
return sections.get(name);
|
|
1310
|
+
};
|
|
1311
|
+
for (const path of wantedPaths)
|
|
1312
|
+
sectionFor(path);
|
|
1313
|
+
const rewritten = [];
|
|
1314
|
+
const blocks = new Map;
|
|
1315
|
+
for (const path of wantedPaths) {
|
|
1316
|
+
const ordered = orderedBlockOf(path);
|
|
1317
|
+
if (ordered)
|
|
1318
|
+
blocks.set(join(ordered.block), ordered.block);
|
|
1319
|
+
}
|
|
1320
|
+
for (const block of blocks.values()) {
|
|
1321
|
+
const inBlock = (line) => startsWith(keyOf2(line), block);
|
|
1322
|
+
const order = (all) => [...new Set(all.filter(inBlock).map((line) => orderedBlockOf(keyOf2(line)).item))];
|
|
1323
|
+
const keys = (all) => all.filter(inBlock).map((line) => join(keyOf2(line))).sort().join(`
|
|
1324
|
+
`);
|
|
1325
|
+
const wantedHere = wanted.filter(inBlock);
|
|
1326
|
+
const existingHere = existing.filter(inBlock);
|
|
1327
|
+
if (join(order(wanted)) === join(order(existing)) && keys(wanted) === keys(existing))
|
|
1328
|
+
continue;
|
|
1329
|
+
rewritten.push(block);
|
|
1330
|
+
const section = sectionFor(block);
|
|
1331
|
+
if (existingHere.length)
|
|
1332
|
+
section.remove.push(`delete ${wordsOf(existingHere[0]).slice(1, 1 + block.length).join(" ")}`);
|
|
1333
|
+
section.show.push(...existingHere.map((line) => `- ${line}`), ...wantedHere.map((line) => `+ ${line}`));
|
|
1334
|
+
section.add.push(...wantedHere);
|
|
1335
|
+
}
|
|
1336
|
+
const isRewritten = (line) => rewritten.some((block) => startsWith(keyOf2(line), block));
|
|
1337
|
+
const removed = [];
|
|
1338
|
+
for (const line of existing) {
|
|
1339
|
+
if (isRewritten(line))
|
|
1340
|
+
continue;
|
|
1341
|
+
const path = keyOf2(line);
|
|
1342
|
+
if (wantedKeys.has(join(path)))
|
|
1343
|
+
continue;
|
|
1344
|
+
if (wantedUnder(path).length)
|
|
1345
|
+
continue;
|
|
1346
|
+
let length = 1;
|
|
1347
|
+
while (wantedUnder(path.slice(0, length)).length)
|
|
1348
|
+
length++;
|
|
1349
|
+
while (length < path.length && !canStopAt(path, length - 1))
|
|
1350
|
+
length++;
|
|
1351
|
+
const hidden = path.indexOf("<secret>");
|
|
1352
|
+
if (hidden >= 0 && hidden < length && !NAMED_LISTS.has(path[hidden - 1]))
|
|
1353
|
+
length = hidden;
|
|
1354
|
+
removed.push(wordsOf(line).slice(1, 1 + length));
|
|
1355
|
+
}
|
|
1356
|
+
const isInside = (path, other) => other.length < path.length && other.every((word, index) => bare(path[index]) === bare(word));
|
|
1357
|
+
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(bare)));
|
|
1359
|
+
for (const path of removals) {
|
|
1360
|
+
const section = sectionFor(path.map(bare));
|
|
1361
|
+
section.remove.push(`delete ${path.join(" ")}`);
|
|
1362
|
+
const under = existing.filter((line) => startsWith(keyOf2(line), path.map(bare)));
|
|
1363
|
+
section.show.push(...under.map((line) => `- ${line}`));
|
|
1364
|
+
}
|
|
1365
|
+
for (const line of deactivated) {
|
|
1366
|
+
const path = wordsOf(line).slice(1);
|
|
1367
|
+
if (!wantedUnder(path.map(bare)).length)
|
|
1368
|
+
continue;
|
|
1369
|
+
const section = sectionFor(path.map(bare));
|
|
1370
|
+
section.activate.push(`activate ${path.join(" ")}`);
|
|
1371
|
+
section.show.push(`+ activate ${path.join(" ")}`);
|
|
1372
|
+
}
|
|
1373
|
+
const extended = (path) => printed.some((line) => !isRemoved(line) && startsWith(keyOf2(line), path));
|
|
1374
|
+
for (const line of wanted) {
|
|
1375
|
+
if (isRewritten(line))
|
|
1376
|
+
continue;
|
|
1377
|
+
const path = keyOf2(line);
|
|
1378
|
+
const isNew = !existingKeys.has(join(path)) && !extended(path);
|
|
1379
|
+
const rotated = options.secrets && line.includes("<secret:");
|
|
1380
|
+
if (!isNew && !rotated)
|
|
1381
|
+
continue;
|
|
1382
|
+
const section = sectionFor(path);
|
|
1383
|
+
section.add.push(line);
|
|
1384
|
+
section.show.push(`+ ${line}`);
|
|
1385
|
+
const clear = "delete snmp community <redacted>";
|
|
1386
|
+
if (rotated && line.startsWith("set snmp community ") && !section.remove.includes(clear))
|
|
1387
|
+
section.remove.push(clear);
|
|
1388
|
+
}
|
|
1389
|
+
const steps = [...sections].filter(([, section]) => section.show.length).map(([title, section]) => ({ title, show: section.show, send: [...section.activate, ...section.remove, ...section.add] }));
|
|
1390
|
+
return { steps, problems: [] };
|
|
1391
|
+
}
|
|
1392
|
+
async function login(device) {
|
|
1393
|
+
const user = device.users?.[device.connection.user];
|
|
1394
|
+
if (!user)
|
|
1395
|
+
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
1396
|
+
return connect({
|
|
1397
|
+
host: device.connection.host,
|
|
1398
|
+
port: device.connection.port,
|
|
1399
|
+
user: device.connection.user,
|
|
1400
|
+
password: resolve(user.password)
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
async function checkVersion(client, device) {
|
|
1404
|
+
const version = await exec(client, "show version | match Junos:");
|
|
1405
|
+
const release = /Junos: (\d+)\.(\S+)/.exec(version);
|
|
1406
|
+
if (!release)
|
|
1407
|
+
throw new Error(`${device.name} did not say which Junos it runs: ${version.slice(0, 200)}`);
|
|
1408
|
+
if (Number(release[1]) !== SUPPORTED_MAJOR) {
|
|
1409
|
+
throw new Error(`${device.name} runs Junos ${release[1]}.${release[2]}; this adapter renders for Junos ${SUPPORTED_MAJOR}`);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
async function read(device) {
|
|
1413
|
+
const client = await login(device);
|
|
1414
|
+
try {
|
|
1415
|
+
await checkVersion(client, device);
|
|
1416
|
+
const output = await exec(client, "show configuration | display set | no-more", 180000);
|
|
1417
|
+
if (!/^set /m.test(output))
|
|
1418
|
+
throw new Error(`${device.name} returned no configuration`);
|
|
1419
|
+
return clean(output);
|
|
1420
|
+
} finally {
|
|
1421
|
+
client.end();
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
var PROMPT = /[>#%] $/;
|
|
1425
|
+
async function run(channel, line, timeout = 60000) {
|
|
1426
|
+
channel.write(`${line}
|
|
1427
|
+
`);
|
|
1428
|
+
const output = await readUntil(channel, (text) => PROMPT.test(text.replace(/\r/g, "")), timeout, 300);
|
|
1429
|
+
return output.replace(/\r/g, "");
|
|
1430
|
+
}
|
|
1431
|
+
var EDIT_PROMPT = /\[edit[^\]\n]*\]\n[^\n]*# $/;
|
|
1432
|
+
async function edit(channel, line, timeout = 60000) {
|
|
1433
|
+
channel.write(`${line}
|
|
1434
|
+
`);
|
|
1435
|
+
const output = await readUntil(channel, (text) => EDIT_PROMPT.test(text.replace(/\r/g, "")), timeout, 300);
|
|
1436
|
+
return output.replace(/\r/g, "");
|
|
1437
|
+
}
|
|
1438
|
+
async function openCli(channel) {
|
|
1439
|
+
const banner = await readUntil(channel, (text) => PROMPT.test(text.replace(/\r/g, "")), 20000, 500);
|
|
1440
|
+
if (/% $/.test(banner.replace(/\r/g, "")))
|
|
1441
|
+
await run(channel, "cli");
|
|
1442
|
+
await run(channel, "set cli screen-length 0");
|
|
1443
|
+
await run(channel, "set cli screen-width 0");
|
|
1444
|
+
await run(channel, "set cli complete-on-space off");
|
|
1445
|
+
}
|
|
1446
|
+
async function named(channel, line) {
|
|
1447
|
+
const words = wordsOf(line).slice(1);
|
|
1448
|
+
const at = words.indexOf("<redacted>");
|
|
1449
|
+
if (at < 0)
|
|
1450
|
+
return [line];
|
|
1451
|
+
const shown = await edit(channel, `show ${words.slice(0, at - 1).join(" ")} | display set`);
|
|
1452
|
+
const inList = (statement) => statement[0] === "set" && words.slice(0, at).every((word, index) => statement[index + 1] === word);
|
|
1453
|
+
const names = new Set(shown.split(`
|
|
1454
|
+
`).map(wordsOf).filter(inList).map((statement) => statement[at + 1]));
|
|
1455
|
+
return [...names].map((name) => `delete ${[...words.slice(0, at), name, ...words.slice(at + 1)].join(" ")}`);
|
|
1456
|
+
}
|
|
1457
|
+
var REFUSED = /^terminal:\d+:\(\d+\) (?!warning)|^error: /m;
|
|
1458
|
+
async function configure(channel, plan, guard) {
|
|
1459
|
+
const sent = plan.steps.flatMap((step) => step.send);
|
|
1460
|
+
const entered = await run(channel, "configure exclusive");
|
|
1461
|
+
if (!/Entering configuration mode/.test(entered) || /^error: /m.test(entered)) {
|
|
1462
|
+
throw new Error(`the candidate could not be locked: ${entered.trim().slice(0, 300)}`);
|
|
1463
|
+
}
|
|
1464
|
+
const deletions = [];
|
|
1465
|
+
for (const line of sent.filter((line) => line.startsWith("delete ")))
|
|
1466
|
+
deletions.push(...await named(channel, line));
|
|
1467
|
+
const ordered = [
|
|
1468
|
+
...sent.filter((line) => line.startsWith("activate ")),
|
|
1469
|
+
...deletions,
|
|
1470
|
+
...sent.filter((line) => line.startsWith("set "))
|
|
1471
|
+
];
|
|
1472
|
+
const discard = async (reason) => {
|
|
1473
|
+
await edit(channel, "rollback 0").catch(() => {
|
|
1474
|
+
return;
|
|
1475
|
+
});
|
|
1476
|
+
await run(channel, "exit configuration-mode").catch(() => {
|
|
1477
|
+
return;
|
|
1478
|
+
});
|
|
1479
|
+
return new Error(reason);
|
|
1480
|
+
};
|
|
1481
|
+
channel.write(`load set terminal
|
|
1482
|
+
`);
|
|
1483
|
+
await readUntil(channel, (text) => text.includes("[Type ^D at a new line to end input]"), 20000, 300);
|
|
1484
|
+
for (const line of ordered)
|
|
1485
|
+
channel.write(`${substitute(line)}
|
|
1486
|
+
`);
|
|
1487
|
+
const loaded = await edit(channel, "\x04", 300000);
|
|
1488
|
+
const refused = loaded.split(`
|
|
1489
|
+
`).filter((line) => REFUSED.test(line));
|
|
1490
|
+
if (refused.length)
|
|
1491
|
+
throw await discard(`the device refused the plan, nothing was applied:
|
|
1492
|
+
${refused.join(`
|
|
1493
|
+
`)}`);
|
|
1494
|
+
const checked = await edit(channel, "commit check", 600000);
|
|
1495
|
+
if (!/configuration check succeeds/.test(checked)) {
|
|
1496
|
+
throw await discard(`commit check failed, nothing was applied:
|
|
1497
|
+
${checked.trim().split(`
|
|
1498
|
+
`).slice(1, 20).join(`
|
|
1499
|
+
`)}`);
|
|
1500
|
+
}
|
|
1501
|
+
if (!guard) {
|
|
1502
|
+
const committed = await edit(channel, "commit comment circuit", 600000);
|
|
1503
|
+
if (!/commit complete/.test(committed))
|
|
1504
|
+
throw await discard(`commit failed, nothing was applied:
|
|
1505
|
+
${committed.trim().slice(0, 300)}`);
|
|
1506
|
+
await run(channel, "exit configuration-mode");
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const { minutes, prove } = guard;
|
|
1510
|
+
const committed = await edit(channel, `commit confirmed ${minutes} comment circuit`, 600000);
|
|
1511
|
+
if (!/commit complete/.test(committed)) {
|
|
1512
|
+
throw await discard(`commit failed, nothing was applied:
|
|
1513
|
+
${committed.trim().split(`
|
|
1514
|
+
`).slice(1, 20).join(`
|
|
1515
|
+
`)}`);
|
|
1516
|
+
}
|
|
1517
|
+
console.log(` committed; it rolls back in ${minutes} minutes unless access is proved`);
|
|
1518
|
+
try {
|
|
1519
|
+
await prove();
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
await edit(channel, "rollback 1", 60000).catch(() => {
|
|
1522
|
+
return;
|
|
1523
|
+
});
|
|
1524
|
+
await edit(channel, "commit comment circuit-restore", 600000).catch(() => {
|
|
1525
|
+
return;
|
|
1526
|
+
});
|
|
1527
|
+
await run(channel, "exit configuration-mode").catch(() => {
|
|
1528
|
+
return;
|
|
1529
|
+
});
|
|
1530
|
+
throw new Error(`a fresh login failed (${error.message}); the previous configuration is restored, at the latest in ${minutes} minutes`);
|
|
1531
|
+
}
|
|
1532
|
+
const confirmed = await edit(channel, "commit comment circuit-confirmed", 600000);
|
|
1533
|
+
if (!/commit complete/.test(confirmed)) {
|
|
1534
|
+
throw new Error(`access was proved but the confirmation failed; the device rolls back in ${minutes} minutes:
|
|
1535
|
+
${confirmed.trim().slice(0, 300)}`);
|
|
1536
|
+
}
|
|
1537
|
+
await run(channel, "exit configuration-mode");
|
|
1538
|
+
console.log(" logged in again, commit confirmed");
|
|
1539
|
+
}
|
|
1540
|
+
async function apply(device, _network, plan, options) {
|
|
1541
|
+
const client = await login(device);
|
|
1542
|
+
try {
|
|
1543
|
+
await checkVersion(client, device);
|
|
1544
|
+
const channel = await shell(client);
|
|
1545
|
+
await openCli(channel);
|
|
1546
|
+
await configure(channel, plan, {
|
|
1547
|
+
minutes: options.rollback,
|
|
1548
|
+
prove: async () => {
|
|
1549
|
+
const proof = await login(device);
|
|
1550
|
+
proof.end();
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
channel.end();
|
|
1554
|
+
} finally {
|
|
1555
|
+
client.end();
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
function unsupported(device) {
|
|
1559
|
+
const found = [];
|
|
1560
|
+
const names = new Names(device);
|
|
1561
|
+
const users = device.users ?? {};
|
|
1562
|
+
const root = users.root;
|
|
1563
|
+
if (!root)
|
|
1564
|
+
found.push("users without root, the platform needs a root password");
|
|
1565
|
+
if (root && root.role !== "admin")
|
|
1566
|
+
found.push("users.root.role, root is always admin");
|
|
1567
|
+
if (root?.description)
|
|
1568
|
+
found.push("users.root.description");
|
|
1569
|
+
if (device.connection.user === "root")
|
|
1570
|
+
found.push("connection.user root, which logs in to a shell rather than the CLI");
|
|
1571
|
+
for (const [name, user] of Object.entries(users)) {
|
|
1572
|
+
if (SYSTEM_ACCOUNTS.has(name))
|
|
1573
|
+
found.push(`users.${name}, a name the platform keeps for a system account`);
|
|
1574
|
+
for (const key of user.ssh_keys ?? []) {
|
|
1575
|
+
const kind = key.split(" ")[0];
|
|
1576
|
+
if (!KEY_STATEMENT[kind])
|
|
1577
|
+
found.push(`users.${name}.ssh_keys, a key of type ${kind}`);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
const management = device.management ?? {};
|
|
1581
|
+
for (const field of ["https", "api_tls", "privilege_password"]) {
|
|
1582
|
+
if (management[field])
|
|
1583
|
+
found.push(`management.${field}`);
|
|
1584
|
+
}
|
|
1585
|
+
if (management.console)
|
|
1586
|
+
found.push("management.console, the console uses the device's users");
|
|
1587
|
+
if (management.native?.interfaces?.length)
|
|
1588
|
+
found.push("management.native.interfaces, NETCONF runs over IP only");
|
|
1589
|
+
if (management.ssh?.auth_timeout || management.ssh?.auth_retries)
|
|
1590
|
+
found.push("management.ssh.auth_timeout and auth_retries");
|
|
1591
|
+
for (const service of ["telnet", "ftp", "snmp"]) {
|
|
1592
|
+
if (management[service]?.port)
|
|
1593
|
+
found.push(`management.${service}.port`);
|
|
1594
|
+
}
|
|
1595
|
+
const system = device.system ?? {};
|
|
1596
|
+
if (system.release_channel)
|
|
1597
|
+
found.push("system.release_channel");
|
|
1598
|
+
if (system.transceiver_monitoring !== undefined)
|
|
1599
|
+
found.push("system.transceiver_monitoring, it is always on");
|
|
1600
|
+
if (system.dns?.serve)
|
|
1601
|
+
found.push("system.dns.serve");
|
|
1602
|
+
if (system.ip?.syn_cookies)
|
|
1603
|
+
found.push("system.ip.syn_cookies");
|
|
1604
|
+
if (system.logging?.local?.lines_per_file !== undefined)
|
|
1605
|
+
found.push("system.logging.local.lines_per_file, files rotate by size");
|
|
1606
|
+
if (system.ntp && !system.ntp.serve && system.ntp.servers.some((server) => !isAddress2(server))) {
|
|
1607
|
+
found.push("system.ntp.servers by name while not serving, the filter keeping others out needs addresses");
|
|
1608
|
+
}
|
|
1609
|
+
for (const [name, port] of Object.entries(device.ports ?? {})) {
|
|
1610
|
+
if (!port)
|
|
1611
|
+
continue;
|
|
1612
|
+
const physical = names.port(name);
|
|
1613
|
+
if (physical.startsWith("em")) {
|
|
1614
|
+
for (const field of [
|
|
1615
|
+
"access_vlan",
|
|
1616
|
+
"trunk_vlans",
|
|
1617
|
+
"native_vlan",
|
|
1618
|
+
"lag",
|
|
1619
|
+
"stp",
|
|
1620
|
+
"storm_control",
|
|
1621
|
+
"speed",
|
|
1622
|
+
"vrrp",
|
|
1623
|
+
"vrf",
|
|
1624
|
+
"mtu",
|
|
1625
|
+
"acl",
|
|
1626
|
+
"ipv6_ra"
|
|
1627
|
+
]) {
|
|
1628
|
+
if (port[field] !== undefined)
|
|
1629
|
+
found.push(`ports.${name}.${field}, a management port only routes`);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
if (port.speed && !(port.speed === "1g" && physical.startsWith("ge-")))
|
|
1633
|
+
found.push(`ports.${name}.speed ${port.speed}, only a 1G optic in a 10G cage`);
|
|
1634
|
+
if (port.hardware_offload !== undefined)
|
|
1635
|
+
found.push(`ports.${name}.hardware_offload`);
|
|
1636
|
+
const acl = port.acl ? device.acls?.[port.acl] : undefined;
|
|
1637
|
+
if (acl?.some((rule) => rule.match?.vlan))
|
|
1638
|
+
found.push(`acls.${port.acl} matching a VLAN, on port ${name}`);
|
|
1639
|
+
found.push(...routedGaps(`ports.${name}`, port));
|
|
1640
|
+
found.push(...switchedGaps(`ports.${name}`, port, device));
|
|
1641
|
+
}
|
|
1642
|
+
const loopbackTables = new Set;
|
|
1643
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
1644
|
+
if (iface.type === "vxlan" || iface.type === "wireguard") {
|
|
1645
|
+
found.push(`interfaces.${name}, a ${iface.type}`);
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
if (iface.hardware_offload !== undefined)
|
|
1649
|
+
found.push(`interfaces.${name}.hardware_offload`);
|
|
1650
|
+
if (iface.type === "loopback" && iface.ipv6_ra)
|
|
1651
|
+
found.push(`interfaces.${name}.ipv6_ra, on a loopback`);
|
|
1652
|
+
if (iface.type === "loopback" && iface.mtu !== undefined)
|
|
1653
|
+
found.push(`interfaces.${name}.mtu, on a loopback`);
|
|
1654
|
+
if (iface.type === "gre" && iface.vrrp?.length)
|
|
1655
|
+
found.push(`interfaces.${name}.vrrp, on a tunnel`);
|
|
1656
|
+
if (iface.type === "vlan" && iface.mtu !== undefined) {
|
|
1657
|
+
const smallest = Math.min(...carriers(device, iface.vlan));
|
|
1658
|
+
if (iface.mtu > smallest)
|
|
1659
|
+
found.push(`interfaces.${name}.mtu ${iface.mtu}, above the ${smallest} its VLAN's ports carry`);
|
|
1660
|
+
}
|
|
1661
|
+
found.push(...routedGaps(`interfaces.${name}`, iface));
|
|
1662
|
+
if (iface.type === "lag")
|
|
1663
|
+
found.push(...switchedGaps(`interfaces.${name}`, iface, device));
|
|
1664
|
+
if (iface.type === "loopback") {
|
|
1665
|
+
const table = iface.vrf ?? "";
|
|
1666
|
+
if (loopbackTables.has(table))
|
|
1667
|
+
found.push(`interfaces.${name}, a second loopback in one routing table`);
|
|
1668
|
+
loopbackTables.add(table);
|
|
1669
|
+
if (iface.vrrp?.length)
|
|
1670
|
+
found.push(`interfaces.${name}.vrrp, on a loopback`);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
const ids = [...lagIds(device).values()];
|
|
1674
|
+
if (ids.length !== new Set(ids).size)
|
|
1675
|
+
found.push("interfaces, two LAGs with one number");
|
|
1676
|
+
if (device.stp?.priority !== undefined && device.stp.priority % 4096 !== 0)
|
|
1677
|
+
found.push("stp.priority, in steps of 4096");
|
|
1678
|
+
const routing = device.routing;
|
|
1679
|
+
found.push(...bgpGaps(device));
|
|
1680
|
+
const tables = new Map;
|
|
1681
|
+
for (const route of routing?.static ?? []) {
|
|
1682
|
+
if (route.description)
|
|
1683
|
+
found.push(`routing.static ${route.prefix} description`);
|
|
1684
|
+
const table = `${route.vrf ?? ""} ${split(route.prefix).addr}/${split(route.prefix).len}`;
|
|
1685
|
+
tables.set(table, (tables.get(table) ?? 0) + 1);
|
|
1686
|
+
}
|
|
1687
|
+
for (const route of routing?.static ?? []) {
|
|
1688
|
+
const table = `${route.vrf ?? ""} ${split(route.prefix).addr}/${split(route.prefix).len}`;
|
|
1689
|
+
if (route.blackhole && tables.get(table) > 1)
|
|
1690
|
+
found.push(`routing.static ${route.prefix}, a blackhole beside another route to the same prefix`);
|
|
1691
|
+
}
|
|
1692
|
+
for (const area of Object.values(routing?.ospf?.areas ?? {})) {
|
|
1693
|
+
for (const iface of Object.keys(area.interfaces)) {
|
|
1694
|
+
const settings = device.interfaces?.[iface] ?? device.ports?.[iface];
|
|
1695
|
+
if (settings?.vrf)
|
|
1696
|
+
found.push(`routing.ospf on ${iface}, which routes in a VRF`);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
for (const field of ["firewall", "hardware_offload", "certificates"]) {
|
|
1700
|
+
if (device[field] !== undefined)
|
|
1701
|
+
found.push(field);
|
|
1702
|
+
}
|
|
1703
|
+
const relayed = new Set(Object.values(device.dhcp_relay ?? {}).map((relay) => relay.interface));
|
|
1704
|
+
for (const [name, server] of Object.entries(device.dhcp ?? {})) {
|
|
1705
|
+
if (relayed.has(server.interface))
|
|
1706
|
+
found.push(`dhcp.${name}.interface, which a DHCP relay also uses`);
|
|
1707
|
+
const settings = device.interfaces?.[server.interface] ?? device.ports?.[server.interface];
|
|
1708
|
+
if (settings?.vrf)
|
|
1709
|
+
found.push(`dhcp.${name}.interface, which routes in a VRF`);
|
|
1710
|
+
if (familyOf2(host2(server.network)) === "ipv6")
|
|
1711
|
+
found.push(`dhcp.${name}.network, IPv6`);
|
|
1712
|
+
}
|
|
1713
|
+
for (const [name, relay] of Object.entries(device.dhcp_relay ?? {})) {
|
|
1714
|
+
if (relay.servers.some((server) => familyOf2(server) === "ipv6"))
|
|
1715
|
+
found.push(`dhcp_relay.${name}.servers, IPv6`);
|
|
1716
|
+
}
|
|
1717
|
+
const flow = device.flow_export;
|
|
1718
|
+
if (flow && flow.protocol !== "sflow")
|
|
1719
|
+
found.push(`flow_export.protocol ${flow.protocol}, the platform exports sFlow`);
|
|
1720
|
+
if (flow?.protocol === "sflow" && flow.sampling === undefined)
|
|
1721
|
+
found.push("flow_export.sampling, the platform samples");
|
|
1722
|
+
for (const name of flow?.interfaces ?? []) {
|
|
1723
|
+
if (names.members(name).some((port) => port.startsWith("em")))
|
|
1724
|
+
found.push(`flow_export.interfaces ${name}, a management port`);
|
|
1725
|
+
}
|
|
1726
|
+
return found;
|
|
1727
|
+
}
|
|
1728
|
+
function carriers(device, vlan) {
|
|
1729
|
+
const sizes = [1500];
|
|
1730
|
+
const carrying = [...Object.values(device.ports ?? {}), ...Object.values(device.interfaces ?? {})];
|
|
1731
|
+
for (const settings of carrying) {
|
|
1732
|
+
if (!settings || !(("access_vlan" in settings) || ("trunk_vlans" in settings)))
|
|
1733
|
+
continue;
|
|
1734
|
+
const members = [settings.access_vlan, settings.native_vlan, ...settings.trunk_vlans ?? []];
|
|
1735
|
+
if (members.includes(vlan))
|
|
1736
|
+
sizes.push(settings.mtu ?? 1500);
|
|
1737
|
+
}
|
|
1738
|
+
return sizes.length > 1 ? sizes.slice(1) : sizes;
|
|
1739
|
+
}
|
|
1740
|
+
function bgpGaps(device) {
|
|
1741
|
+
const found = [];
|
|
1742
|
+
const bgp = device.routing?.bgp;
|
|
1743
|
+
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
1744
|
+
const settings = settingsOf(device, neighbor);
|
|
1745
|
+
if (settings.role)
|
|
1746
|
+
found.push(`routing.bgp.neighbors.${name}.role, Junos 21 has no BGP roles`);
|
|
1747
|
+
if (familyOf2(neighbor.address) === "ipv6" && settings.families?.includes("ipv4")) {
|
|
1748
|
+
found.push(`routing.bgp.neighbors.${name}.families ipv4 over an IPv6 session`);
|
|
1749
|
+
}
|
|
1750
|
+
if (settings.keepalive !== undefined)
|
|
1751
|
+
found.push(`routing.bgp.neighbors.${name}.keepalive, the platform keeps it at a third of hold_time`);
|
|
1752
|
+
}
|
|
1753
|
+
for (const server of device.routing?.rpki ?? []) {
|
|
1754
|
+
if (server.description)
|
|
1755
|
+
found.push(`routing.rpki ${server.address} description`);
|
|
1756
|
+
}
|
|
1757
|
+
for (const [name, rules] of Object.entries(device.policies ?? {})) {
|
|
1758
|
+
rules.forEach((rule, index) => {
|
|
1759
|
+
const where = `policies.${name}[${index}]`;
|
|
1760
|
+
if (rule.set?.preferred_source)
|
|
1761
|
+
found.push(`${where}.set.preferred_source`);
|
|
1762
|
+
if (rule.match?.prefix && rule.match.prefix_length)
|
|
1763
|
+
found.push(`${where}.match.prefix with prefix_length`);
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
return found;
|
|
1767
|
+
}
|
|
1768
|
+
function routedGaps(where, settings) {
|
|
1769
|
+
const found = [];
|
|
1770
|
+
const addresses = (settings.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
|
|
1771
|
+
if (settings.addresses?.some((entry) => typeof entry !== "string"))
|
|
1772
|
+
found.push(`${where}.addresses with a peer`);
|
|
1773
|
+
for (const group of settings.vrrp ?? []) {
|
|
1774
|
+
if (familyOf2(group.address) === "ipv6")
|
|
1775
|
+
found.push(`${where}.vrrp ${group.id}, IPv6`);
|
|
1776
|
+
else if (!vrrpAddress(addresses, group))
|
|
1777
|
+
found.push(`${where}.vrrp ${group.id}, no address of the interface holds ${group.address}`);
|
|
1778
|
+
}
|
|
1779
|
+
return found;
|
|
1780
|
+
}
|
|
1781
|
+
function switchedGaps(where, settings, device) {
|
|
1782
|
+
const found = [];
|
|
1783
|
+
const storm = Object.values(settings.storm_control ?? {});
|
|
1784
|
+
if (storm.some((threshold) => ("pps" in threshold)))
|
|
1785
|
+
found.push(`${where}.storm_control in pps, the platform takes percent`);
|
|
1786
|
+
else if (new Set(storm.map((threshold) => ("percent" in threshold) ? threshold.percent : 0)).size > 1) {
|
|
1787
|
+
found.push(`${where}.storm_control, one threshold for every kind`);
|
|
1788
|
+
}
|
|
1789
|
+
if (typeof settings.stp === "object" && !device.stp)
|
|
1790
|
+
found.push(`${where}.stp while spanning tree is off on the device`);
|
|
1791
|
+
return found;
|
|
1792
|
+
}
|
|
1793
|
+
function fillPlan(set, prefixes) {
|
|
1794
|
+
const list = `policy-options prefix-list ${set}`;
|
|
1795
|
+
const send = [`delete ${list}`, `set ${list}`, ...prefixes.map((prefix) => `set ${list} ${prefix}`)];
|
|
1796
|
+
return { steps: [{ title: list, show: [], send }], problems: [] };
|
|
1797
|
+
}
|
|
1798
|
+
async function fill(device, _network, set, _family, prefixes) {
|
|
1799
|
+
const client = await login(device);
|
|
1800
|
+
try {
|
|
1801
|
+
await checkVersion(client, device);
|
|
1802
|
+
const channel = await shell(client);
|
|
1803
|
+
await openCli(channel);
|
|
1804
|
+
await configure(channel, fillPlan(set, prefixes));
|
|
1805
|
+
channel.end();
|
|
1806
|
+
console.log(` ${set}: ${prefixes.length} prefixes on the device`);
|
|
1807
|
+
} finally {
|
|
1808
|
+
client.end();
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
var junos = {
|
|
1812
|
+
extension: ".conf",
|
|
1813
|
+
unsupported,
|
|
1814
|
+
render,
|
|
1815
|
+
read,
|
|
1816
|
+
plan,
|
|
1817
|
+
apply,
|
|
1818
|
+
fill
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1821
|
+
// src/core/time.ts
|
|
1822
|
+
function offset(zone) {
|
|
1823
|
+
const name = new Intl.DateTimeFormat("en-US", {
|
|
1824
|
+
timeZone: zone,
|
|
1825
|
+
timeZoneName: "longOffset"
|
|
1826
|
+
}).formatToParts(new Date).find((entry) => entry.type === "timeZoneName").value;
|
|
1827
|
+
const found = /GMT([+-])(\d{2}):(\d{2})/.exec(name);
|
|
1828
|
+
if (!found)
|
|
1829
|
+
return 0;
|
|
1830
|
+
const [, sign, hours, minutes] = found;
|
|
1831
|
+
const sizeInMinutes = Number(hours) * 60 + Number(minutes);
|
|
1832
|
+
return sign === "-" ? -sizeInMinutes : sizeInMinutes;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
267
1835
|
// src/adapters/devices/lines.ts
|
|
268
1836
|
var indentOf = (raw) => raw.length - raw.trimStart().length;
|
|
269
1837
|
function entries(text, dialect) {
|
|
@@ -391,10 +1959,10 @@ function steps(groups, dialect, around = nothingAround) {
|
|
|
391
1959
|
});
|
|
392
1960
|
}
|
|
393
1961
|
var STORED_CREDENTIAL = /(password (?:irreversible-)?cipher|community read cipher|^snmp-server community)( +)\S+/gm;
|
|
394
|
-
var
|
|
1962
|
+
var scrub2 = (text) => text.replace(STORED_CREDENTIAL, "$1$2<redacted>");
|
|
395
1963
|
|
|
396
1964
|
// src/adapters/devices/raisecom-ros/index.ts
|
|
397
|
-
var
|
|
1965
|
+
var DEVICE_FACTS2 = [
|
|
398
1966
|
"Being processed",
|
|
399
1967
|
"System current configuration",
|
|
400
1968
|
"recv-mode",
|
|
@@ -447,7 +2015,7 @@ var dialect = {
|
|
|
447
2015
|
block: /^(interface |access-list |router ospf )/,
|
|
448
2016
|
indented: false,
|
|
449
2017
|
terminator: "!",
|
|
450
|
-
ignore: (line) => line.startsWith("!") ||
|
|
2018
|
+
ignore: (line) => line.startsWith("!") || DEVICE_FACTS2.some((fact) => line.startsWith(fact)),
|
|
451
2019
|
undo,
|
|
452
2020
|
leave: "exit",
|
|
453
2021
|
canon: (line) => line.replace(/password (cipher )?(\S+)/, "password <secret>").replace(/community <secret:\w+>/, "community <redacted>").replace(/^snmp-server user (\S+) (?:authentication|authkey) (\S+) \S+ (?:privacy|privkey)\s+(\S+) \S+$/, "snmp-server user $1 $2 $3 <secret>").replace(/^(snmp-server access \S+ read \S+) notify internet (usm \S+)$/, "$1 $2"),
|
|
@@ -463,8 +2031,8 @@ var dialect = {
|
|
|
463
2031
|
}
|
|
464
2032
|
};
|
|
465
2033
|
var OSPF_PROCESS = 1;
|
|
466
|
-
var
|
|
467
|
-
var
|
|
2034
|
+
var SNMP_GROUP2 = "circuit";
|
|
2035
|
+
var SNMP_AUTH2 = { sha1: "sha" };
|
|
468
2036
|
var MANAGEMENT_ACL = 1000;
|
|
469
2037
|
function vlanList(ids) {
|
|
470
2038
|
const sorted = [...new Set(ids)].sort((a, b) => a - b);
|
|
@@ -478,7 +2046,7 @@ function vlanList(ids) {
|
|
|
478
2046
|
}
|
|
479
2047
|
return ranges.join(",");
|
|
480
2048
|
}
|
|
481
|
-
function
|
|
2049
|
+
function render2(device) {
|
|
482
2050
|
const lines = [];
|
|
483
2051
|
const vlans = device.vlans ?? {};
|
|
484
2052
|
const vlanId = (name) => vlans[name].id;
|
|
@@ -496,7 +2064,7 @@ function render(device) {
|
|
|
496
2064
|
lines.push(`description ${lag.description}`);
|
|
497
2065
|
if (lag.mode === "static")
|
|
498
2066
|
lines.push("mode manual");
|
|
499
|
-
lines.push(...
|
|
2067
|
+
lines.push(...switching2(lag, vlanId), "!");
|
|
500
2068
|
}
|
|
501
2069
|
if (allow.length) {
|
|
502
2070
|
lines.push(`access-list ${MANAGEMENT_ACL}`, " description management");
|
|
@@ -570,11 +2138,11 @@ function render(device) {
|
|
|
570
2138
|
lines.push(`snmp-server community ${marker(snmp.community)} ro`);
|
|
571
2139
|
const snmpUsers = Object.entries(snmp?.users ?? {});
|
|
572
2140
|
if (snmpUsers.length)
|
|
573
|
-
lines.push(`snmp-server access ${
|
|
2141
|
+
lines.push(`snmp-server access ${SNMP_GROUP2} read internet usm authpriv`);
|
|
574
2142
|
for (const [name, user] of snmpUsers) {
|
|
575
|
-
const authentication = `authentication ${
|
|
2143
|
+
const authentication = `authentication ${SNMP_AUTH2[user.auth]} ${marker(user.auth_password)}`;
|
|
576
2144
|
const privacy = `privacy ${user.privacy} ${marker(user.privacy_password)}`;
|
|
577
|
-
lines.push(`snmp-server user ${name} ${authentication} ${privacy}`, `snmp-server group ${
|
|
2145
|
+
lines.push(`snmp-server user ${name} ${authentication} ${privacy}`, `snmp-server group ${SNMP_GROUP2} user ${name} usm`);
|
|
578
2146
|
}
|
|
579
2147
|
if ((snmp?.community || snmpUsers.length) && allow.length)
|
|
580
2148
|
lines.push(`snmp-server access-list ${MANAGEMENT_ACL}`);
|
|
@@ -603,7 +2171,7 @@ function render(device) {
|
|
|
603
2171
|
`) + `
|
|
604
2172
|
`;
|
|
605
2173
|
}
|
|
606
|
-
function
|
|
2174
|
+
function switching2(settings, vlanId) {
|
|
607
2175
|
const lines = [];
|
|
608
2176
|
if (settings.access_vlan)
|
|
609
2177
|
lines.push(`switchport access vlan ${vlanId(settings.access_vlan)}`);
|
|
@@ -632,7 +2200,7 @@ function physicalPort(settings, vlanId, lagNumbers) {
|
|
|
632
2200
|
lines.push(`description ${settings.description}`);
|
|
633
2201
|
if (settings.lag)
|
|
634
2202
|
return [...lines, `port-channel ${lagNumbers.get(settings.lag)}`];
|
|
635
|
-
lines.push(...
|
|
2203
|
+
lines.push(...switching2(settings, vlanId));
|
|
636
2204
|
if (settings.lldp === false)
|
|
637
2205
|
lines.push("lldp disable");
|
|
638
2206
|
if (settings.stp === false)
|
|
@@ -668,7 +2236,7 @@ async function open(device) {
|
|
|
668
2236
|
await send(channel, "terminal page-break disable", 15000, 1000);
|
|
669
2237
|
return { channel, close };
|
|
670
2238
|
}
|
|
671
|
-
async function
|
|
2239
|
+
async function read2(device) {
|
|
672
2240
|
const { channel, close } = await open(device);
|
|
673
2241
|
try {
|
|
674
2242
|
const dump = await send(channel, "show running-config", 150000, 6000);
|
|
@@ -680,13 +2248,13 @@ async function read(device) {
|
|
|
680
2248
|
const body = lines.slice(start).filter((line) => line.startsWith("!") || !/[#>]\s*$/.test(line.trim()));
|
|
681
2249
|
const text = body.join(`
|
|
682
2250
|
`).replace(/^!(System (up )?time|Boot times):.*\n/gm, "").trim();
|
|
683
|
-
return
|
|
2251
|
+
return scrub2(text + `
|
|
684
2252
|
`).replace(/(authkey \S+|privkey\s+\S+) \S+/g, "$1 <redacted>");
|
|
685
2253
|
} finally {
|
|
686
2254
|
close();
|
|
687
2255
|
}
|
|
688
2256
|
}
|
|
689
|
-
function
|
|
2257
|
+
function unsupported2(device) {
|
|
690
2258
|
const found = [];
|
|
691
2259
|
for (const [name, port] of Object.entries(device.ports ?? {})) {
|
|
692
2260
|
if (!port)
|
|
@@ -779,7 +2347,7 @@ function unsupported(device) {
|
|
|
779
2347
|
found.push(`management.${field}`);
|
|
780
2348
|
}
|
|
781
2349
|
for (const [name, user] of Object.entries(management.snmp?.users ?? {})) {
|
|
782
|
-
if (!
|
|
2350
|
+
if (!SNMP_AUTH2[user.auth])
|
|
783
2351
|
found.push(`management.snmp.users.${name}.auth ${user.auth}, the platform authenticates with SHA1 or MD5`);
|
|
784
2352
|
}
|
|
785
2353
|
if (management.snmp?.contact || management.snmp?.location)
|
|
@@ -801,10 +2369,10 @@ function unsupported(device) {
|
|
|
801
2369
|
found.push(...switchUnsupported(device));
|
|
802
2370
|
return found;
|
|
803
2371
|
}
|
|
804
|
-
var
|
|
2372
|
+
var REFUSED2 = /^(%|Error|Incomplete|Unrecognized)|can not|cannot |is in use|Wrong parameter|invalid|unsuccessful/im;
|
|
805
2373
|
var CONFIRM = /^(portswitch|switchport trunk allowed vlan|switchport mode)/;
|
|
806
2374
|
var RELEASE = /^no (\S+ access-list \d+)$/;
|
|
807
|
-
async function
|
|
2375
|
+
async function apply2(device, _network, plan) {
|
|
808
2376
|
const { channel, close } = await open(device);
|
|
809
2377
|
const released = [];
|
|
810
2378
|
try {
|
|
@@ -815,7 +2383,7 @@ async function apply(device, _network, plan) {
|
|
|
815
2383
|
const asks = /[[(]y\/n[\])]|input 'y'/i.test(answer) || CONFIRM.test(line) && !/set successfully/i.test(answer);
|
|
816
2384
|
if (asks)
|
|
817
2385
|
answer += await send(channel, "y", 1e4, 1000);
|
|
818
|
-
if (
|
|
2386
|
+
if (REFUSED2.test(answer))
|
|
819
2387
|
throw new Error(`${step.title}: ${line}
|
|
820
2388
|
device said: ${answer.trim().slice(0, 300)}`);
|
|
821
2389
|
const release = RELEASE.exec(line);
|
|
@@ -863,7 +2431,7 @@ function createdIds(line) {
|
|
|
863
2431
|
}
|
|
864
2432
|
return ids;
|
|
865
2433
|
}
|
|
866
|
-
function
|
|
2434
|
+
function plan2(desired, current) {
|
|
867
2435
|
const groups = compare(desired, current, dialect);
|
|
868
2436
|
const existing = entries(current, dialect);
|
|
869
2437
|
for (const group of groups) {
|
|
@@ -913,7 +2481,7 @@ function plan(desired, current) {
|
|
|
913
2481
|
}
|
|
914
2482
|
return { steps: planned, problems: [] };
|
|
915
2483
|
}
|
|
916
|
-
var raisecom = { extension: ".txt", unsupported, render, read, plan, apply };
|
|
2484
|
+
var raisecom = { extension: ".txt", unsupported: unsupported2, render: render2, read: read2, plan: plan2, apply: apply2 };
|
|
917
2485
|
|
|
918
2486
|
// src/adapters/devices/routeros/export.ts
|
|
919
2487
|
var unwrap = (text) => text.replace(/\r/g, "").replace(/\\\n\s*/g, "");
|
|
@@ -1230,9 +2798,9 @@ var DYNAMIC = new Set([
|
|
|
1230
2798
|
"/ip dhcp-server lease",
|
|
1231
2799
|
"/interface bridge vlan"
|
|
1232
2800
|
]);
|
|
1233
|
-
var
|
|
1234
|
-
var value = (raw) => /^[\w.:/@,!-]+$/.test(raw) ? raw :
|
|
1235
|
-
var findValue = (raw) => /^[\w.:-]+$/.test(raw) ? raw :
|
|
2801
|
+
var quote2 = (value) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$")}"`;
|
|
2802
|
+
var value = (raw) => /^[\w.:/@,!-]+$/.test(raw) ? raw : quote2(raw);
|
|
2803
|
+
var findValue = (raw) => /^[\w.:-]+$/.test(raw) ? raw : quote2(raw);
|
|
1236
2804
|
function find(menu, id) {
|
|
1237
2805
|
const conditions = Object.entries(id).map(([property, raw]) => `${property}=${findValue(raw)}`);
|
|
1238
2806
|
if (DYNAMIC.has(menu))
|
|
@@ -1347,7 +2915,7 @@ function planEntries(menu, wanted, existing, sendSecrets, problems) {
|
|
|
1347
2915
|
for (const list of wholeLists) {
|
|
1348
2916
|
const count = removed.filter((entry) => entry.attrs.list === list).length;
|
|
1349
2917
|
removeStep.show.push(`- list ${list}, ${count} entr${count === 1 ? "y" : "ies"}`);
|
|
1350
|
-
removeStep.send.push(`${menu} remove [ find list=${
|
|
2918
|
+
removeStep.send.push(`${menu} remove [ find list=${quote2(list)} and !dynamic ]`);
|
|
1351
2919
|
}
|
|
1352
2920
|
for (const entry of removed) {
|
|
1353
2921
|
if (wholeLists.has(entry.attrs.list ?? ""))
|
|
@@ -1379,19 +2947,19 @@ function planCertificates(wanted, existing) {
|
|
|
1379
2947
|
const oldName = sameCertificate.attrs.name;
|
|
1380
2948
|
renamed.add(oldName);
|
|
1381
2949
|
install.show.push(`~ rename ${oldName} to ${name}`);
|
|
1382
|
-
install.send.push(`${menu} set [ find name=${
|
|
2950
|
+
install.send.push(`${menu} set [ find name=${quote2(oldName)} ] name=${quote2(name)}`);
|
|
1383
2951
|
continue;
|
|
1384
2952
|
}
|
|
1385
2953
|
install.show.push(`${current ? "~" : "+"} install ${name}`);
|
|
1386
2954
|
if (current)
|
|
1387
|
-
install.send.push(`${menu} remove [ find name=${
|
|
2955
|
+
install.send.push(`${menu} remove [ find name=${quote2(name)} ]`);
|
|
1388
2956
|
install.send.push(INSTALL_CERTIFICATE + name);
|
|
1389
2957
|
}
|
|
1390
2958
|
for (const entry of existing) {
|
|
1391
2959
|
if (wantedNames.has(entry.attrs.name) || renamed.has(entry.attrs.name))
|
|
1392
2960
|
continue;
|
|
1393
2961
|
remove.show.push(`- ${entry.attrs.name}`);
|
|
1394
|
-
remove.send.push(`${menu} remove [ find name=${
|
|
2962
|
+
remove.send.push(`${menu} remove [ find name=${quote2(entry.attrs.name)} ]`);
|
|
1395
2963
|
}
|
|
1396
2964
|
if (install.send.length)
|
|
1397
2965
|
result.first.push(install);
|
|
@@ -1413,7 +2981,7 @@ function planOrdered(menu, wanted, existing) {
|
|
|
1413
2981
|
const droppedChains = [...new Set(existing.map((item) => item.attrs.chain ?? ""))].filter((chain) => !keptChains.has(chain));
|
|
1414
2982
|
let removeOld = `${menu} remove $${OLD_RULES}`;
|
|
1415
2983
|
if (hasChains) {
|
|
1416
|
-
const keep = [...keptChains].map((chain) =>
|
|
2984
|
+
const keep = [...keptChains].map((chain) => quote2(chain)).join(";");
|
|
1417
2985
|
const inKeptChain = `[:typeof [:find $keep [${menu} get $id chain]]] != "nil"`;
|
|
1418
2986
|
removeOld = `:local keep {${keep}}; :foreach id in=$${OLD_RULES} do={ :if (${inKeptChain}) do={ ${menu} remove $id } }`;
|
|
1419
2987
|
}
|
|
@@ -1426,13 +2994,13 @@ function planOrdered(menu, wanted, existing) {
|
|
|
1426
2994
|
result.first.push({ title: `${menu}, in the order of the config`, show, send });
|
|
1427
2995
|
if (hasChains) {
|
|
1428
2996
|
for (const chain of droppedChains) {
|
|
1429
|
-
const selector = `[ find chain=${
|
|
2997
|
+
const selector = `[ find chain=${quote2(chain)}${DYNAMIC.has(menu) ? " and !dynamic" : ""} ]`;
|
|
1430
2998
|
result.last.push({ title: `${menu} chain ${chain}`, show: [`- chain ${chain}`], send: [`${menu} remove ${selector}`] });
|
|
1431
2999
|
}
|
|
1432
3000
|
}
|
|
1433
3001
|
return result;
|
|
1434
3002
|
}
|
|
1435
|
-
function
|
|
3003
|
+
function plan3(desired, current, options) {
|
|
1436
3004
|
const want = parse(desired);
|
|
1437
3005
|
const have = parse(current);
|
|
1438
3006
|
const owned = (command) => !(command.menu.endsWith("address-list") && want.generated.has(command.attrs.list ?? ""));
|
|
@@ -1479,7 +3047,7 @@ class Output {
|
|
|
1479
3047
|
`;
|
|
1480
3048
|
}
|
|
1481
3049
|
}
|
|
1482
|
-
function
|
|
3050
|
+
function quote3(value) {
|
|
1483
3051
|
const plain = /^[\w.:/@-]+$/.test(value);
|
|
1484
3052
|
if (plain)
|
|
1485
3053
|
return value;
|
|
@@ -1491,18 +3059,18 @@ function command(verb, properties) {
|
|
|
1491
3059
|
for (const [name, value] of Object.entries(properties)) {
|
|
1492
3060
|
if (value === undefined || value === false)
|
|
1493
3061
|
continue;
|
|
1494
|
-
const written = typeof value === "number" ? String(value) :
|
|
3062
|
+
const written = typeof value === "number" ? String(value) : quote3(value);
|
|
1495
3063
|
words.push(`${name}=${written}`);
|
|
1496
3064
|
}
|
|
1497
3065
|
return words.join(" ");
|
|
1498
3066
|
}
|
|
1499
3067
|
var yesNo = (on) => on ? "yes" : "no";
|
|
1500
3068
|
var familySuffix = (family) => family === "ipv4" ? "V4" : "V6";
|
|
1501
|
-
var
|
|
3069
|
+
var eachPort2 = (ports) => [ports].flat().map((port) => typeof port === "number" ? String(port) : `${port.min}-${port.max}`);
|
|
1502
3070
|
function portList(ports) {
|
|
1503
3071
|
if (ports === undefined)
|
|
1504
3072
|
return;
|
|
1505
|
-
return
|
|
3073
|
+
return eachPort2(ports).join(",");
|
|
1506
3074
|
}
|
|
1507
3075
|
function duration(seconds) {
|
|
1508
3076
|
const units = [
|
|
@@ -1585,7 +3153,7 @@ function renderInterfaces(out, ctx) {
|
|
|
1585
3153
|
bridge(out, ctx);
|
|
1586
3154
|
ethernet(out, ctx);
|
|
1587
3155
|
switchPorts(out, ctx);
|
|
1588
|
-
|
|
3156
|
+
lags2(out, ctx);
|
|
1589
3157
|
vlanInterfaces(out, ctx);
|
|
1590
3158
|
tunnels(out, ctx);
|
|
1591
3159
|
wireguard(out, ctx);
|
|
@@ -1594,7 +3162,7 @@ function renderInterfaces(out, ctx) {
|
|
|
1594
3162
|
bridgeVlans(out, ctx);
|
|
1595
3163
|
bridgeFilters(out, ctx);
|
|
1596
3164
|
interfaceLists(out, ctx);
|
|
1597
|
-
|
|
3165
|
+
vrfs2(out, ctx);
|
|
1598
3166
|
}
|
|
1599
3167
|
function hardwareOffload(out, ctx) {
|
|
1600
3168
|
const on = yesNo(ctx.device.hardware_offload);
|
|
@@ -1669,7 +3237,7 @@ function switchPorts(out, ctx) {
|
|
|
1669
3237
|
}));
|
|
1670
3238
|
}
|
|
1671
3239
|
}
|
|
1672
|
-
function
|
|
3240
|
+
function lags2(out, ctx) {
|
|
1673
3241
|
for (const [name, lag] of Object.entries(ctx.interfaces)) {
|
|
1674
3242
|
if (lag.type !== "lag")
|
|
1675
3243
|
continue;
|
|
@@ -1865,7 +3433,7 @@ function lldpMembers(ctx) {
|
|
|
1865
3433
|
}
|
|
1866
3434
|
return [...members];
|
|
1867
3435
|
}
|
|
1868
|
-
function
|
|
3436
|
+
function vrfs2(out, ctx) {
|
|
1869
3437
|
for (const [vrf, settings] of Object.entries(ctx.device.vrfs ?? {})) {
|
|
1870
3438
|
const members = ctx.routed().filter(({ settings: iface }) => iface.vrf === vrf).map(({ name }) => name);
|
|
1871
3439
|
out.add("/ip vrf", command("add", { comment: settings.description, interfaces: members.join(","), name: vrf }));
|
|
@@ -2062,7 +3630,7 @@ function helpers(out, ctx) {
|
|
|
2062
3630
|
}
|
|
2063
3631
|
}
|
|
2064
3632
|
function switchAcls(out, ctx) {
|
|
2065
|
-
const each = (ports) => ports === undefined ? [undefined] :
|
|
3633
|
+
const each = (ports) => ports === undefined ? [undefined] : eachPort2(ports);
|
|
2066
3634
|
for (const [port, settings] of Object.entries(ctx.ports)) {
|
|
2067
3635
|
if (!settings.acl)
|
|
2068
3636
|
continue;
|
|
@@ -2091,8 +3659,8 @@ function switchAcls(out, ctx) {
|
|
|
2091
3659
|
}
|
|
2092
3660
|
}
|
|
2093
3661
|
}
|
|
2094
|
-
var
|
|
2095
|
-
var
|
|
3662
|
+
var RPKI_GROUP2 = "rpki";
|
|
3663
|
+
var REJECT_ALL2 = "REJECT-ALL";
|
|
2096
3664
|
var ROLES = {
|
|
2097
3665
|
provider: "ebgp-customer",
|
|
2098
3666
|
customer: "ebgp-provider",
|
|
@@ -2101,17 +3669,17 @@ var ROLES = {
|
|
|
2101
3669
|
"rs-client": "ebgp-rs"
|
|
2102
3670
|
};
|
|
2103
3671
|
function renderRouting(out, ctx) {
|
|
2104
|
-
|
|
2105
|
-
|
|
3672
|
+
staticRoutes2(out, ctx);
|
|
3673
|
+
rpki2(out, ctx);
|
|
2106
3674
|
if (ctx.device.routing?.bgp) {
|
|
2107
3675
|
bgpInstance(out, ctx);
|
|
2108
3676
|
policies(out, ctx);
|
|
2109
3677
|
bgpConnections(out, ctx);
|
|
2110
3678
|
}
|
|
2111
|
-
|
|
3679
|
+
ospf2(out, ctx);
|
|
2112
3680
|
bfd(out, ctx);
|
|
2113
3681
|
}
|
|
2114
|
-
function
|
|
3682
|
+
function staticRoutes2(out, ctx) {
|
|
2115
3683
|
for (const route of ctx.device.routing?.static ?? []) {
|
|
2116
3684
|
const menu = familyOf2(host2(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
|
|
2117
3685
|
const verb = route.blackhole ? "add blackhole" : "add";
|
|
@@ -2125,12 +3693,12 @@ function staticRoutes(out, ctx) {
|
|
|
2125
3693
|
}));
|
|
2126
3694
|
}
|
|
2127
3695
|
}
|
|
2128
|
-
function
|
|
3696
|
+
function rpki2(out, ctx) {
|
|
2129
3697
|
for (const server of ctx.device.routing?.rpki ?? []) {
|
|
2130
3698
|
out.add("/routing rpki", command("add", {
|
|
2131
3699
|
address: server.address,
|
|
2132
3700
|
comment: server.description,
|
|
2133
|
-
group:
|
|
3701
|
+
group: RPKI_GROUP2,
|
|
2134
3702
|
port: server.port
|
|
2135
3703
|
}));
|
|
2136
3704
|
}
|
|
@@ -2139,12 +3707,12 @@ function bgpInstance(out, ctx) {
|
|
|
2139
3707
|
const bgp = ctx.device.routing.bgp;
|
|
2140
3708
|
out.add("/routing bgp instance", command("add", { as: bgp.asn, name: "default", "router-id": ctx.device.routing?.router_id }));
|
|
2141
3709
|
}
|
|
2142
|
-
function
|
|
3710
|
+
function settingsOf2(ctx, neighbor) {
|
|
2143
3711
|
const group = neighbor.group ? ctx.device.routing?.bgp?.groups?.[neighbor.group] : undefined;
|
|
2144
3712
|
return { ...group, ...neighbor };
|
|
2145
3713
|
}
|
|
2146
3714
|
function familiesOf(ctx, neighbor) {
|
|
2147
|
-
return
|
|
3715
|
+
return settingsOf2(ctx, neighbor).families ?? [familyOf2(neighbor.address)];
|
|
2148
3716
|
}
|
|
2149
3717
|
var chainName = (policy, family) => `${policy}-${familySuffix(family)}`;
|
|
2150
3718
|
function appliesTo(ctx, rule, family) {
|
|
@@ -2224,7 +3792,7 @@ function ruleText(rule, family) {
|
|
|
2224
3792
|
if (rule.call)
|
|
2225
3793
|
parts.push(`jump ${chainName(rule.call, family)}`);
|
|
2226
3794
|
if (rule.match?.rpki)
|
|
2227
|
-
parts.push(`rpki-verify ${
|
|
3795
|
+
parts.push(`rpki-verify ${RPKI_GROUP2}`);
|
|
2228
3796
|
const tests = conditions(rule);
|
|
2229
3797
|
const steps = actions(rule, family);
|
|
2230
3798
|
if (tests.length)
|
|
@@ -2249,7 +3817,7 @@ function policies(out, ctx) {
|
|
|
2249
3817
|
};
|
|
2250
3818
|
let rejectAllNeeded = false;
|
|
2251
3819
|
for (const neighbor of Object.values(bgp.neighbors ?? {})) {
|
|
2252
|
-
const settings =
|
|
3820
|
+
const settings = settingsOf2(ctx, neighbor);
|
|
2253
3821
|
for (const family of familiesOf(ctx, neighbor)) {
|
|
2254
3822
|
for (const policy of [settings.import, settings.export]) {
|
|
2255
3823
|
if (policy)
|
|
@@ -2280,7 +3848,7 @@ function policies(out, ctx) {
|
|
|
2280
3848
|
if (rejectAllNeeded) {
|
|
2281
3849
|
for (const family of ["ipv4", "ipv6"])
|
|
2282
3850
|
out.add("/routing filter rule", command("add", {
|
|
2283
|
-
chain: chainName(
|
|
3851
|
+
chain: chainName(REJECT_ALL2, family),
|
|
2284
3852
|
rule: "reject"
|
|
2285
3853
|
}));
|
|
2286
3854
|
}
|
|
@@ -2288,7 +3856,7 @@ function policies(out, ctx) {
|
|
|
2288
3856
|
function bgpConnections(out, ctx) {
|
|
2289
3857
|
const bgp = ctx.device.routing.bgp;
|
|
2290
3858
|
for (const [name, neighbor] of Object.entries(bgp.neighbors ?? {})) {
|
|
2291
|
-
const settings =
|
|
3859
|
+
const settings = settingsOf2(ctx, neighbor);
|
|
2292
3860
|
const [family] = familiesOf(ctx, neighbor);
|
|
2293
3861
|
const addressFamily = familyOf2(neighbor.address);
|
|
2294
3862
|
const internal = settings.remote_as === bgp.asn;
|
|
@@ -2299,7 +3867,7 @@ function bgpConnections(out, ctx) {
|
|
|
2299
3867
|
as: bgp.asn,
|
|
2300
3868
|
connect: settings.passive ? "no" : undefined,
|
|
2301
3869
|
"hold-time": settings.hold_time ? duration(settings.hold_time) : undefined,
|
|
2302
|
-
"input.filter": chainName(settings.import ??
|
|
3870
|
+
"input.filter": chainName(settings.import ?? REJECT_ALL2, family),
|
|
2303
3871
|
instance: "default",
|
|
2304
3872
|
[`input.limit-process-routes-${family}`]: settings.max_prefixes,
|
|
2305
3873
|
"keepalive-time": settings.keepalive ? duration(settings.keepalive) : undefined,
|
|
@@ -2308,7 +3876,7 @@ function bgpConnections(out, ctx) {
|
|
|
2308
3876
|
"local.role": role,
|
|
2309
3877
|
multihop: settings.multihop ? "yes" : undefined,
|
|
2310
3878
|
name,
|
|
2311
|
-
"output.filter-chain": chainName(settings.export ??
|
|
3879
|
+
"output.filter-chain": chainName(settings.export ?? REJECT_ALL2, family),
|
|
2312
3880
|
"output.network": originates ? NETWORKS[family] : undefined,
|
|
2313
3881
|
"remote.address": `${neighbor.address}/${addressFamily === "ipv4" ? 32 : 128}`,
|
|
2314
3882
|
"remote.as": settings.remote_as,
|
|
@@ -2317,7 +3885,7 @@ function bgpConnections(out, ctx) {
|
|
|
2317
3885
|
}));
|
|
2318
3886
|
}
|
|
2319
3887
|
}
|
|
2320
|
-
function
|
|
3888
|
+
function ospf2(out, ctx) {
|
|
2321
3889
|
const ospf = ctx.device.routing?.ospf;
|
|
2322
3890
|
if (!ospf)
|
|
2323
3891
|
return;
|
|
@@ -2348,7 +3916,7 @@ function ospf(out, ctx) {
|
|
|
2348
3916
|
}
|
|
2349
3917
|
function bfd(out, ctx) {
|
|
2350
3918
|
const routing = ctx.device.routing;
|
|
2351
|
-
const bgpWants = Object.values(routing?.bgp?.neighbors ?? {}).some((neighbor) =>
|
|
3919
|
+
const bgpWants = Object.values(routing?.bgp?.neighbors ?? {}).some((neighbor) => settingsOf2(ctx, neighbor).bfd);
|
|
2352
3920
|
const ospfWants = Object.values(routing?.ospf?.areas ?? {}).some((area) => Object.values(area.interfaces).some((iface) => iface?.bfd));
|
|
2353
3921
|
if (bgpWants || ospfWants)
|
|
2354
3922
|
out.add("/routing bfd configuration", command("add", { interfaces: "all" }));
|
|
@@ -2364,16 +3932,16 @@ var SERVICES = {
|
|
|
2364
3932
|
native: "winbox"
|
|
2365
3933
|
};
|
|
2366
3934
|
var USER_GROUPS = { admin: "full", operator: "write", "read-only": "read" };
|
|
2367
|
-
var
|
|
2368
|
-
var
|
|
3935
|
+
var SNMP_AUTH3 = { sha1: "SHA1", sha256: "SHA1" };
|
|
3936
|
+
var SNMP_PRIVACY2 = { aes128: "AES", des: "DES" };
|
|
2369
3937
|
function renderManagement(out, ctx) {
|
|
2370
|
-
|
|
3938
|
+
services2(out, ctx);
|
|
2371
3939
|
layerTwo(out, ctx);
|
|
2372
|
-
|
|
2373
|
-
|
|
3940
|
+
snmp2(out, ctx);
|
|
3941
|
+
users2(out, ctx);
|
|
2374
3942
|
certificates(out, ctx);
|
|
2375
3943
|
}
|
|
2376
|
-
function
|
|
3944
|
+
function services2(out, ctx) {
|
|
2377
3945
|
const management = ctx.device.management ?? {};
|
|
2378
3946
|
for (const [neutral, platform] of Object.entries(SERVICES)) {
|
|
2379
3947
|
const service = management[neutral];
|
|
@@ -2405,7 +3973,7 @@ function layerTwo(out, ctx) {
|
|
|
2405
3973
|
discovery = "all";
|
|
2406
3974
|
out.add("/ip neighbor discovery-settings", command("set", { "discover-interface-list": discovery, protocol: "lldp" }));
|
|
2407
3975
|
}
|
|
2408
|
-
function
|
|
3976
|
+
function snmp2(out, ctx) {
|
|
2409
3977
|
const management = ctx.device.management ?? {};
|
|
2410
3978
|
const snmp = management.snmp;
|
|
2411
3979
|
if (!snmp) {
|
|
@@ -2423,15 +3991,15 @@ function snmp(out, ctx) {
|
|
|
2423
3991
|
out.add("/snmp community", command("add", {
|
|
2424
3992
|
addresses: allow,
|
|
2425
3993
|
"authentication-password": marker(user.auth_password),
|
|
2426
|
-
"authentication-protocol":
|
|
3994
|
+
"authentication-protocol": SNMP_AUTH3[user.auth],
|
|
2427
3995
|
"encryption-password": marker(user.privacy_password),
|
|
2428
|
-
"encryption-protocol":
|
|
3996
|
+
"encryption-protocol": SNMP_PRIVACY2[user.privacy],
|
|
2429
3997
|
name,
|
|
2430
3998
|
security: "private"
|
|
2431
3999
|
}));
|
|
2432
4000
|
}
|
|
2433
4001
|
}
|
|
2434
|
-
function
|
|
4002
|
+
function users2(out, ctx) {
|
|
2435
4003
|
for (const [name, user] of Object.entries(ctx.device.users ?? {})) {
|
|
2436
4004
|
out.add("/user", command("add", {
|
|
2437
4005
|
comment: user.description,
|
|
@@ -2526,7 +4094,7 @@ function flowExport(out, ctx) {
|
|
|
2526
4094
|
out.add("/ip traffic-flow target", command("add", { "dst-address": collector.address, port: collector.port ?? 2055, version }));
|
|
2527
4095
|
}
|
|
2528
4096
|
}
|
|
2529
|
-
function
|
|
4097
|
+
function render3(device) {
|
|
2530
4098
|
const out = new Output;
|
|
2531
4099
|
const ctx = new Context(device);
|
|
2532
4100
|
renderInterfaces(out, ctx);
|
|
@@ -2539,8 +4107,8 @@ function render2(device) {
|
|
|
2539
4107
|
}
|
|
2540
4108
|
|
|
2541
4109
|
// src/adapters/devices/routeros/index.ts
|
|
2542
|
-
var
|
|
2543
|
-
async function
|
|
4110
|
+
var SUPPORTED_MAJOR2 = 7;
|
|
4111
|
+
async function login2(device) {
|
|
2544
4112
|
const user = device.users?.[device.connection.user];
|
|
2545
4113
|
if (!user)
|
|
2546
4114
|
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
@@ -2551,11 +4119,11 @@ async function login(device) {
|
|
|
2551
4119
|
password: resolve(user.password)
|
|
2552
4120
|
});
|
|
2553
4121
|
}
|
|
2554
|
-
async function
|
|
4122
|
+
async function checkVersion2(client, device) {
|
|
2555
4123
|
const version = (await exec(client, ":put [/system resource get version]")).trim();
|
|
2556
4124
|
const major = Number(version.split(".")[0]);
|
|
2557
|
-
if (major !==
|
|
2558
|
-
throw new Error(`${device.name} runs RouterOS ${version}; this adapter renders for version ${
|
|
4125
|
+
if (major !== SUPPORTED_MAJOR2) {
|
|
4126
|
+
throw new Error(`${device.name} runs RouterOS ${version}; this adapter renders for version ${SUPPORTED_MAJOR2}`);
|
|
2559
4127
|
}
|
|
2560
4128
|
}
|
|
2561
4129
|
var EXPORT_TIMESTAMP = /^# \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} by RouterOS/m;
|
|
@@ -2567,10 +4135,10 @@ var LIST_CERTIFICATES = [
|
|
|
2567
4135
|
':put ("add fingerprint=" . [/certificate get $c fingerprint] . " name=" . [/certificate get $c name] . " private-key=" . $key)',
|
|
2568
4136
|
"}"
|
|
2569
4137
|
].join(" ");
|
|
2570
|
-
async function
|
|
2571
|
-
const client = await
|
|
4138
|
+
async function read3(device) {
|
|
4139
|
+
const client = await login2(device);
|
|
2572
4140
|
try {
|
|
2573
|
-
await
|
|
4141
|
+
await checkVersion2(client, device);
|
|
2574
4142
|
const config = await exec(client, "/export", 300000);
|
|
2575
4143
|
const users = await exec(client, "/user export");
|
|
2576
4144
|
const certificates = (await exec(client, LIST_CERTIFICATES)).trim();
|
|
@@ -2591,7 +4159,7 @@ ${certificates}` : "";
|
|
|
2591
4159
|
client.end();
|
|
2592
4160
|
}
|
|
2593
4161
|
}
|
|
2594
|
-
function
|
|
4162
|
+
function unsupported3(device) {
|
|
2595
4163
|
const found = [];
|
|
2596
4164
|
for (const [name, port] of Object.entries(device.ports ?? {})) {
|
|
2597
4165
|
if (!port)
|
|
@@ -2665,9 +4233,9 @@ async function installCertificate(client, device, name) {
|
|
|
2665
4233
|
}
|
|
2666
4234
|
await exec(client, `/file remove [find name="${name}.crt" or name="${name}.key"]`);
|
|
2667
4235
|
}
|
|
2668
|
-
async function
|
|
2669
|
-
const client = await
|
|
2670
|
-
await
|
|
4236
|
+
async function apply3(device, _network, changes, options) {
|
|
4237
|
+
const client = await login2(device);
|
|
4238
|
+
await checkVersion2(client, device);
|
|
2671
4239
|
const commands = changes.steps.flatMap((step) => step.send.map((command) => ({ title: step.title, command })));
|
|
2672
4240
|
for (const { title, command } of commands) {
|
|
2673
4241
|
if (command.startsWith(INSTALL_CERTIFICATE))
|
|
@@ -2713,7 +4281,7 @@ async function apply2(device, _network, changes, options) {
|
|
|
2713
4281
|
throw new Error("apply failed and the backup is being restored");
|
|
2714
4282
|
}
|
|
2715
4283
|
client.end();
|
|
2716
|
-
const proof = await
|
|
4284
|
+
const proof = await login2(device).catch(() => {
|
|
2717
4285
|
return;
|
|
2718
4286
|
});
|
|
2719
4287
|
if (!proof)
|
|
@@ -2723,10 +4291,10 @@ async function apply2(device, _network, changes, options) {
|
|
|
2723
4291
|
proof.end();
|
|
2724
4292
|
console.log(" logged in again, rollback disarmed");
|
|
2725
4293
|
}
|
|
2726
|
-
async function
|
|
4294
|
+
async function fill2(device, _network, set, family, prefixes) {
|
|
2727
4295
|
const menu = family === "ipv4" ? "/ip firewall address-list" : "/ipv6 firewall address-list";
|
|
2728
4296
|
const batchSize = 60;
|
|
2729
|
-
const client = await
|
|
4297
|
+
const client = await login2(device);
|
|
2730
4298
|
try {
|
|
2731
4299
|
await exec(client, `${menu} remove [find list="${set}" and !dynamic]`);
|
|
2732
4300
|
for (let start = 0;start < prefixes.length; start += batchSize) {
|
|
@@ -2743,16 +4311,16 @@ async function fill(device, _network, set, family, prefixes) {
|
|
|
2743
4311
|
}
|
|
2744
4312
|
var routeros = {
|
|
2745
4313
|
extension: ".rsc",
|
|
2746
|
-
unsupported:
|
|
2747
|
-
render:
|
|
2748
|
-
read:
|
|
2749
|
-
plan:
|
|
2750
|
-
apply:
|
|
2751
|
-
fill
|
|
4314
|
+
unsupported: unsupported3,
|
|
4315
|
+
render: render3,
|
|
4316
|
+
read: read3,
|
|
4317
|
+
plan: plan3,
|
|
4318
|
+
apply: apply3,
|
|
4319
|
+
fill: fill2
|
|
2752
4320
|
};
|
|
2753
4321
|
|
|
2754
4322
|
// src/adapters/devices/vrp/index.ts
|
|
2755
|
-
var
|
|
4323
|
+
var DEVICE_FACTS3 = [
|
|
2756
4324
|
"!",
|
|
2757
4325
|
"device ",
|
|
2758
4326
|
"system resource",
|
|
@@ -2807,7 +4375,7 @@ var dialect2 = {
|
|
|
2807
4375
|
block: /^(interface |vlan \d+$|acl number |aaa$|user-interface |ospf |ip vpn-instance |bfd$)/,
|
|
2808
4376
|
indented: true,
|
|
2809
4377
|
terminator: "#",
|
|
2810
|
-
ignore: (line) =>
|
|
4378
|
+
ignore: (line) => DEVICE_FACTS3.some((fact) => line.startsWith(fact)),
|
|
2811
4379
|
undo: undo2,
|
|
2812
4380
|
leave: "quit",
|
|
2813
4381
|
first: /^authentication-mode /,
|
|
@@ -2817,10 +4385,10 @@ var dialect2 = {
|
|
|
2817
4385
|
var MANAGEMENT_ACL2 = 2000;
|
|
2818
4386
|
var OSPF_PROCESS2 = 1;
|
|
2819
4387
|
var FIRST_COLLECTOR = 1;
|
|
2820
|
-
var
|
|
4388
|
+
var SNMP_GROUP3 = "circuit";
|
|
2821
4389
|
var USER_LEVEL = { admin: 3, operator: 2, "read-only": 1 };
|
|
2822
|
-
var
|
|
2823
|
-
var
|
|
4390
|
+
var SNMP_AUTH4 = { sha1: "sha", sha256: "sha2-256" };
|
|
4391
|
+
var SNMP_PRIVACY3 = { aes128: "aes128", des: "des56" };
|
|
2824
4392
|
function batchIds(line) {
|
|
2825
4393
|
const words = line.replace(/^vlan batch /, "").split(" ");
|
|
2826
4394
|
const ids = [];
|
|
@@ -2867,7 +4435,7 @@ class Config {
|
|
|
2867
4435
|
`;
|
|
2868
4436
|
}
|
|
2869
4437
|
}
|
|
2870
|
-
function
|
|
4438
|
+
function render4(device) {
|
|
2871
4439
|
const config = new Config;
|
|
2872
4440
|
const vlans = device.vlans ?? {};
|
|
2873
4441
|
const vlanId = (name) => vlans[name].id;
|
|
@@ -2881,14 +4449,14 @@ function render3(device) {
|
|
|
2881
4449
|
return `Vlanif${vlanId(iface.vlan)}`;
|
|
2882
4450
|
return spell(device.platform, device.model, portOrInterface);
|
|
2883
4451
|
};
|
|
2884
|
-
|
|
4452
|
+
system2(config, device);
|
|
2885
4453
|
vlanBlocks(config, device);
|
|
2886
4454
|
managementAcl(config, device);
|
|
2887
|
-
|
|
2888
|
-
|
|
4455
|
+
users3(config, device);
|
|
4456
|
+
vrfs3(config, device);
|
|
2889
4457
|
for (const [name, lag] of Object.entries(interfaces)) {
|
|
2890
4458
|
if (lag.type === "lag")
|
|
2891
|
-
config.block(`interface ${nameOf(name)}`, [...lagMode(lag), ...
|
|
4459
|
+
config.block(`interface ${nameOf(name)}`, [...lagMode(lag), ...switching3(lag, vlanId)]);
|
|
2892
4460
|
}
|
|
2893
4461
|
for (const [name, iface] of Object.entries(interfaces)) {
|
|
2894
4462
|
if (iface.type === "vlan")
|
|
@@ -2898,8 +4466,8 @@ function render3(device) {
|
|
|
2898
4466
|
const settings = device.ports?.[port];
|
|
2899
4467
|
config.block(`interface ${nameOf(port)}`, physicalPort2(device, port, settings, vlanId, lagNumbers));
|
|
2900
4468
|
}
|
|
2901
|
-
|
|
2902
|
-
|
|
4469
|
+
staticRoutes3(config, device, nameOf);
|
|
4470
|
+
ospf3(config, device, nameOf);
|
|
2903
4471
|
if (wantsBfd(device))
|
|
2904
4472
|
config.block("bfd", []);
|
|
2905
4473
|
if (device.dhcp_relay && Object.keys(device.dhcp_relay).length) {
|
|
@@ -2907,12 +4475,12 @@ function render3(device) {
|
|
|
2907
4475
|
config.separator();
|
|
2908
4476
|
}
|
|
2909
4477
|
flowExport2(config, device);
|
|
2910
|
-
|
|
4478
|
+
snmp3(config, device);
|
|
2911
4479
|
ssh(config, device);
|
|
2912
4480
|
userInterfaces(config, device);
|
|
2913
4481
|
return config.text();
|
|
2914
4482
|
}
|
|
2915
|
-
function
|
|
4483
|
+
function system2(config, device) {
|
|
2916
4484
|
config.line(`sysname ${device.name}`);
|
|
2917
4485
|
config.separator();
|
|
2918
4486
|
const timezone = device.system?.timezone;
|
|
@@ -2967,7 +4535,7 @@ function managementAcl(config, device) {
|
|
|
2967
4535
|
const rules = allow.map((cidr, index) => `rule ${(index + 1) * 5} permit source ${host2(cidr)} ${wildcard(split(cidr).len)}`);
|
|
2968
4536
|
config.block(`acl number ${MANAGEMENT_ACL2}`, ["description Management access", ...rules, `rule ${(allow.length + 1) * 5} deny`]);
|
|
2969
4537
|
}
|
|
2970
|
-
function
|
|
4538
|
+
function users3(config, device) {
|
|
2971
4539
|
const lines = [];
|
|
2972
4540
|
for (const [name, user] of Object.entries(device.users ?? {})) {
|
|
2973
4541
|
lines.push(`local-user ${name} password irreversible-cipher ${marker(user.password)}`);
|
|
@@ -2976,7 +4544,7 @@ function users2(config, device) {
|
|
|
2976
4544
|
}
|
|
2977
4545
|
config.block("aaa", lines);
|
|
2978
4546
|
}
|
|
2979
|
-
function
|
|
4547
|
+
function vrfs3(config, device) {
|
|
2980
4548
|
for (const name of Object.keys(device.vrfs ?? {}))
|
|
2981
4549
|
config.block(`ip vpn-instance ${name}`, ["ipv4-family"]);
|
|
2982
4550
|
}
|
|
@@ -2986,7 +4554,7 @@ function lagMode(lag) {
|
|
|
2986
4554
|
lines.unshift(`description ${lag.description}`);
|
|
2987
4555
|
return lines;
|
|
2988
4556
|
}
|
|
2989
|
-
function
|
|
4557
|
+
function switching3(settings, vlanId) {
|
|
2990
4558
|
const lines = [];
|
|
2991
4559
|
if (settings.access_vlan)
|
|
2992
4560
|
lines.push(`port default vlan ${vlanId(settings.access_vlan)}`);
|
|
@@ -3079,7 +4647,7 @@ function physicalPort2(device, port, settings, vlanId, lagNumbers) {
|
|
|
3079
4647
|
const lines = [];
|
|
3080
4648
|
if (settings.description)
|
|
3081
4649
|
lines.push(`description ${settings.description}`);
|
|
3082
|
-
lines.push(...
|
|
4650
|
+
lines.push(...switching3(settings, vlanId));
|
|
3083
4651
|
if (settings.lldp === false)
|
|
3084
4652
|
lines.push("lldp disable");
|
|
3085
4653
|
const flow = device.flow_export;
|
|
@@ -3092,7 +4660,7 @@ function physicalPort2(device, port, settings, vlanId, lagNumbers) {
|
|
|
3092
4660
|
}
|
|
3093
4661
|
return lines;
|
|
3094
4662
|
}
|
|
3095
|
-
function
|
|
4663
|
+
function staticRoutes3(config, device, nameOf) {
|
|
3096
4664
|
for (const route of device.routing?.static ?? []) {
|
|
3097
4665
|
const { addr, len } = split(route.prefix);
|
|
3098
4666
|
const nextHop = route.blackhole ? "NULL0" : route.via ?? nameOf(route.interface);
|
|
@@ -3106,7 +4674,7 @@ function staticRoutes2(config, device, nameOf) {
|
|
|
3106
4674
|
}
|
|
3107
4675
|
config.separator();
|
|
3108
4676
|
}
|
|
3109
|
-
function
|
|
4677
|
+
function ospf3(config, device, nameOf) {
|
|
3110
4678
|
const ospf = device.routing?.ospf;
|
|
3111
4679
|
if (!ospf)
|
|
3112
4680
|
return;
|
|
@@ -3141,7 +4709,7 @@ function flowExport2(config, device) {
|
|
|
3141
4709
|
});
|
|
3142
4710
|
config.separator();
|
|
3143
4711
|
}
|
|
3144
|
-
function
|
|
4712
|
+
function snmp3(config, device) {
|
|
3145
4713
|
const snmp = device.management?.snmp;
|
|
3146
4714
|
if (!snmp)
|
|
3147
4715
|
return;
|
|
@@ -3157,11 +4725,11 @@ function snmp2(config, device) {
|
|
|
3157
4725
|
config.line(`snmp-agent community read cipher ${marker(snmp.community)}${guarded ? ` acl ${MANAGEMENT_ACL2}` : ""}`);
|
|
3158
4726
|
if (snmp.users) {
|
|
3159
4727
|
config.line("snmp-agent mib-view included all iso");
|
|
3160
|
-
config.line(`snmp-agent group v3 ${
|
|
4728
|
+
config.line(`snmp-agent group v3 ${SNMP_GROUP3} privacy read-view all${guarded ? ` acl ${MANAGEMENT_ACL2}` : ""}`);
|
|
3161
4729
|
for (const [name, user] of Object.entries(snmp.users)) {
|
|
3162
|
-
config.line(`snmp-agent usm-user v3 ${name} group ${
|
|
3163
|
-
config.line(`snmp-agent usm-user v3 ${name} authentication-mode ${
|
|
3164
|
-
config.line(`snmp-agent usm-user v3 ${name} privacy-mode ${
|
|
4730
|
+
config.line(`snmp-agent usm-user v3 ${name} group ${SNMP_GROUP3}`);
|
|
4731
|
+
config.line(`snmp-agent usm-user v3 ${name} authentication-mode ${SNMP_AUTH4[user.auth]} cipher ${marker(user.auth_password)}`);
|
|
4732
|
+
config.line(`snmp-agent usm-user v3 ${name} privacy-mode ${SNMP_PRIVACY3[user.privacy]} cipher ${marker(user.privacy_password)}`);
|
|
3165
4733
|
}
|
|
3166
4734
|
}
|
|
3167
4735
|
config.separator();
|
|
@@ -3195,7 +4763,7 @@ function userInterfaces(config, device) {
|
|
|
3195
4763
|
config.block("user-interface vty 0 4", ["authentication-mode aaa", `protocol inbound ${device.management?.telnet ? "all" : "ssh"}`]);
|
|
3196
4764
|
}
|
|
3197
4765
|
var HOST_KEYS2 = ["ssh-rsa"];
|
|
3198
|
-
async function
|
|
4766
|
+
async function login3(device) {
|
|
3199
4767
|
const user = device.users?.[device.connection.user];
|
|
3200
4768
|
if (!user)
|
|
3201
4769
|
throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
@@ -3215,8 +4783,8 @@ async function openShell(client) {
|
|
|
3215
4783
|
await send(channel, "screen-length 0 temporary", 1e4, 1500);
|
|
3216
4784
|
return channel;
|
|
3217
4785
|
}
|
|
3218
|
-
async function
|
|
3219
|
-
const client = await
|
|
4786
|
+
async function read4(device) {
|
|
4787
|
+
const client = await login3(device);
|
|
3220
4788
|
try {
|
|
3221
4789
|
const channel = await openShell(client);
|
|
3222
4790
|
const dump = await send(channel, "display current-configuration", 120000, 3000);
|
|
@@ -3227,14 +4795,14 @@ async function read3(device) {
|
|
|
3227
4795
|
if (start < 0)
|
|
3228
4796
|
throw new Error(`${device.name} returned no configuration`);
|
|
3229
4797
|
const body = lines.slice(start).filter((line) => !/^<.*>$/.test(line.trim()) && !line.startsWith("!Last configuration"));
|
|
3230
|
-
return
|
|
4798
|
+
return scrub2(body.join(`
|
|
3231
4799
|
`).trim() + `
|
|
3232
4800
|
`);
|
|
3233
4801
|
} finally {
|
|
3234
4802
|
client.end();
|
|
3235
4803
|
}
|
|
3236
4804
|
}
|
|
3237
|
-
function
|
|
4805
|
+
function unsupported4(device) {
|
|
3238
4806
|
const found = [];
|
|
3239
4807
|
for (const [name, port] of Object.entries(device.ports ?? {})) {
|
|
3240
4808
|
if (!port)
|
|
@@ -3313,7 +4881,7 @@ function unsupported3(device) {
|
|
|
3313
4881
|
}
|
|
3314
4882
|
return found;
|
|
3315
4883
|
}
|
|
3316
|
-
var
|
|
4884
|
+
var REFUSED3 = /^\s*Error|Unrecognized command|Incomplete command|Wrong parameter|Too many parameters/m;
|
|
3317
4885
|
var INTERACTIVE = /^(snmp-agent usm-user v3 \S+ (?:authentication|privacy)-mode \S+) cipher <secret:([A-Za-z0-9_./:-]+)>$/;
|
|
3318
4886
|
async function sendLine(channel, line) {
|
|
3319
4887
|
const interactive = INTERACTIVE.exec(line.trim());
|
|
@@ -3330,21 +4898,21 @@ async function sendLine(channel, line) {
|
|
|
3330
4898
|
answer += await send(channel, "Y", 20000, 800);
|
|
3331
4899
|
return answer;
|
|
3332
4900
|
}
|
|
3333
|
-
async function
|
|
3334
|
-
const client = await
|
|
4901
|
+
async function apply4(device, _network, plan) {
|
|
4902
|
+
const client = await login3(device);
|
|
3335
4903
|
const channel = await openShell(client);
|
|
3336
4904
|
try {
|
|
3337
4905
|
await send(channel, "system-view", 1e4, 1500);
|
|
3338
4906
|
for (const step of plan.steps) {
|
|
3339
4907
|
for (const line of step.send) {
|
|
3340
4908
|
const answer = await sendLine(channel, line);
|
|
3341
|
-
if (
|
|
4909
|
+
if (REFUSED3.test(answer))
|
|
3342
4910
|
throw new Error(`${step.title}: ${line}
|
|
3343
4911
|
device said: ${answer.trim().slice(0, 300)}`);
|
|
3344
4912
|
}
|
|
3345
4913
|
}
|
|
3346
4914
|
const committed = await send(channel, "commit", 60000, 3000);
|
|
3347
|
-
if (
|
|
4915
|
+
if (REFUSED3.test(committed))
|
|
3348
4916
|
throw new Error(`commit refused: ${committed.trim().slice(0, 300)}`);
|
|
3349
4917
|
await send(channel, "quit", 1e4);
|
|
3350
4918
|
} catch (error) {
|
|
@@ -3355,7 +4923,7 @@ async function apply3(device, _network, plan) {
|
|
|
3355
4923
|
client.end();
|
|
3356
4924
|
throw error;
|
|
3357
4925
|
}
|
|
3358
|
-
const proof = await
|
|
4926
|
+
const proof = await login3(device).catch(() => {
|
|
3359
4927
|
return;
|
|
3360
4928
|
});
|
|
3361
4929
|
if (!proof) {
|
|
@@ -3371,7 +4939,7 @@ async function apply3(device, _network, plan) {
|
|
|
3371
4939
|
client.end();
|
|
3372
4940
|
console.log(" committed, login proved, saved");
|
|
3373
4941
|
}
|
|
3374
|
-
function
|
|
4942
|
+
function plan4(desired, current) {
|
|
3375
4943
|
const groups = compare(desired, current, dialect2);
|
|
3376
4944
|
vlanListChange(groups, "vlan batch ", batchIds, (ids) => [`vlan batch ${vlanBatch(ids)}`], (ids) => [`vlan batch ${vlanBatch(ids)}`]);
|
|
3377
4945
|
const top = groups.find((group) => group.path.length === 0);
|
|
@@ -3393,15 +4961,15 @@ function plan3(desired, current) {
|
|
|
3393
4961
|
}
|
|
3394
4962
|
var vrp = {
|
|
3395
4963
|
extension: ".cfg",
|
|
3396
|
-
unsupported:
|
|
3397
|
-
render:
|
|
3398
|
-
read:
|
|
3399
|
-
plan:
|
|
3400
|
-
apply:
|
|
4964
|
+
unsupported: unsupported4,
|
|
4965
|
+
render: render4,
|
|
4966
|
+
read: read4,
|
|
4967
|
+
plan: plan4,
|
|
4968
|
+
apply: apply4
|
|
3401
4969
|
};
|
|
3402
4970
|
|
|
3403
4971
|
// src/adapters/devices/index.ts
|
|
3404
|
-
var adapters = { routeros, vrp, "raisecom-ros": raisecom };
|
|
4972
|
+
var adapters = { routeros, vrp, "raisecom-ros": raisecom, junos };
|
|
3405
4973
|
|
|
3406
4974
|
// src/adapters/registries/index.ts
|
|
3407
4975
|
async function ripeStat(query, family) {
|
|
@@ -3422,7 +4990,7 @@ var registries = { "ripe-stat": ripeStat };
|
|
|
3422
4990
|
// src/adapters/registries/librenms.ts
|
|
3423
4991
|
var AUTH = { sha1: "SHA", sha256: "SHA-256" };
|
|
3424
4992
|
var PRIVACY = { aes128: "AES", des: "DES" };
|
|
3425
|
-
function
|
|
4993
|
+
function settingsOf3(network) {
|
|
3426
4994
|
if (!network.librenms)
|
|
3427
4995
|
throw new Error("the network has no librenms settings");
|
|
3428
4996
|
return {
|
|
@@ -3449,7 +5017,7 @@ function credentials(device) {
|
|
|
3449
5017
|
return;
|
|
3450
5018
|
}
|
|
3451
5019
|
async function planLibreNms(network) {
|
|
3452
|
-
const { base, headers } =
|
|
5020
|
+
const { base, headers } = settingsOf3(network);
|
|
3453
5021
|
const response = await fetch(`${base}/devices?type=all`, { headers, signal: AbortSignal.timeout(30000) });
|
|
3454
5022
|
if (!response.ok)
|
|
3455
5023
|
throw new Error(`LibreNMS answered ${response.status} to the device list`);
|
|
@@ -3463,7 +5031,7 @@ async function planLibreNms(network) {
|
|
|
3463
5031
|
};
|
|
3464
5032
|
}
|
|
3465
5033
|
async function sendLibreNms(network, additions) {
|
|
3466
|
-
const { base, headers } =
|
|
5034
|
+
const { base, headers } = settingsOf3(network);
|
|
3467
5035
|
for (const device of additions) {
|
|
3468
5036
|
const body = { hostname: device.connection.host, display_template: device.name, ...credentials(device) };
|
|
3469
5037
|
const response = await fetch(`${base}/devices`, {
|
|
@@ -3749,7 +5317,7 @@ function checkAddresses(holders) {
|
|
|
3749
5317
|
const first = new Map;
|
|
3750
5318
|
const isLoopback = (holder) => holder.device.interfaces?.[holder.name]?.type === "loopback";
|
|
3751
5319
|
for (const holder of holders) {
|
|
3752
|
-
const key = keyOf(host2(holder.address))
|
|
5320
|
+
const key = `${holder.vrf ?? ""} ${keyOf(host2(holder.address))}`;
|
|
3753
5321
|
const earlier = first.get(key);
|
|
3754
5322
|
if (!earlier) {
|
|
3755
5323
|
first.set(key, holder);
|
|
@@ -3853,7 +5421,9 @@ function checkTunnels(devices, owners) {
|
|
|
3853
5421
|
continue;
|
|
3854
5422
|
const report = (message) => findings.push({ level: "error", device: end.device.name, message: `interface ${end.name}: ${message}` });
|
|
3855
5423
|
const kind = end.iface.type === "gre" ? "GRE" : "VXLAN";
|
|
3856
|
-
const
|
|
5424
|
+
const back = ends.filter((other) => other.device === far && other.iface.type === end.iface.type && same(other.iface.local, end.iface.remote) && same(other.iface.remote, end.iface.local));
|
|
5425
|
+
const vniOf = (iface) => iface.type === "vxlan" ? iface.vni : undefined;
|
|
5426
|
+
const mirror = back.find((other) => vniOf(other.iface) === vniOf(end.iface)) ?? back[0];
|
|
3857
5427
|
if (!mirror) {
|
|
3858
5428
|
report(`${end.iface.remote} is ${far.name}'s, which has no ${kind} tunnel from there back to ${end.iface.local}`);
|
|
3859
5429
|
continue;
|
|
@@ -3895,7 +5465,7 @@ function validate(network) {
|
|
|
3895
5465
|
return findings;
|
|
3896
5466
|
}
|
|
3897
5467
|
|
|
3898
|
-
class
|
|
5468
|
+
class Names2 {
|
|
3899
5469
|
device;
|
|
3900
5470
|
report;
|
|
3901
5471
|
ports;
|
|
@@ -3922,7 +5492,7 @@ class Names {
|
|
|
3922
5492
|
}
|
|
3923
5493
|
}
|
|
3924
5494
|
function checkDevice(device, report) {
|
|
3925
|
-
const names = new
|
|
5495
|
+
const names = new Names2(device, report);
|
|
3926
5496
|
const cidr = (where, value) => {
|
|
3927
5497
|
if (!isCidr2(value))
|
|
3928
5498
|
report(`${where}: '${value}' is not an address with a length`);
|
|
@@ -3966,9 +5536,10 @@ function checkAddresses2(device, report, cidr, ip) {
|
|
|
3966
5536
|
if (typeof entry !== "string")
|
|
3967
5537
|
ip(where, entry.peer);
|
|
3968
5538
|
const bare = address.split("/")[0];
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
5539
|
+
const key = `${settings?.vrf ?? ""} ${bare}`;
|
|
5540
|
+
if (holderOf.has(key))
|
|
5541
|
+
report(`${bare} is on both ${holderOf.get(key)} and ${where}`);
|
|
5542
|
+
holderOf.set(key, where);
|
|
3972
5543
|
}
|
|
3973
5544
|
for (const group of settings?.vrrp ?? []) {
|
|
3974
5545
|
ip(`${where} VRRP ${group.id}`, group.address);
|
|
@@ -4824,7 +6395,7 @@ async function studioCommand() {
|
|
|
4824
6395
|
const port = Number(flagValue("port") ?? 4600);
|
|
4825
6396
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
4826
6397
|
fail("--port takes a number from 1 to 65535");
|
|
4827
|
-
const { isLoopback, studio } = await import("../chunks/server-
|
|
6398
|
+
const { isLoopback, studio } = await import("../chunks/server-n2q4tzy4.js");
|
|
4828
6399
|
const host = flagValue("host") ?? "127.0.0.1";
|
|
4829
6400
|
const allowHosts = (flagValue("allow-host") ?? "").split(",").filter(Boolean);
|
|
4830
6401
|
if (!isLoopback(host) && !allowHosts.length) {
|
|
@@ -4850,9 +6421,9 @@ var commands = {
|
|
|
4850
6421
|
graph: graphCommand,
|
|
4851
6422
|
studio: studioCommand
|
|
4852
6423
|
};
|
|
4853
|
-
var
|
|
4854
|
-
if (
|
|
4855
|
-
await
|
|
6424
|
+
var run2 = commands[command2];
|
|
6425
|
+
if (run2)
|
|
6426
|
+
await run2();
|
|
4856
6427
|
else
|
|
4857
6428
|
fail(`no command ${command2}
|
|
4858
6429
|
|