@takodotid/circuit 0.41.0 → 0.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/index-6wb41q67.js +62 -0
- package/dist/chunks/{index-k38bby43.js → index-yzw86rc8.js} +4 -62
- package/dist/chunks/{server-ggertnce.js → server-cxr9ynxc.js} +1 -1
- package/dist/cli/index.js +226 -55
- package/dist/helpers/index.js +14 -9
- package/dist/types/core/cabling.d.ts +0 -6
- package/dist/types/core/define.d.ts +1 -1
- package/dist/types/core/validator.d.ts +2 -0
- package/dist/types/helpers/cables.d.ts +14 -11
- package/dist/types/helpers/exchange.d.ts +7 -1
- package/dist/types/helpers/upstreams.d.ts +11 -3
- package/dist/types/platforms/bgp-wait.d.ts +2 -2
- package/dist/types/platforms/derive.d.ts +1 -1
- package/dist/types/platforms/fortios/routing.d.ts +2 -2
- package/dist/types/platforms/junos/bgp.d.ts +2 -2
- package/dist/types/platforms/platform.d.ts +3 -0
- package/dist/types/platforms/routeros/index.d.ts +14 -1
- package/dist/types/platforms/routeros/plan.d.ts +4 -0
- package/dist/types/platforms/routeros/render.d.ts +4 -0
- package/dist/types/schema/device.d.ts +4 -4
- package/dist/types/schema/interfaces.d.ts +1 -1
- package/dist/types/schema/routing.d.ts +43 -10
- package/package.json +1 -1
- package/src/cli/index.ts +26 -29
- package/src/core/cabling.ts +0 -30
- package/src/core/define.ts +3 -1
- package/src/core/validator.ts +64 -1
- package/src/helpers/cables.ts +20 -15
- package/src/helpers/exchange.ts +9 -3
- package/src/helpers/upstreams.ts +9 -4
- package/src/platforms/bgp-wait.ts +11 -11
- package/src/platforms/derive.ts +23 -1
- package/src/platforms/fortios/routing.ts +2 -2
- package/src/platforms/junos/bgp.ts +2 -2
- package/src/platforms/platform.ts +3 -0
- package/src/platforms/routeros/TRAPS.md +6 -0
- package/src/platforms/routeros/export.ts +1 -0
- package/src/platforms/routeros/index.ts +133 -4
- package/src/platforms/routeros/plan.ts +20 -1
- package/src/platforms/routeros/render.ts +28 -3
- package/src/schema/device.ts +4 -3
- package/src/schema/interfaces.ts +1 -1
- package/src/schema/routing.ts +46 -10
- package/templates/colocation/checks.ts +14 -11
- package/templates/colocation/edge-01.ts +2 -2
- package/templates/colocation/tenancy.ts +50 -71
- package/templates/colocation/tenants/acme.ts +10 -10
- package/templates/colocation/tenants/index.ts +2 -1
- package/templates/colocation/tor-01.ts +2 -2
- package/templates/project/AGENTS.md +15 -15
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/platforms/fortios/models.ts
|
|
2
|
+
var models = {
|
|
3
|
+
"FG-3700D": {
|
|
4
|
+
ports: { "40g": 4, "10g": 28, "1g": 2 },
|
|
5
|
+
spelling: { "40g": "port{n}", "10g": "port{n+4}", "1g": "mgmt{n}" }
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// src/platforms/junos/models.ts
|
|
10
|
+
var models2 = {
|
|
11
|
+
"QFX5100-48S-6Q": {
|
|
12
|
+
ports: { "40g": 6, "10g": 48, "1g": 2 },
|
|
13
|
+
spelling: { "40g": "et-0/0/{n+47}", "10g": "xe-0/0/{n-1}", "1g": "em{n-1}" }
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/platforms/raisecom-ros/models.ts
|
|
18
|
+
var models3 = {
|
|
19
|
+
"RAX721-C-6C48": {
|
|
20
|
+
ports: { "100g": 6, "25g": 24, "10g": 24 },
|
|
21
|
+
spelling: { "100g": "hundredgige 1/1/{n}", "25g": "twenty-fivegige 1/2/{n}", "10g": "tengigabitethernet 1/3/{n}" }
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/platforms/routeros/models.ts
|
|
26
|
+
var models4 = {
|
|
27
|
+
"CCR2216-1G-12XS-2XQ": {
|
|
28
|
+
ports: { "100g": 2, "25g": 12, "1g": 1 },
|
|
29
|
+
spelling: { "100g": "qsfp28-{n}-1", "25g": "sfp28-{n}", "1g": "ether{n}" },
|
|
30
|
+
limits: { lag_members: 8, switch_port_ranges: 5 }
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// src/platforms/vrp/models.ts
|
|
35
|
+
var models5 = {
|
|
36
|
+
"CE6855-48S6Q-HI": { ports: { "10g": 48, "40g": 6 }, spelling: { "10g": "10GE1/0/{n}", "40g": "40GE1/0/{n}" } }
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/platforms/catalog.ts
|
|
40
|
+
class Catalog {
|
|
41
|
+
static MODELS = { routeros: models4, vrp: models5, "raisecom-ros": models3, junos: models2, fortios: models };
|
|
42
|
+
static ports(platform, model) {
|
|
43
|
+
const spec = Catalog.MODELS[platform]?.[model];
|
|
44
|
+
if (!spec)
|
|
45
|
+
throw new Error(`${platform} has no model ${model}`);
|
|
46
|
+
return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));
|
|
47
|
+
}
|
|
48
|
+
static limits(platform, model) {
|
|
49
|
+
const spec = Catalog.MODELS[platform]?.[model];
|
|
50
|
+
return spec?.limits ?? {};
|
|
51
|
+
}
|
|
52
|
+
static spell(platform, model, port) {
|
|
53
|
+
const spec = Catalog.MODELS[platform]?.[model];
|
|
54
|
+
const parts = /^(\d+[gm])-(\d+)$/.exec(port);
|
|
55
|
+
const template = parts && spec?.spelling[parts[1]];
|
|
56
|
+
if (!template)
|
|
57
|
+
throw new Error(`${model} has no port ${port}`);
|
|
58
|
+
return template.replace(/\{n([+-]\d+)?\}/, (_, shift) => String(Number(parts[2]) + Number(shift ?? 0)));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Catalog };
|
|
@@ -1,68 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Catalog
|
|
3
|
+
} from "./index-6wb41q67.js";
|
|
1
4
|
import {
|
|
2
5
|
Address2
|
|
3
6
|
} from "./index-n7w3tg5y.js";
|
|
4
7
|
|
|
5
|
-
// src/platforms/fortios/models.ts
|
|
6
|
-
var models = {
|
|
7
|
-
"FG-3700D": {
|
|
8
|
-
ports: { "40g": 4, "10g": 28, "1g": 2 },
|
|
9
|
-
spelling: { "40g": "port{n}", "10g": "port{n+4}", "1g": "mgmt{n}" }
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
// src/platforms/junos/models.ts
|
|
14
|
-
var models2 = {
|
|
15
|
-
"QFX5100-48S-6Q": {
|
|
16
|
-
ports: { "40g": 6, "10g": 48, "1g": 2 },
|
|
17
|
-
spelling: { "40g": "et-0/0/{n+47}", "10g": "xe-0/0/{n-1}", "1g": "em{n-1}" }
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
// src/platforms/raisecom-ros/models.ts
|
|
22
|
-
var models3 = {
|
|
23
|
-
"RAX721-C-6C48": {
|
|
24
|
-
ports: { "100g": 6, "25g": 24, "10g": 24 },
|
|
25
|
-
spelling: { "100g": "hundredgige 1/1/{n}", "25g": "twenty-fivegige 1/2/{n}", "10g": "tengigabitethernet 1/3/{n}" }
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// src/platforms/routeros/models.ts
|
|
30
|
-
var models4 = {
|
|
31
|
-
"CCR2216-1G-12XS-2XQ": {
|
|
32
|
-
ports: { "100g": 2, "25g": 12, "1g": 1 },
|
|
33
|
-
spelling: { "100g": "qsfp28-{n}-1", "25g": "sfp28-{n}", "1g": "ether{n}" },
|
|
34
|
-
limits: { lag_members: 8, switch_port_ranges: 5 }
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// src/platforms/vrp/models.ts
|
|
39
|
-
var models5 = {
|
|
40
|
-
"CE6855-48S6Q-HI": { ports: { "10g": 48, "40g": 6 }, spelling: { "10g": "10GE1/0/{n}", "40g": "40GE1/0/{n}" } }
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// src/platforms/catalog.ts
|
|
44
|
-
class Catalog {
|
|
45
|
-
static MODELS = { routeros: models4, vrp: models5, "raisecom-ros": models3, junos: models2, fortios: models };
|
|
46
|
-
static ports(platform, model) {
|
|
47
|
-
const spec = Catalog.MODELS[platform]?.[model];
|
|
48
|
-
if (!spec)
|
|
49
|
-
throw new Error(`${platform} has no model ${model}`);
|
|
50
|
-
return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));
|
|
51
|
-
}
|
|
52
|
-
static limits(platform, model) {
|
|
53
|
-
const spec = Catalog.MODELS[platform]?.[model];
|
|
54
|
-
return spec?.limits ?? {};
|
|
55
|
-
}
|
|
56
|
-
static spell(platform, model, port) {
|
|
57
|
-
const spec = Catalog.MODELS[platform]?.[model];
|
|
58
|
-
const parts = /^(\d+[gm])-(\d+)$/.exec(port);
|
|
59
|
-
const template = parts && spec?.spelling[parts[1]];
|
|
60
|
-
if (!template)
|
|
61
|
-
throw new Error(`${model} has no port ${port}`);
|
|
62
|
-
return template.replace(/\{n([+-]\d+)?\}/, (_, shift) => String(Number(parts[2]) + Number(shift ?? 0)));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
8
|
// src/core/topology.ts
|
|
67
9
|
class Builder {
|
|
68
10
|
nodes = new Map;
|
|
@@ -976,4 +918,4 @@ class StudioModel {
|
|
|
976
918
|
}
|
|
977
919
|
}
|
|
978
920
|
|
|
979
|
-
export {
|
|
921
|
+
export { Topology, AddressSpace, StudioModel };
|
package/dist/cli/index.js
CHANGED
|
@@ -4,11 +4,13 @@ import {
|
|
|
4
4
|
Secrets2
|
|
5
5
|
} from "../chunks/index-20kcjxjg.js";
|
|
6
6
|
import {
|
|
7
|
-
Catalog,
|
|
8
7
|
Topology,
|
|
9
8
|
AddressSpace,
|
|
10
9
|
StudioModel
|
|
11
|
-
} from "../chunks/index-
|
|
10
|
+
} from "../chunks/index-yzw86rc8.js";
|
|
11
|
+
import {
|
|
12
|
+
Catalog
|
|
13
|
+
} from "../chunks/index-6wb41q67.js";
|
|
12
14
|
import {
|
|
13
15
|
Address2
|
|
14
16
|
} from "../chunks/index-n7w3tg5y.js";
|
|
@@ -547,9 +549,23 @@ class Derive {
|
|
|
547
549
|
for (const route of routing?.static ?? []) {
|
|
548
550
|
if (route.bfd && !has("static-route-bfd"))
|
|
549
551
|
found.push(`routing.static ${route.prefix} bfd, BFD on a static route is ${unproved}`);
|
|
552
|
+
if (route.table && !has("routing-tables"))
|
|
553
|
+
found.push(`routing.static ${route.prefix} table, a routing table beside the main one is ${unproved}`);
|
|
554
|
+
}
|
|
555
|
+
if (!has("routing-tables")) {
|
|
556
|
+
if (routing?.tables)
|
|
557
|
+
found.push(`routing.tables, a routing table beside the main one is ${unproved}`);
|
|
558
|
+
if (routing?.rules)
|
|
559
|
+
found.push(`routing.rules, routing rules are ${unproved}`);
|
|
550
560
|
}
|
|
551
561
|
const bgp = routing?.bgp;
|
|
562
|
+
for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
|
|
563
|
+
if (group.table && !has("routing-tables"))
|
|
564
|
+
found.push(`routing.bgp.groups.${name}.table, a routing table beside the main one is ${unproved}`);
|
|
565
|
+
}
|
|
552
566
|
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
567
|
+
if (neighbor.table && !has("routing-tables"))
|
|
568
|
+
found.push(`routing.bgp.neighbors.${name}.table, a routing table beside the main one is ${unproved}`);
|
|
553
569
|
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
554
570
|
const bfd = neighbor.bfd ?? group?.bfd;
|
|
555
571
|
const multihop = neighbor.multihop ?? group?.multihop;
|
|
@@ -3969,28 +3985,6 @@ class Cabling {
|
|
|
3969
3985
|
}
|
|
3970
3986
|
return findings;
|
|
3971
3987
|
}
|
|
3972
|
-
static describeLinks(devices) {
|
|
3973
|
-
const byName = new Map(devices.map((device) => [device.name, device]));
|
|
3974
|
-
return devices.map((device) => {
|
|
3975
|
-
const ports = Object.entries(device.ports ?? {});
|
|
3976
|
-
if (!ports.some(([, port]) => port?.link && port.description === undefined))
|
|
3977
|
-
return device;
|
|
3978
|
-
const described = ports.map(([name, port]) => {
|
|
3979
|
-
const far = port?.link ? byName.get(port.link.device) : undefined;
|
|
3980
|
-
if (!port?.link || port.description !== undefined || !far)
|
|
3981
|
-
return [name, port];
|
|
3982
|
-
return [name, { ...port, description: `${far.name} ${Cabling.labelOf(far, port.link.port)}` }];
|
|
3983
|
-
});
|
|
3984
|
-
return { ...device, ports: Object.fromEntries(described) };
|
|
3985
|
-
});
|
|
3986
|
-
}
|
|
3987
|
-
static labelOf(device, port) {
|
|
3988
|
-
try {
|
|
3989
|
-
return Catalog.spell(device.platform, device.model, port);
|
|
3990
|
-
} catch {
|
|
3991
|
-
return port;
|
|
3992
|
-
}
|
|
3993
|
-
}
|
|
3994
3988
|
static isDevice(system, name) {
|
|
3995
3989
|
return system === name || system.startsWith(`${name}.`);
|
|
3996
3990
|
}
|
|
@@ -6828,13 +6822,13 @@ class BgpWait {
|
|
|
6828
6822
|
static AFTER_APPLY = { seconds: 180, every: 10 };
|
|
6829
6823
|
static async until(read, enough, limits, waiting = () => {}) {
|
|
6830
6824
|
const deadline = Date.now() + limits.seconds * 1000;
|
|
6831
|
-
let
|
|
6832
|
-
while (!enough(
|
|
6833
|
-
waiting(
|
|
6825
|
+
let value = await read();
|
|
6826
|
+
while (!enough(value) && Date.now() < deadline) {
|
|
6827
|
+
waiting(value);
|
|
6834
6828
|
await sleep(Math.min(limits.every * 1000, Math.max(0, deadline - Date.now())));
|
|
6835
|
-
|
|
6829
|
+
value = await read();
|
|
6836
6830
|
}
|
|
6837
|
-
return
|
|
6831
|
+
return value;
|
|
6838
6832
|
}
|
|
6839
6833
|
static upCount(sessions) {
|
|
6840
6834
|
return sessions.filter((session) => session.established).length;
|
|
@@ -6954,6 +6948,7 @@ class RouterOsExport {
|
|
|
6954
6948
|
};
|
|
6955
6949
|
static ORDERED = new Set([
|
|
6956
6950
|
"/routing filter rule",
|
|
6951
|
+
"/routing rule",
|
|
6957
6952
|
"/ip firewall filter",
|
|
6958
6953
|
"/ipv6 firewall filter",
|
|
6959
6954
|
"/ip firewall nat",
|
|
@@ -7135,6 +7130,8 @@ class RouterOsPlan {
|
|
|
7135
7130
|
"/interface bridge filter",
|
|
7136
7131
|
"/interface list",
|
|
7137
7132
|
"/ip vrf",
|
|
7133
|
+
"/routing table",
|
|
7134
|
+
"/routing rule",
|
|
7138
7135
|
"/ip address",
|
|
7139
7136
|
"/ipv6 address",
|
|
7140
7137
|
"/ip pool",
|
|
@@ -7179,6 +7176,7 @@ class RouterOsPlan {
|
|
|
7179
7176
|
"/interface bridge vlan"
|
|
7180
7177
|
]);
|
|
7181
7178
|
static CHANGED_IN_PLACE = new Set(["/interface vlan"]);
|
|
7179
|
+
static RESTART_ON = { "/routing bgp connection": ["routing-table"] };
|
|
7182
7180
|
static OLD_RULES = "frameworkOldRules";
|
|
7183
7181
|
problems = [];
|
|
7184
7182
|
constructor(sendSecrets) {
|
|
@@ -7349,8 +7347,16 @@ class RouterOsPlan {
|
|
|
7349
7347
|
const update = this.changes(entry, current, `${menu} ${entryKey}`);
|
|
7350
7348
|
if (!Object.keys(update).length)
|
|
7351
7349
|
continue;
|
|
7350
|
+
const selector = RouterOsPlan.find(menu, RouterOsExport.identity(current));
|
|
7351
|
+
const restartsOn = (RouterOsPlan.RESTART_ON[menu] ?? []).filter((property) => (property in update));
|
|
7352
|
+
if (restartsOn.length) {
|
|
7353
|
+
changeStep.show.push(`~ ${entryKey} ${RouterOsPlan.describe(update)}, restarting the session: it takes a new ${restartsOn.join(" and ")} only when it restarts`);
|
|
7354
|
+
changeStep.send.push(RouterOsPlan.line(menu, "set", selector, [], { disabled: "yes", ...update }));
|
|
7355
|
+
changeStep.send.push(RouterOsPlan.line(menu, "set", selector, [], { disabled: "no" }));
|
|
7356
|
+
continue;
|
|
7357
|
+
}
|
|
7352
7358
|
changeStep.show.push(`~ ${entryKey} ${RouterOsPlan.describe(update)}`);
|
|
7353
|
-
changeStep.send.push(RouterOsPlan.line(menu, "set",
|
|
7359
|
+
changeStep.send.push(RouterOsPlan.line(menu, "set", selector, [], update));
|
|
7354
7360
|
}
|
|
7355
7361
|
const removed = existingEntries.filter((entry) => !kept.has(key(entry)));
|
|
7356
7362
|
const wholeLists = new Set;
|
|
@@ -8297,6 +8303,8 @@ class RouterOsRender {
|
|
|
8297
8303
|
return rules;
|
|
8298
8304
|
}
|
|
8299
8305
|
routing() {
|
|
8306
|
+
this.routingTables();
|
|
8307
|
+
this.routingRules();
|
|
8300
8308
|
this.staticRoutes();
|
|
8301
8309
|
this.rpki();
|
|
8302
8310
|
if (this.device.routing?.bgp) {
|
|
@@ -8307,6 +8315,24 @@ class RouterOsRender {
|
|
|
8307
8315
|
this.ospf();
|
|
8308
8316
|
this.bfd();
|
|
8309
8317
|
}
|
|
8318
|
+
routingTables() {
|
|
8319
|
+
for (const [name, table] of Object.entries(this.device.routing?.tables ?? {})) {
|
|
8320
|
+
this.add("/routing table", "add fib", { comment: table.description, name });
|
|
8321
|
+
}
|
|
8322
|
+
}
|
|
8323
|
+
routingRules() {
|
|
8324
|
+
for (const rule of this.device.routing?.rules ?? []) {
|
|
8325
|
+
const onlyInTable = rule.table !== "main" && (rule.fallback ?? "none") === "none";
|
|
8326
|
+
this.add("/routing rule", "add", {
|
|
8327
|
+
action: onlyInTable ? "lookup-only-in-table" : "lookup",
|
|
8328
|
+
comment: rule.description,
|
|
8329
|
+
"dst-address": rule.match?.dst,
|
|
8330
|
+
interface: rule.match?.in_interface && this.name(rule.match.in_interface),
|
|
8331
|
+
"src-address": rule.match?.src,
|
|
8332
|
+
table: rule.table
|
|
8333
|
+
});
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8310
8336
|
staticRoutes() {
|
|
8311
8337
|
for (const route of this.device.routing?.static ?? []) {
|
|
8312
8338
|
const menu = Address2.family(Address2.host(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
|
|
@@ -8317,7 +8343,7 @@ class RouterOsRender {
|
|
|
8317
8343
|
distance: route.distance,
|
|
8318
8344
|
"dst-address": route.prefix,
|
|
8319
8345
|
gateway,
|
|
8320
|
-
"routing-table": route.vrf
|
|
8346
|
+
"routing-table": route.vrf ?? route.table
|
|
8321
8347
|
});
|
|
8322
8348
|
}
|
|
8323
8349
|
}
|
|
@@ -8438,6 +8464,7 @@ class RouterOsRender {
|
|
|
8438
8464
|
"output.network": originates ? RouterOsRender.NETWORKS[family] : undefined,
|
|
8439
8465
|
"remote.address": `${neighbor.address}/${addressFamily === "ipv4" ? 32 : 128}`,
|
|
8440
8466
|
"remote.as": settings.remote_as,
|
|
8467
|
+
"routing-table": settings.table,
|
|
8441
8468
|
"tcp-md5-key": settings.password && Secrets2.marker(settings.password),
|
|
8442
8469
|
"use-bfd": settings.bfd ? "yes" : undefined
|
|
8443
8470
|
});
|
|
@@ -8669,6 +8696,10 @@ class RouterOs extends Platform {
|
|
|
8669
8696
|
':put ("disabled|" . [/routing bgp connection get $c name] . "|" . [/routing bgp connection get $c remote.address])',
|
|
8670
8697
|
"}"
|
|
8671
8698
|
].join(" ");
|
|
8699
|
+
static listAdvertisements(prefixes) {
|
|
8700
|
+
const where = prefixes.map((prefix) => `dst=${prefix}`).join(" or ");
|
|
8701
|
+
return `:foreach a in=[/routing bgp advertisements print as-value where ${where}] do={ :put ("ADV|" . ($a->"peer") . "|" . ($a->"dst")) }`;
|
|
8702
|
+
}
|
|
8672
8703
|
static clearLimit(address) {
|
|
8673
8704
|
return `/routing bgp session clear [find remote.address=${address} and limit-exceeded] flag=limit-exceeded`;
|
|
8674
8705
|
}
|
|
@@ -8695,6 +8726,42 @@ class RouterOs extends Platform {
|
|
|
8695
8726
|
};
|
|
8696
8727
|
extension = ".rsc";
|
|
8697
8728
|
console = RouterOs.CONSOLE;
|
|
8729
|
+
checks(device) {
|
|
8730
|
+
const bgp = device.routing?.bgp;
|
|
8731
|
+
const statics = device.routing?.static ?? [];
|
|
8732
|
+
const findings = [];
|
|
8733
|
+
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
8734
|
+
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
8735
|
+
const table = neighbor.table ?? group?.table;
|
|
8736
|
+
if (!table)
|
|
8737
|
+
continue;
|
|
8738
|
+
const families = neighbor.families ?? group?.families ?? [Address2.family(neighbor.address)];
|
|
8739
|
+
const networks = (bgp?.networks ?? []).filter((prefix) => families.includes(Address2.family(Address2.host(prefix))));
|
|
8740
|
+
const inTable = (prefix) => statics.some((route) => route.table === table && Address2.canonical(route.prefix) === Address2.canonical(prefix));
|
|
8741
|
+
const missing = networks.filter((prefix) => !inTable(prefix));
|
|
8742
|
+
if (!missing.length)
|
|
8743
|
+
continue;
|
|
8744
|
+
const where = `routing.bgp.neighbors.${name}`;
|
|
8745
|
+
const route = (prefix) => `{ prefix: "${prefix}", blackhole: true, table: "${table}" }`;
|
|
8746
|
+
const fix = `On RouterOS a session announces from its own table, ${table}`;
|
|
8747
|
+
if (missing.length === networks.length) {
|
|
8748
|
+
findings.push({
|
|
8749
|
+
level: "error",
|
|
8750
|
+
device: device.name,
|
|
8751
|
+
message: `${where} would announce nothing: ${fix}, which has no route for any of networks. Add the routes it announces to ${table} too, such as ${route(missing[0])} in routing.static`
|
|
8752
|
+
});
|
|
8753
|
+
continue;
|
|
8754
|
+
}
|
|
8755
|
+
for (const prefix of missing) {
|
|
8756
|
+
findings.push({
|
|
8757
|
+
level: "warning",
|
|
8758
|
+
device: device.name,
|
|
8759
|
+
message: `${where} does not announce ${prefix}: ${fix}, which has no route for it. If its export should announce it, add ${route(prefix)} to routing.static`
|
|
8760
|
+
});
|
|
8761
|
+
}
|
|
8762
|
+
}
|
|
8763
|
+
return findings;
|
|
8764
|
+
}
|
|
8698
8765
|
unsupported(device) {
|
|
8699
8766
|
const found = [];
|
|
8700
8767
|
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
@@ -8761,7 +8828,14 @@ class RouterOs extends Platform {
|
|
|
8761
8828
|
if (route.bfd)
|
|
8762
8829
|
found.push(`routing.static ${route.prefix} bfd, a static route's check-gateway takes arp, none or ping on 7.23`);
|
|
8763
8830
|
}
|
|
8764
|
-
found.push(...Derive.unproved(device, [
|
|
8831
|
+
found.push(...Derive.unproved(device, [
|
|
8832
|
+
"bfd-timers",
|
|
8833
|
+
"multihop-bfd",
|
|
8834
|
+
"static-route-bfd",
|
|
8835
|
+
"mac-limit",
|
|
8836
|
+
"ethertype-filter",
|
|
8837
|
+
"routing-tables"
|
|
8838
|
+
]));
|
|
8765
8839
|
return found;
|
|
8766
8840
|
}
|
|
8767
8841
|
render(device) {
|
|
@@ -8925,6 +8999,7 @@ ${certificates}` : "";
|
|
|
8925
8999
|
const switchRules = plan.steps.some((step) => step.send.some((command) => command.startsWith(RouterOs.SWITCH_RULES)));
|
|
8926
9000
|
const limits = options.bgp ?? BgpWait.AFTER_APPLY;
|
|
8927
9001
|
const establishedBefore = await this.establishedSessions(device, options);
|
|
9002
|
+
const announcedBefore = establishedBefore.length ? await this.announcedNow(device, options) : [];
|
|
8928
9003
|
return {
|
|
8929
9004
|
run: async () => {
|
|
8930
9005
|
if (!switchRules && !establishedBefore.length) {
|
|
@@ -8935,7 +9010,8 @@ ${certificates}` : "";
|
|
|
8935
9010
|
try {
|
|
8936
9011
|
const switchProblems = switchRules ? await RouterOs.invalidSwitchRules(session, device) : [];
|
|
8937
9012
|
const bgpProblems = await this.bgpBack(device, session, establishedBefore, limits);
|
|
8938
|
-
|
|
9013
|
+
const announceProblems = await this.announcedBack(device, session, announcedBefore, limits);
|
|
9014
|
+
return [...switchProblems, ...bgpProblems, ...announceProblems];
|
|
8939
9015
|
} finally {
|
|
8940
9016
|
session.close();
|
|
8941
9017
|
}
|
|
@@ -9003,7 +9079,51 @@ ${certificates}` : "";
|
|
|
9003
9079
|
console.log(` BGP: all ${neighbors.length} established again`);
|
|
9004
9080
|
return [];
|
|
9005
9081
|
}
|
|
9006
|
-
return down.map((bgp) => bgp.limit ? `BGP session ${bgp.neighbor} was established before the apply and closed on its limit${bgp.limit.max !== undefined ? ` of ${bgp.limit.max} prefixes` : ""}: the neighbor sends more than it may. Find out why, then: circuit bgp ${device.name}
|
|
9082
|
+
return down.map((bgp) => bgp.limit ? `BGP session ${bgp.neighbor} was established before the apply and closed on its limit${bgp.limit.max !== undefined ? ` of ${bgp.limit.max} prefixes` : ""}: the neighbor sends more than it may. Find out why, then: circuit bgp ${device.name} --neighbors '^${bgp.neighbor}$' --clear-limit` : `BGP session ${bgp.neighbor} was established before the apply and is not ${limits.seconds} s after it (${bgp.reason ?? (bgp.uptime === "no session" ? "no session" : "not established")}). Look with: circuit bgp ${device.name} --log`);
|
|
9083
|
+
}
|
|
9084
|
+
async announcedNow(device, options) {
|
|
9085
|
+
if (!device.routing?.bgp?.networks?.length)
|
|
9086
|
+
return [];
|
|
9087
|
+
const session = await this.session(device, options);
|
|
9088
|
+
try {
|
|
9089
|
+
return await this.announced(device, session);
|
|
9090
|
+
} finally {
|
|
9091
|
+
session.close();
|
|
9092
|
+
}
|
|
9093
|
+
}
|
|
9094
|
+
async announced(device, session) {
|
|
9095
|
+
const bgp = device.routing?.bgp;
|
|
9096
|
+
const networks = bgp?.networks ?? [];
|
|
9097
|
+
if (!networks.length)
|
|
9098
|
+
return [];
|
|
9099
|
+
const sessions = (await session.run(RouterOs.LIST_BGP_SESSIONS, 60000)).split(/\r?\n/).map((line) => line.trim().split("|")).filter((fields) => fields.length > 1 && fields[0] !== "disabled");
|
|
9100
|
+
const neighbors = Object.entries(bgp?.neighbors ?? {});
|
|
9101
|
+
const neighborOf = (sessionName) => {
|
|
9102
|
+
const remote = sessions.find(([name]) => name === sessionName)?.[1];
|
|
9103
|
+
const byAddress = neighbors.find(([, neighbor]) => neighbor.address === remote);
|
|
9104
|
+
const byName = neighbors.find(([name]) => name === sessionName.replace(/-\d+$/, ""));
|
|
9105
|
+
return (byAddress ?? byName)?.[0];
|
|
9106
|
+
};
|
|
9107
|
+
const output = await session.run(RouterOs.listAdvertisements(networks), 60000);
|
|
9108
|
+
return output.split(/\r?\n/).map((line) => line.trim().split("|")).flatMap(([marker, peer, prefix]) => {
|
|
9109
|
+
const neighbor = marker === "ADV" && peer ? neighborOf(peer) : undefined;
|
|
9110
|
+
return neighbor && prefix ? [`${neighbor} ${prefix}`] : [];
|
|
9111
|
+
}).sort();
|
|
9112
|
+
}
|
|
9113
|
+
async announcedBack(device, session, before, limits) {
|
|
9114
|
+
if (!before.length)
|
|
9115
|
+
return [];
|
|
9116
|
+
const missing = (now) => before.filter((pair) => !now.includes(pair));
|
|
9117
|
+
const after = await BgpWait.until(() => this.announced(device, session), (now) => !missing(now).length, limits, (now) => console.log(` BGP: ${missing(now).length} announcement(s) not back yet, checking again in ${limits.every} s`));
|
|
9118
|
+
const gone = missing(after);
|
|
9119
|
+
if (!gone.length) {
|
|
9120
|
+
console.log(` BGP: every session announces what it announced before`);
|
|
9121
|
+
return [];
|
|
9122
|
+
}
|
|
9123
|
+
return gone.map((pair) => {
|
|
9124
|
+
const [neighbor, prefix] = pair.split(" ");
|
|
9125
|
+
return `BGP session ${neighbor} announced ${prefix} before the apply and does not ${limits.seconds} s after it, though it may still be established. A RouterOS session announces from its routing table: that table needs a route for ${prefix}, and a session whose table changed must restart. Look with: circuit show ${device.name} '/routing bgp advertisements print'`;
|
|
9126
|
+
});
|
|
9007
9127
|
}
|
|
9008
9128
|
static async invalidSwitchRules(session, device) {
|
|
9009
9129
|
const answer = (await session.run(RouterOs.COUNT_INVALID_SWITCH_RULES)).trim();
|
|
@@ -11220,9 +11340,11 @@ class Validator {
|
|
|
11220
11340
|
const adapter = Platforms.find(device.platform);
|
|
11221
11341
|
if (!adapter)
|
|
11222
11342
|
report(`no adapter for platform ${device.platform}`);
|
|
11223
|
-
else
|
|
11343
|
+
else {
|
|
11224
11344
|
for (const field of adapter.unsupported(device))
|
|
11225
11345
|
report(`${device.platform} cannot express ${field}`);
|
|
11346
|
+
findings.push(...adapter.checks?.(device) ?? []);
|
|
11347
|
+
}
|
|
11226
11348
|
}
|
|
11227
11349
|
findings.push(...CrossChecks.run(network.devices));
|
|
11228
11350
|
for (const check of network.checks ?? [])
|
|
@@ -11433,15 +11555,21 @@ class Validator {
|
|
|
11433
11555
|
report(`static route ${route.prefix} has bfd without via, the address BFD watches`);
|
|
11434
11556
|
names.expectInterface(`static route ${route.prefix} interface`, route.interface);
|
|
11435
11557
|
names.expect(`static route ${route.prefix} VRF`, route.vrf, route.vrf !== undefined && route.vrf in (device.vrfs ?? {}));
|
|
11558
|
+
names.expect(`static route ${route.prefix} table`, route.table, route.table !== undefined && route.table in (routing?.tables ?? {}));
|
|
11559
|
+
if (route.vrf && route.table)
|
|
11560
|
+
report(`static route ${route.prefix} has both vrf and table; a route is in one of them`);
|
|
11436
11561
|
}
|
|
11562
|
+
Validator.checkTables(device, names, report, cidr);
|
|
11437
11563
|
const bgp = routing?.bgp;
|
|
11438
11564
|
const isPolicy = (policy) => policy !== undefined && (policy in policies);
|
|
11439
11565
|
for (const prefix of bgp?.networks ?? [])
|
|
11440
11566
|
cidr("BGP network", prefix);
|
|
11567
|
+
const isTable = (table) => table !== undefined && (table in (routing?.tables ?? {}));
|
|
11441
11568
|
for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
|
|
11442
11569
|
for (const policy of [group.import, group.export]) {
|
|
11443
11570
|
names.expect(`BGP group ${name} policy`, policy, isPolicy(policy));
|
|
11444
11571
|
}
|
|
11572
|
+
names.expect(`BGP group ${name} table`, group.table, isTable(group.table));
|
|
11445
11573
|
}
|
|
11446
11574
|
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
11447
11575
|
ip(`neighbor ${name}`, neighbor.address);
|
|
@@ -11452,6 +11580,7 @@ class Validator {
|
|
|
11452
11580
|
for (const policy of [neighbor.import, neighbor.export]) {
|
|
11453
11581
|
names.expect(`neighbor ${name} policy`, policy, isPolicy(policy));
|
|
11454
11582
|
}
|
|
11583
|
+
names.expect(`neighbor ${name} table`, neighbor.table, isTable(neighbor.table));
|
|
11455
11584
|
const local = neighbor.local_address ?? group?.local_address;
|
|
11456
11585
|
if (local && Address2.family(local) !== Address2.family(neighbor.address))
|
|
11457
11586
|
report(`neighbor ${name} local_address and address differ in family`);
|
|
@@ -11463,6 +11592,48 @@ class Validator {
|
|
|
11463
11592
|
names.expectInterface(`OSPF area ${areaId} interface`, iface);
|
|
11464
11593
|
}
|
|
11465
11594
|
}
|
|
11595
|
+
static checkTables(device, names, report, cidr) {
|
|
11596
|
+
const routing = device.routing;
|
|
11597
|
+
const tables = routing?.tables ?? {};
|
|
11598
|
+
const bgp = routing?.bgp;
|
|
11599
|
+
const installed = new Set;
|
|
11600
|
+
for (const route of routing?.static ?? [])
|
|
11601
|
+
if (route.table)
|
|
11602
|
+
installed.add(route.table);
|
|
11603
|
+
for (const neighbor of Object.values(bgp?.neighbors ?? {})) {
|
|
11604
|
+
const table = neighbor.table ?? (neighbor.group ? bgp?.groups?.[neighbor.group]?.table : undefined);
|
|
11605
|
+
if (table)
|
|
11606
|
+
installed.add(table);
|
|
11607
|
+
}
|
|
11608
|
+
for (const name of Object.keys(tables)) {
|
|
11609
|
+
if (name === "main")
|
|
11610
|
+
report("routing.tables has main, the main table every device has; give the table another name");
|
|
11611
|
+
if (name in (device.vrfs ?? {}))
|
|
11612
|
+
report(`routing.tables ${name} has the name of a VRF; give the table another name`);
|
|
11613
|
+
if (name !== "main" && !installed.has(name))
|
|
11614
|
+
report(`routing.tables ${name}: nothing installs routes in it; give a static route or a BGP neighbor table: "${name}", or remove the table`);
|
|
11615
|
+
}
|
|
11616
|
+
const rules = routing?.rules ?? [];
|
|
11617
|
+
rules.forEach((rule, index) => {
|
|
11618
|
+
const where = `routing.rules ${index + 1}${rule.description ? ` (${rule.description})` : ""}`;
|
|
11619
|
+
const match = rule.match ?? {};
|
|
11620
|
+
names.expect(`${where} table`, rule.table, rule.table === "main" || rule.table in tables);
|
|
11621
|
+
names.expectInterface(`${where} in_interface`, match.in_interface);
|
|
11622
|
+
if (match.src)
|
|
11623
|
+
cidr(where, match.src);
|
|
11624
|
+
if (match.dst)
|
|
11625
|
+
cidr(where, match.dst);
|
|
11626
|
+
if (match.src && match.dst && Address2.isCidr(match.src) && Address2.isCidr(match.dst)) {
|
|
11627
|
+
if (Address2.family(Address2.host(match.src)) !== Address2.family(Address2.host(match.dst)))
|
|
11628
|
+
report(`${where}: src and dst differ in family, so no packet matches both`);
|
|
11629
|
+
}
|
|
11630
|
+
if (rule.table === "main" && rule.fallback)
|
|
11631
|
+
report(`${where} looks up main and has fallback; fallback is for another table, so remove it`);
|
|
11632
|
+
const matchesAll = !match.src && !match.dst && !match.in_interface;
|
|
11633
|
+
if (matchesAll && index < rules.length - 1)
|
|
11634
|
+
report(`${where} matches every packet, so the rules after it never run; give it a match, or move it last`);
|
|
11635
|
+
});
|
|
11636
|
+
}
|
|
11466
11637
|
static checkPolicies(device, names, report, cidr) {
|
|
11467
11638
|
const policies = device.policies ?? {};
|
|
11468
11639
|
const sets = device.prefix_sets ?? {};
|
|
@@ -11865,14 +12036,10 @@ class Cli {
|
|
|
11865
12036
|
return new Cli(configPath, network, options);
|
|
11866
12037
|
}
|
|
11867
12038
|
static async load(path) {
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
11872
|
-
const { createJiti } = await import("jiti");
|
|
11873
|
-
network = await createJiti(import.meta.url).import(path, { default: true });
|
|
11874
|
-
}
|
|
11875
|
-
return { ...network, devices: Cabling.describeLinks(network.devices) };
|
|
12039
|
+
if (process.versions.bun)
|
|
12040
|
+
return (await import(path)).default;
|
|
12041
|
+
const { createJiti } = await import("jiti");
|
|
12042
|
+
return createJiti(import.meta.url).import(path, { default: true });
|
|
11876
12043
|
}
|
|
11877
12044
|
static fail(message) {
|
|
11878
12045
|
console.error(message);
|
|
@@ -11916,7 +12083,12 @@ class Cli {
|
|
|
11916
12083
|
program.command("snapshot").description("read what each device runs into the state directory").argument("[device...]", "every device when none is named").option("--console", "through the device's serial console on its jump host, instead of SSH").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (devices, flags) => (await Cli.open(flags.config, flags)).snapshot(devices));
|
|
11917
12084
|
program.command("apply").description("read the device, plan against what it runs, and send with --confirm").argument("<device>").option("--confirm", "send the plan; without it, nothing is sent").option("--secrets", "also send every secret, to rotate one").option("--rollback <minutes>", "minutes before a device that can restore itself does", Cli.minutes, 10).option("--console", "through the device's serial console on its jump host, instead of SSH").option("--force", "send even when the pings before --confirm find a path that loses too much or is too slow").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (device, flags) => (await Cli.open(flags.config, flags)).apply(device, flags.confirm ?? false, flags.force ?? false));
|
|
11918
12085
|
program.command("show").description("run commands that only read on a device and print what it answers, secrets hidden").argument("<device>").argument("[command...]", "each command in quotes, such as '/ip route print'").option("--file <path>", "also each line of this file; blank lines and lines starting with # are skipped").option("--timeout <seconds>", "how long each command may take", Cli.seconds, 90).option("--console", "through the device's serial console, instead of SSH").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (device, commands, flags) => (await Cli.open(flags.config, flags)).show(device, commands, flags.file, flags.timeout));
|
|
11919
|
-
program.command("bgp").description("each BGP neighbor in the config next to its live session: up or down and why, prefixes, uptime").argument("[device]", "may be left out when only one device has BGP neighbors").
|
|
12086
|
+
program.command("bgp").description("each BGP neighbor in the config next to its live session: up or down and why, prefixes, uptime").argument("[device]", "may be left out when only one device has BGP neighbors").allowExcessArguments().option("--neighbors <regex>", "only the neighbors whose name matches").option("--wait [count]", `read again every ${BgpWait.COMMAND.every} seconds until every neighbor shown, or this many, is up`, Cli.count).option("--max <seconds>", "how long --wait waits at most", Cli.seconds, BgpWait.COMMAND.seconds).option("--log [minutes]", "also print the device's BGP log lines from the last minutes, 5 by default", Cli.minutes).option("--clear-limit", "open again the neighbors shown that closed on their prefix limit; any other is left alone").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (device, flags, command) => {
|
|
12087
|
+
const [, filter] = command.args;
|
|
12088
|
+
if (filter !== undefined)
|
|
12089
|
+
Cli.fail(`circuit bgp takes one device; filter the neighbors with --neighbors: circuit bgp ${device} --neighbors '${filter}'`);
|
|
12090
|
+
(await Cli.open(flags.config)).bgp(device, flags);
|
|
12091
|
+
});
|
|
11920
12092
|
program.command("console").description("open a device's serial console here, logged in; or send it lines and print what it answers").argument("<device>").argument("[line...]", "lines to send, each in quotes; none joins the console to this terminal, Ctrl-] leaves").option("--raw", "do not log in or out: only join the console, or send the lines").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (device, lines, flags) => (await Cli.open(flags.config)).console(device, lines, flags.raw ?? false));
|
|
11921
12093
|
program.command("refresh").description("fetch prefix sets that come from a registry onto the device, with --confirm").argument("<device>").option("--confirm", "put them on the device; without it, nothing is sent").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (device, flags) => (await Cli.open(flags.config)).refresh(device, flags.confirm ?? false));
|
|
11922
12094
|
program.command("wireguard").description("a client config for one peer, its private key left for the peer to fill").argument("<device>").argument("<interface>").argument("<peer>").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (device, iface, peer, flags) => (await Cli.open(flags.config)).wireguard(device, iface, peer));
|
|
@@ -12138,8 +12310,9 @@ ${output.replace(/\r/g, "").trimEnd()}${stoppedAtLimit}
|
|
|
12138
12310
|
if (failed)
|
|
12139
12311
|
process.exitCode = 1;
|
|
12140
12312
|
}
|
|
12141
|
-
async bgp(
|
|
12142
|
-
const
|
|
12313
|
+
async bgp(deviceName, flags) {
|
|
12314
|
+
const device = this.bgpDevice(deviceName);
|
|
12315
|
+
const pattern = flags.neighbors;
|
|
12143
12316
|
const adapter = this.adapterOf(device);
|
|
12144
12317
|
const supported = Platforms.all().filter((platform) => platform.bgpSessions).map((platform) => platform.name).join(", ");
|
|
12145
12318
|
if (!adapter.bgpSessions)
|
|
@@ -12199,17 +12372,15 @@ no neighbor shown is closed on its prefix limit, so nothing was cleared`);
|
|
|
12199
12372
|
`);
|
|
12200
12373
|
Cli.printBgp(await read());
|
|
12201
12374
|
}
|
|
12202
|
-
|
|
12203
|
-
if (
|
|
12204
|
-
return
|
|
12205
|
-
}
|
|
12375
|
+
bgpDevice(name) {
|
|
12376
|
+
if (name !== undefined)
|
|
12377
|
+
return this.one(name);
|
|
12206
12378
|
const withBgp = this.network.devices.filter((device) => Object.keys(device.routing?.bgp?.neighbors ?? {}).length);
|
|
12207
|
-
const named = first ? `no device ${first}, and ` : "";
|
|
12208
12379
|
if (withBgp.length === 1)
|
|
12209
|
-
return
|
|
12380
|
+
return withBgp[0];
|
|
12210
12381
|
if (!withBgp.length)
|
|
12211
|
-
Cli.fail(
|
|
12212
|
-
return Cli.fail(
|
|
12382
|
+
Cli.fail("no device has BGP neighbors");
|
|
12383
|
+
return Cli.fail(`several devices have BGP neighbors, so name one: ${withBgp.map((device) => device.name).join(", ")}`);
|
|
12213
12384
|
}
|
|
12214
12385
|
static printBgp(sessions) {
|
|
12215
12386
|
const width = Math.max(...sessions.map((session) => session.neighbor.length));
|
|
@@ -12677,7 +12848,7 @@ Renamed. Run circuit validate, then circuit apply ${to}: the device keeps its ol
|
|
|
12677
12848
|
const port = Number(flags.port);
|
|
12678
12849
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
12679
12850
|
Cli.fail("--port takes a number from 1 to 65535");
|
|
12680
|
-
const { StudioServer } = await import("../chunks/server-
|
|
12851
|
+
const { StudioServer } = await import("../chunks/server-cxr9ynxc.js");
|
|
12681
12852
|
const host = flags.host;
|
|
12682
12853
|
const allowHosts = (flags.allowHost ?? "").split(",").filter(Boolean);
|
|
12683
12854
|
if (!StudioServer.isLoopback(host) && !allowHosts.length) {
|