@takodotid/circuit 0.40.0 → 0.42.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/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 +127 -49
- package/dist/helpers/index.js +143 -156
- 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/bgp.d.ts +53 -9
- package/dist/types/helpers/cables.d.ts +14 -11
- package/dist/types/helpers/checks.d.ts +4 -0
- package/dist/types/helpers/exchange.d.ts +7 -1
- package/dist/types/helpers/index.d.ts +2 -2
- package/dist/types/helpers/upstreams.d.ts +123 -181
- 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/routeros/render.d.ts +4 -0
- package/dist/types/schema/device.d.ts +4 -4
- package/dist/types/schema/firewall.d.ts +5 -1
- package/dist/types/schema/interfaces.d.ts +1 -1
- package/dist/types/schema/policy.d.ts +15 -6
- 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 +70 -2
- package/src/helpers/bgp.ts +117 -67
- package/src/helpers/cables.ts +20 -15
- package/src/helpers/checks.ts +23 -0
- package/src/helpers/exchange.ts +9 -3
- package/src/helpers/index.ts +2 -2
- package/src/helpers/upstreams.ts +162 -274
- 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/routeros/TRAPS.md +4 -0
- package/src/platforms/routeros/export.ts +1 -0
- package/src/platforms/routeros/index.ts +11 -2
- package/src/platforms/routeros/plan.ts +3 -0
- package/src/platforms/routeros/render.ts +28 -3
- package/src/schema/device.ts +4 -3
- package/src/schema/firewall.ts +5 -1
- package/src/schema/interfaces.ts +1 -1
- package/src/schema/policy.ts +17 -6
- 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
|
}
|
|
@@ -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",
|
|
@@ -8297,6 +8294,8 @@ class RouterOsRender {
|
|
|
8297
8294
|
return rules;
|
|
8298
8295
|
}
|
|
8299
8296
|
routing() {
|
|
8297
|
+
this.routingTables();
|
|
8298
|
+
this.routingRules();
|
|
8300
8299
|
this.staticRoutes();
|
|
8301
8300
|
this.rpki();
|
|
8302
8301
|
if (this.device.routing?.bgp) {
|
|
@@ -8307,6 +8306,24 @@ class RouterOsRender {
|
|
|
8307
8306
|
this.ospf();
|
|
8308
8307
|
this.bfd();
|
|
8309
8308
|
}
|
|
8309
|
+
routingTables() {
|
|
8310
|
+
for (const [name, table] of Object.entries(this.device.routing?.tables ?? {})) {
|
|
8311
|
+
this.add("/routing table", "add fib", { comment: table.description, name });
|
|
8312
|
+
}
|
|
8313
|
+
}
|
|
8314
|
+
routingRules() {
|
|
8315
|
+
for (const rule of this.device.routing?.rules ?? []) {
|
|
8316
|
+
const onlyInTable = rule.table !== "main" && (rule.fallback ?? "none") === "none";
|
|
8317
|
+
this.add("/routing rule", "add", {
|
|
8318
|
+
action: onlyInTable ? "lookup-only-in-table" : "lookup",
|
|
8319
|
+
comment: rule.description,
|
|
8320
|
+
"dst-address": rule.match?.dst,
|
|
8321
|
+
interface: rule.match?.in_interface && this.name(rule.match.in_interface),
|
|
8322
|
+
"src-address": rule.match?.src,
|
|
8323
|
+
table: rule.table
|
|
8324
|
+
});
|
|
8325
|
+
}
|
|
8326
|
+
}
|
|
8310
8327
|
staticRoutes() {
|
|
8311
8328
|
for (const route of this.device.routing?.static ?? []) {
|
|
8312
8329
|
const menu = Address2.family(Address2.host(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
|
|
@@ -8317,7 +8334,7 @@ class RouterOsRender {
|
|
|
8317
8334
|
distance: route.distance,
|
|
8318
8335
|
"dst-address": route.prefix,
|
|
8319
8336
|
gateway,
|
|
8320
|
-
"routing-table": route.vrf
|
|
8337
|
+
"routing-table": route.vrf ?? route.table
|
|
8321
8338
|
});
|
|
8322
8339
|
}
|
|
8323
8340
|
}
|
|
@@ -8438,6 +8455,7 @@ class RouterOsRender {
|
|
|
8438
8455
|
"output.network": originates ? RouterOsRender.NETWORKS[family] : undefined,
|
|
8439
8456
|
"remote.address": `${neighbor.address}/${addressFamily === "ipv4" ? 32 : 128}`,
|
|
8440
8457
|
"remote.as": settings.remote_as,
|
|
8458
|
+
"routing-table": settings.table,
|
|
8441
8459
|
"tcp-md5-key": settings.password && Secrets2.marker(settings.password),
|
|
8442
8460
|
"use-bfd": settings.bfd ? "yes" : undefined
|
|
8443
8461
|
});
|
|
@@ -8761,7 +8779,14 @@ class RouterOs extends Platform {
|
|
|
8761
8779
|
if (route.bfd)
|
|
8762
8780
|
found.push(`routing.static ${route.prefix} bfd, a static route's check-gateway takes arp, none or ping on 7.23`);
|
|
8763
8781
|
}
|
|
8764
|
-
found.push(...Derive.unproved(device, [
|
|
8782
|
+
found.push(...Derive.unproved(device, [
|
|
8783
|
+
"bfd-timers",
|
|
8784
|
+
"multihop-bfd",
|
|
8785
|
+
"static-route-bfd",
|
|
8786
|
+
"mac-limit",
|
|
8787
|
+
"ethertype-filter",
|
|
8788
|
+
"routing-tables"
|
|
8789
|
+
]));
|
|
8765
8790
|
return found;
|
|
8766
8791
|
}
|
|
8767
8792
|
render(device) {
|
|
@@ -9003,7 +9028,7 @@ ${certificates}` : "";
|
|
|
9003
9028
|
console.log(` BGP: all ${neighbors.length} established again`);
|
|
9004
9029
|
return [];
|
|
9005
9030
|
}
|
|
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}
|
|
9031
|
+
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`);
|
|
9007
9032
|
}
|
|
9008
9033
|
static async invalidSwitchRules(session, device) {
|
|
9009
9034
|
const answer = (await session.run(RouterOs.COUNT_INVALID_SWITCH_RULES)).trim();
|
|
@@ -11433,15 +11458,21 @@ class Validator {
|
|
|
11433
11458
|
report(`static route ${route.prefix} has bfd without via, the address BFD watches`);
|
|
11434
11459
|
names.expectInterface(`static route ${route.prefix} interface`, route.interface);
|
|
11435
11460
|
names.expect(`static route ${route.prefix} VRF`, route.vrf, route.vrf !== undefined && route.vrf in (device.vrfs ?? {}));
|
|
11461
|
+
names.expect(`static route ${route.prefix} table`, route.table, route.table !== undefined && route.table in (routing?.tables ?? {}));
|
|
11462
|
+
if (route.vrf && route.table)
|
|
11463
|
+
report(`static route ${route.prefix} has both vrf and table; a route is in one of them`);
|
|
11436
11464
|
}
|
|
11465
|
+
Validator.checkTables(device, names, report, cidr);
|
|
11437
11466
|
const bgp = routing?.bgp;
|
|
11438
11467
|
const isPolicy = (policy) => policy !== undefined && (policy in policies);
|
|
11439
11468
|
for (const prefix of bgp?.networks ?? [])
|
|
11440
11469
|
cidr("BGP network", prefix);
|
|
11470
|
+
const isTable = (table) => table !== undefined && (table in (routing?.tables ?? {}));
|
|
11441
11471
|
for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
|
|
11442
11472
|
for (const policy of [group.import, group.export]) {
|
|
11443
11473
|
names.expect(`BGP group ${name} policy`, policy, isPolicy(policy));
|
|
11444
11474
|
}
|
|
11475
|
+
names.expect(`BGP group ${name} table`, group.table, isTable(group.table));
|
|
11445
11476
|
}
|
|
11446
11477
|
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
11447
11478
|
ip(`neighbor ${name}`, neighbor.address);
|
|
@@ -11452,6 +11483,7 @@ class Validator {
|
|
|
11452
11483
|
for (const policy of [neighbor.import, neighbor.export]) {
|
|
11453
11484
|
names.expect(`neighbor ${name} policy`, policy, isPolicy(policy));
|
|
11454
11485
|
}
|
|
11486
|
+
names.expect(`neighbor ${name} table`, neighbor.table, isTable(neighbor.table));
|
|
11455
11487
|
const local = neighbor.local_address ?? group?.local_address;
|
|
11456
11488
|
if (local && Address2.family(local) !== Address2.family(neighbor.address))
|
|
11457
11489
|
report(`neighbor ${name} local_address and address differ in family`);
|
|
@@ -11463,17 +11495,63 @@ class Validator {
|
|
|
11463
11495
|
names.expectInterface(`OSPF area ${areaId} interface`, iface);
|
|
11464
11496
|
}
|
|
11465
11497
|
}
|
|
11498
|
+
static checkTables(device, names, report, cidr) {
|
|
11499
|
+
const routing = device.routing;
|
|
11500
|
+
const tables = routing?.tables ?? {};
|
|
11501
|
+
const bgp = routing?.bgp;
|
|
11502
|
+
const installed = new Set;
|
|
11503
|
+
for (const route of routing?.static ?? [])
|
|
11504
|
+
if (route.table)
|
|
11505
|
+
installed.add(route.table);
|
|
11506
|
+
for (const neighbor of Object.values(bgp?.neighbors ?? {})) {
|
|
11507
|
+
const table = neighbor.table ?? (neighbor.group ? bgp?.groups?.[neighbor.group]?.table : undefined);
|
|
11508
|
+
if (table)
|
|
11509
|
+
installed.add(table);
|
|
11510
|
+
}
|
|
11511
|
+
for (const name of Object.keys(tables)) {
|
|
11512
|
+
if (name === "main")
|
|
11513
|
+
report("routing.tables has main, the main table every device has; give the table another name");
|
|
11514
|
+
if (name in (device.vrfs ?? {}))
|
|
11515
|
+
report(`routing.tables ${name} has the name of a VRF; give the table another name`);
|
|
11516
|
+
if (name !== "main" && !installed.has(name))
|
|
11517
|
+
report(`routing.tables ${name}: nothing installs routes in it; give a static route or a BGP neighbor table: "${name}", or remove the table`);
|
|
11518
|
+
}
|
|
11519
|
+
const rules = routing?.rules ?? [];
|
|
11520
|
+
rules.forEach((rule, index) => {
|
|
11521
|
+
const where = `routing.rules ${index + 1}${rule.description ? ` (${rule.description})` : ""}`;
|
|
11522
|
+
const match = rule.match ?? {};
|
|
11523
|
+
names.expect(`${where} table`, rule.table, rule.table === "main" || rule.table in tables);
|
|
11524
|
+
names.expectInterface(`${where} in_interface`, match.in_interface);
|
|
11525
|
+
if (match.src)
|
|
11526
|
+
cidr(where, match.src);
|
|
11527
|
+
if (match.dst)
|
|
11528
|
+
cidr(where, match.dst);
|
|
11529
|
+
if (match.src && match.dst && Address2.isCidr(match.src) && Address2.isCidr(match.dst)) {
|
|
11530
|
+
if (Address2.family(Address2.host(match.src)) !== Address2.family(Address2.host(match.dst)))
|
|
11531
|
+
report(`${where}: src and dst differ in family, so no packet matches both`);
|
|
11532
|
+
}
|
|
11533
|
+
if (rule.table === "main" && rule.fallback)
|
|
11534
|
+
report(`${where} looks up main and has fallback; fallback is for another table, so remove it`);
|
|
11535
|
+
const matchesAll = !match.src && !match.dst && !match.in_interface;
|
|
11536
|
+
if (matchesAll && index < rules.length - 1)
|
|
11537
|
+
report(`${where} matches every packet, so the rules after it never run; give it a match, or move it last`);
|
|
11538
|
+
});
|
|
11539
|
+
}
|
|
11466
11540
|
static checkPolicies(device, names, report, cidr) {
|
|
11467
11541
|
const policies = device.policies ?? {};
|
|
11468
11542
|
const sets = device.prefix_sets ?? {};
|
|
11469
11543
|
for (const [name, rules] of Object.entries(policies)) {
|
|
11470
|
-
|
|
11544
|
+
rules.forEach((rule, index) => {
|
|
11545
|
+
const beside = ["match", "set", "action"].filter((key) => rule.call !== undefined && rule[key] !== undefined);
|
|
11546
|
+
if (beside.length) {
|
|
11547
|
+
report(`policy ${name} rule ${index + 1} has call and ${beside.join(", ")}: a call is a rule of its own, run for every route. Put ${beside.join(", ")} in the next rule, or the match inside ${rule.call}`);
|
|
11548
|
+
}
|
|
11471
11549
|
names.expect(`policy ${name} call`, rule.call, rule.call !== undefined && rule.call in policies);
|
|
11472
11550
|
const set = rule.match?.prefix_set;
|
|
11473
11551
|
names.expect(`policy ${name} prefix set`, set, set !== undefined && set in sets);
|
|
11474
11552
|
if (rule.match?.prefix)
|
|
11475
11553
|
cidr(`policy ${name}`, rule.match.prefix);
|
|
11476
|
-
}
|
|
11554
|
+
});
|
|
11477
11555
|
}
|
|
11478
11556
|
const visit = (policy, path) => {
|
|
11479
11557
|
if (path.includes(policy))
|
|
@@ -11861,14 +11939,10 @@ class Cli {
|
|
|
11861
11939
|
return new Cli(configPath, network, options);
|
|
11862
11940
|
}
|
|
11863
11941
|
static async load(path) {
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
const { createJiti } = await import("jiti");
|
|
11869
|
-
network = await createJiti(import.meta.url).import(path, { default: true });
|
|
11870
|
-
}
|
|
11871
|
-
return { ...network, devices: Cabling.describeLinks(network.devices) };
|
|
11942
|
+
if (process.versions.bun)
|
|
11943
|
+
return (await import(path)).default;
|
|
11944
|
+
const { createJiti } = await import("jiti");
|
|
11945
|
+
return createJiti(import.meta.url).import(path, { default: true });
|
|
11872
11946
|
}
|
|
11873
11947
|
static fail(message) {
|
|
11874
11948
|
console.error(message);
|
|
@@ -11912,7 +11986,12 @@ class Cli {
|
|
|
11912
11986
|
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));
|
|
11913
11987
|
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));
|
|
11914
11988
|
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));
|
|
11915
|
-
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").
|
|
11989
|
+
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) => {
|
|
11990
|
+
const [, filter] = command.args;
|
|
11991
|
+
if (filter !== undefined)
|
|
11992
|
+
Cli.fail(`circuit bgp takes one device; filter the neighbors with --neighbors: circuit bgp ${device} --neighbors '${filter}'`);
|
|
11993
|
+
(await Cli.open(flags.config)).bgp(device, flags);
|
|
11994
|
+
});
|
|
11916
11995
|
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));
|
|
11917
11996
|
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));
|
|
11918
11997
|
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));
|
|
@@ -12134,8 +12213,9 @@ ${output.replace(/\r/g, "").trimEnd()}${stoppedAtLimit}
|
|
|
12134
12213
|
if (failed)
|
|
12135
12214
|
process.exitCode = 1;
|
|
12136
12215
|
}
|
|
12137
|
-
async bgp(
|
|
12138
|
-
const
|
|
12216
|
+
async bgp(deviceName, flags) {
|
|
12217
|
+
const device = this.bgpDevice(deviceName);
|
|
12218
|
+
const pattern = flags.neighbors;
|
|
12139
12219
|
const adapter = this.adapterOf(device);
|
|
12140
12220
|
const supported = Platforms.all().filter((platform) => platform.bgpSessions).map((platform) => platform.name).join(", ");
|
|
12141
12221
|
if (!adapter.bgpSessions)
|
|
@@ -12195,17 +12275,15 @@ no neighbor shown is closed on its prefix limit, so nothing was cleared`);
|
|
|
12195
12275
|
`);
|
|
12196
12276
|
Cli.printBgp(await read());
|
|
12197
12277
|
}
|
|
12198
|
-
|
|
12199
|
-
if (
|
|
12200
|
-
return
|
|
12201
|
-
}
|
|
12278
|
+
bgpDevice(name) {
|
|
12279
|
+
if (name !== undefined)
|
|
12280
|
+
return this.one(name);
|
|
12202
12281
|
const withBgp = this.network.devices.filter((device) => Object.keys(device.routing?.bgp?.neighbors ?? {}).length);
|
|
12203
|
-
const named = first ? `no device ${first}, and ` : "";
|
|
12204
12282
|
if (withBgp.length === 1)
|
|
12205
|
-
return
|
|
12283
|
+
return withBgp[0];
|
|
12206
12284
|
if (!withBgp.length)
|
|
12207
|
-
Cli.fail(
|
|
12208
|
-
return Cli.fail(
|
|
12285
|
+
Cli.fail("no device has BGP neighbors");
|
|
12286
|
+
return Cli.fail(`several devices have BGP neighbors, so name one: ${withBgp.map((device) => device.name).join(", ")}`);
|
|
12209
12287
|
}
|
|
12210
12288
|
static printBgp(sessions) {
|
|
12211
12289
|
const width = Math.max(...sessions.map((session) => session.neighbor.length));
|
|
@@ -12673,7 +12751,7 @@ Renamed. Run circuit validate, then circuit apply ${to}: the device keeps its ol
|
|
|
12673
12751
|
const port = Number(flags.port);
|
|
12674
12752
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
12675
12753
|
Cli.fail("--port takes a number from 1 to 65535");
|
|
12676
|
-
const { StudioServer } = await import("../chunks/server-
|
|
12754
|
+
const { StudioServer } = await import("../chunks/server-cxr9ynxc.js");
|
|
12677
12755
|
const host = flags.host;
|
|
12678
12756
|
const allowHosts = (flags.allowHost ?? "").split(",").filter(Boolean);
|
|
12679
12757
|
if (!StudioServer.isLoopback(host) && !allowHosts.length) {
|