@takodotid/circuit 0.36.0 → 0.38.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-2r4bbhac.js → index-yzw86rc8.js} +9 -65
- package/dist/chunks/{server-gzsf5me5.js → server-cxr9ynxc.js} +1 -1
- package/dist/cli/index.js +110 -13
- package/dist/helpers/index.js +227 -1
- package/dist/types/core/address-space.d.ts +1 -1
- package/dist/types/core/address.d.ts +1 -1
- package/dist/types/core/cabling.d.ts +3 -3
- package/dist/types/core/cross-checks.d.ts +3 -3
- package/dist/types/core/define.d.ts +2 -2
- package/dist/types/core/secrets.d.ts +2 -2
- package/dist/types/core/topology.d.ts +2 -2
- package/dist/types/core/validator.d.ts +1 -1
- package/dist/types/helpers/bgp.d.ts +48 -5
- package/dist/types/helpers/cables.d.ts +70 -0
- package/dist/types/helpers/checks.d.ts +1 -1
- package/dist/types/helpers/exchange.d.ts +3 -3
- package/dist/types/helpers/filters.d.ts +1 -1
- package/dist/types/helpers/index.d.ts +14 -10
- package/dist/types/helpers/route-server.d.ts +2 -2
- package/dist/types/helpers/upstreams.d.ts +156 -0
- package/dist/types/index.d.ts +8 -6
- package/dist/types/integrations/librenms.d.ts +2 -2
- package/dist/types/integrations/netbox.d.ts +1 -1
- package/dist/types/integrations/peeringdb.d.ts +1 -1
- package/dist/types/integrations/prometheus.d.ts +2 -2
- package/dist/types/integrations/registries.d.ts +2 -2
- package/dist/types/platforms/bgp-wait.d.ts +7 -1
- package/dist/types/platforms/derive.d.ts +1 -1
- package/dist/types/platforms/fortios/firewall.d.ts +2 -2
- package/dist/types/platforms/fortios/index.d.ts +4 -4
- package/dist/types/platforms/fortios/names.d.ts +1 -1
- package/dist/types/platforms/fortios/plan.d.ts +1 -1
- package/dist/types/platforms/fortios/render.d.ts +1 -1
- package/dist/types/platforms/fortios/routing.d.ts +2 -2
- package/dist/types/platforms/index.d.ts +1 -1
- package/dist/types/platforms/junos/bgp.d.ts +2 -2
- package/dist/types/platforms/junos/index.d.ts +4 -4
- package/dist/types/platforms/junos/render.d.ts +1 -1
- package/dist/types/platforms/line-diff.d.ts +1 -1
- package/dist/types/platforms/platform.d.ts +17 -5
- package/dist/types/platforms/raisecom-ros/index.d.ts +4 -4
- package/dist/types/platforms/raisecom-ros/render.d.ts +1 -1
- package/dist/types/platforms/routeros/index.d.ts +12 -5
- package/dist/types/platforms/routeros/plan.d.ts +1 -1
- package/dist/types/platforms/routeros/render.d.ts +1 -1
- package/dist/types/platforms/vrp/index.d.ts +4 -4
- package/dist/types/platforms/vrp/render.d.ts +1 -1
- package/dist/types/schema/device.d.ts +6 -6
- package/dist/types/schema/firewall.d.ts +1 -1
- package/dist/types/schema/index.d.ts +8 -8
- package/dist/types/schema/interfaces.d.ts +5 -1
- package/dist/types/schema/policy.d.ts +1 -1
- package/dist/types/schema/routing.d.ts +1 -1
- package/dist/types/schema/system.d.ts +2 -2
- package/dist/types/transport/console.d.ts +1 -1
- package/dist/types/transport/ssh.d.ts +1 -1
- package/package.json +2 -2
- package/src/cli/index.ts +55 -4
- package/src/core/cabling.ts +4 -1
- package/src/core/cross-checks.ts +6 -1
- package/src/core/topology.ts +4 -3
- package/src/helpers/bgp.ts +104 -2
- package/src/helpers/cables.ts +91 -0
- package/src/helpers/index.ts +6 -2
- package/src/helpers/upstreams.ts +306 -0
- package/src/index.ts +3 -0
- package/src/platforms/bgp-wait.ts +24 -1
- package/src/platforms/platform.ts +9 -0
- package/src/platforms/routeros/TRAPS.md +1 -0
- package/src/platforms/routeros/index.ts +57 -10
- package/src/schema/interfaces.ts +4 -0
- package/src/studio/data.ts +3 -0
- package/src/studio/public/tables.js +15 -1
|
@@ -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;
|
|
@@ -160,7 +102,8 @@ class Topology {
|
|
|
160
102
|
const farPort = far?.ports?.[link.port];
|
|
161
103
|
const linksBack = farPort?.link?.device === device.name && farPort.link.port === portName;
|
|
162
104
|
const to = far ? Topology.deviceId(far.name) : graph.node(`missing:${link.device}`, "outside", link.device, ["named by a link, and not in the network"]);
|
|
163
|
-
const
|
|
105
|
+
const through = link.through ? `, through ${link.through}` : "";
|
|
106
|
+
const cableLine = `${here} to ${there}${through}`;
|
|
164
107
|
const lag = port.lag;
|
|
165
108
|
const farLag = farPort?.lag;
|
|
166
109
|
const bundleKey = `${device.name} ${lag}|${link.device} ${farLag}`;
|
|
@@ -168,7 +111,7 @@ class Topology {
|
|
|
168
111
|
if (bundle) {
|
|
169
112
|
bundle.members++;
|
|
170
113
|
bundle.cable.detail.push(cableLine);
|
|
171
|
-
bundle.cable.label = `${lag} to ${farLag}, ${bundle.members} cables`;
|
|
114
|
+
bundle.cable.label = `${lag} to ${farLag}, ${bundle.members} cables${through}`;
|
|
172
115
|
continue;
|
|
173
116
|
}
|
|
174
117
|
const isBundle = Boolean(lag && farLag && linksBack);
|
|
@@ -176,7 +119,7 @@ class Topology {
|
|
|
176
119
|
layer: "l1",
|
|
177
120
|
from: Topology.deviceId(device.name),
|
|
178
121
|
to,
|
|
179
|
-
label: isBundle ? `${lag} to ${farLag}` : `${portName} to ${link.port}`,
|
|
122
|
+
label: `${isBundle ? `${lag} to ${farLag}` : `${portName} to ${link.port}`}${through}`,
|
|
180
123
|
detail: [cableLine],
|
|
181
124
|
...linksBack ? {} : { oneSided: true }
|
|
182
125
|
});
|
|
@@ -950,6 +893,7 @@ class StudioModel {
|
|
|
950
893
|
...port.lag ? { lag: port.lag } : {},
|
|
951
894
|
...farPort?.lag ? { farLag: farPort.lag } : {},
|
|
952
895
|
...port.description ? { description: port.description } : {},
|
|
896
|
+
...link.through ? { through: link.through } : {},
|
|
953
897
|
vlans: vlansOf(device, portName),
|
|
954
898
|
...linksBack ? {} : { oneSided: true }
|
|
955
899
|
});
|
|
@@ -974,4 +918,4 @@ class StudioModel {
|
|
|
974
918
|
}
|
|
975
919
|
}
|
|
976
920
|
|
|
977
|
-
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";
|
|
@@ -3732,6 +3734,8 @@ class Cabling {
|
|
|
3732
3734
|
}
|
|
3733
3735
|
continue;
|
|
3734
3736
|
}
|
|
3737
|
+
if (link.through)
|
|
3738
|
+
continue;
|
|
3735
3739
|
const far = devices.find((candidate) => candidate.name === link.device);
|
|
3736
3740
|
if (!far || !Cabling.runsOn(far, link.port))
|
|
3737
3741
|
continue;
|
|
@@ -6594,6 +6598,20 @@ class BgpWait {
|
|
|
6594
6598
|
static upCount(sessions) {
|
|
6595
6599
|
return sessions.filter((session) => session.established).length;
|
|
6596
6600
|
}
|
|
6601
|
+
static BURST_MINUTES = 5;
|
|
6602
|
+
static limitBursts(sessions) {
|
|
6603
|
+
const closed = sessions.filter((session) => session.limit?.stoppedAt !== undefined && !Number.isNaN(session.limit.stoppedAt)).sort((a, b) => a.limit.stoppedAt - b.limit.stoppedAt);
|
|
6604
|
+
const bursts = [];
|
|
6605
|
+
for (const session of closed) {
|
|
6606
|
+
const last = bursts.at(-1);
|
|
6607
|
+
const previous = last?.at(-1);
|
|
6608
|
+
if (last && previous && session.limit.stoppedAt - previous.limit.stoppedAt <= BgpWait.BURST_MINUTES * 60000)
|
|
6609
|
+
last.push(session);
|
|
6610
|
+
else
|
|
6611
|
+
bursts.push([session]);
|
|
6612
|
+
}
|
|
6613
|
+
return bursts.filter((burst) => burst.length > 1);
|
|
6614
|
+
}
|
|
6597
6615
|
}
|
|
6598
6616
|
|
|
6599
6617
|
// src/platforms/routeros/export.ts
|
|
@@ -8379,9 +8397,15 @@ class RouterOs extends Platform {
|
|
|
8379
8397
|
static LIST_BGP_SESSIONS = [
|
|
8380
8398
|
":foreach s in=[/routing bgp session find] do={",
|
|
8381
8399
|
":local g [/routing bgp session get $s];",
|
|
8382
|
-
':put (($g->"name") . "|" . ($g->"remote.address") . "|" . ($g->"established") . "|" . ($g->"prefix-count") . "|" . ($g->"uptime"))',
|
|
8400
|
+
':put (($g->"name") . "|" . ($g->"remote.address") . "|" . ($g->"established") . "|" . ($g->"prefix-count") . "|" . ($g->"uptime") . "|" . ($g->"limit-exceeded") . "|" . ($g->"input.limit-process-routes") . "|" . ($g->"last-stopped"))',
|
|
8401
|
+
"};",
|
|
8402
|
+
":foreach c in=[/routing bgp connection find disabled] do={",
|
|
8403
|
+
':put ("disabled|" . [/routing bgp connection get $c name] . "|" . [/routing bgp connection get $c remote.address])',
|
|
8383
8404
|
"}"
|
|
8384
8405
|
].join(" ");
|
|
8406
|
+
static clearLimit(address) {
|
|
8407
|
+
return `/routing bgp session clear [find remote.address=${address} and limit-exceeded] flag=limit-exceeded`;
|
|
8408
|
+
}
|
|
8385
8409
|
static LIST_NEIGHBORS = [
|
|
8386
8410
|
':foreach n in=[/ip neighbor find where discovered-by~"lldp"] do={',
|
|
8387
8411
|
":local g [/ip neighbor get $n];",
|
|
@@ -8576,21 +8600,48 @@ ${certificates}` : "";
|
|
|
8576
8600
|
}
|
|
8577
8601
|
async bgpSessions(device, session) {
|
|
8578
8602
|
const output = await session.run(RouterOs.LIST_BGP_SESSIONS, 60000);
|
|
8579
|
-
const
|
|
8603
|
+
const lines = output.split(/\r?\n/).filter((line) => line.includes("|")).map((line) => line.trim().split("|"));
|
|
8604
|
+
const live = lines.filter(([first]) => first !== "disabled");
|
|
8605
|
+
const disabled = lines.filter(([first]) => first === "disabled").map(([, name, address]) => ({ name, address: address?.split("/")[0] }));
|
|
8580
8606
|
return Object.entries(device.routing?.bgp?.neighbors ?? {}).map(([name, neighbor]) => {
|
|
8581
8607
|
const found = live.find(([, remote]) => remote === neighbor.address) ?? live.find(([sessionName]) => sessionName === name || sessionName?.replace(/-\d+$/, "") === name);
|
|
8582
|
-
if (!found)
|
|
8583
|
-
|
|
8584
|
-
|
|
8608
|
+
if (!found) {
|
|
8609
|
+
const off = disabled.some((connection) => connection.address === neighbor.address || connection.name === name);
|
|
8610
|
+
return {
|
|
8611
|
+
neighbor: name,
|
|
8612
|
+
remote: "-",
|
|
8613
|
+
established: false,
|
|
8614
|
+
prefixes: "-",
|
|
8615
|
+
uptime: "no session",
|
|
8616
|
+
...off ? { reason: "admin down" } : {}
|
|
8617
|
+
};
|
|
8618
|
+
}
|
|
8619
|
+
const [, remote, established, prefixes, uptime, limitExceeded, max, stopped] = found;
|
|
8620
|
+
const up = established === "true";
|
|
8621
|
+
const limited = limitExceeded === "true";
|
|
8585
8622
|
return {
|
|
8586
8623
|
neighbor: name,
|
|
8587
8624
|
remote: remote || "-",
|
|
8588
|
-
established:
|
|
8625
|
+
established: up,
|
|
8589
8626
|
prefixes: prefixes || "-",
|
|
8590
|
-
uptime: uptime || "-"
|
|
8627
|
+
uptime: uptime || "-",
|
|
8628
|
+
...up ? {} : { reason: limited ? "limit exceeded" : "connecting" },
|
|
8629
|
+
...limited ? {
|
|
8630
|
+
limit: {
|
|
8631
|
+
...max && /^\d+$/.test(max) ? { max: Number(max) } : {},
|
|
8632
|
+
...stopped ? { stopped, stoppedAt: RouterOs.logTime(stopped) } : {}
|
|
8633
|
+
}
|
|
8634
|
+
} : {}
|
|
8591
8635
|
};
|
|
8592
8636
|
});
|
|
8593
8637
|
}
|
|
8638
|
+
async bgpClearLimit(session, sessions) {
|
|
8639
|
+
for (const bgp of sessions) {
|
|
8640
|
+
const answer = (await session.run(RouterOs.clearLimit(bgp.remote))).trim();
|
|
8641
|
+
if (answer)
|
|
8642
|
+
throw new Error(`could not clear ${bgp.neighbor}: ${answer.slice(0, 200)}`);
|
|
8643
|
+
}
|
|
8644
|
+
}
|
|
8594
8645
|
async bgpLog(session, minutes) {
|
|
8595
8646
|
const now = (await session.run(RouterOs.CLOCK)).trim();
|
|
8596
8647
|
const nowTime = RouterOs.logTime(now);
|
|
@@ -8686,7 +8737,7 @@ ${certificates}` : "";
|
|
|
8686
8737
|
console.log(` BGP: all ${neighbors.length} established again`);
|
|
8687
8738
|
return [];
|
|
8688
8739
|
}
|
|
8689
|
-
return down.map((bgp) => `BGP session ${bgp.neighbor} was established before the apply and is not ${limits.seconds} s after it (${bgp.uptime === "no session" ? "no session" : "not established"}). Look with: circuit bgp ${device.name} --log`);
|
|
8740
|
+
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} ${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`);
|
|
8690
8741
|
}
|
|
8691
8742
|
static async invalidSwitchRules(session, device) {
|
|
8692
8743
|
const answer = (await session.run(RouterOs.COUNT_INVALID_SWITCH_RULES)).trim();
|
|
@@ -10694,6 +10745,10 @@ class CrossChecks {
|
|
|
10694
10745
|
report(`${link.device} ${link.port} does not link back to it`);
|
|
10695
10746
|
continue;
|
|
10696
10747
|
}
|
|
10748
|
+
if (link.through !== farPort.link?.through) {
|
|
10749
|
+
const say = (through) => through ? `through ${through}` : "direct";
|
|
10750
|
+
report(`${say(link.through)} here, and ${say(farPort.link?.through)} on ${link.device} ${link.port}`);
|
|
10751
|
+
}
|
|
10697
10752
|
const near = CrossChecks.carrierOf(device, portName, port);
|
|
10698
10753
|
const away = CrossChecks.carrierOf(far, link.port, farPort);
|
|
10699
10754
|
const inLag = near.name !== portName;
|
|
@@ -11561,7 +11616,7 @@ class Cli {
|
|
|
11561
11616
|
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));
|
|
11562
11617
|
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));
|
|
11563
11618
|
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));
|
|
11564
|
-
program.command("bgp").description("each BGP neighbor in the config next to its live session: up or down, prefixes, uptime").argument("[device]", "may be left out when only one device has BGP neighbors").argument("[name-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(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (first, second, flags) => (await Cli.open(flags.config)).bgp(first, second, flags));
|
|
11619
|
+
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").argument("[name-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 (first, second, flags) => (await Cli.open(flags.config)).bgp(first, second, flags));
|
|
11565
11620
|
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));
|
|
11566
11621
|
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));
|
|
11567
11622
|
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));
|
|
@@ -11791,6 +11846,10 @@ ${output.replace(/\r/g, "").trimEnd()}${stoppedAtLimit}
|
|
|
11791
11846
|
Cli.fail(`circuit bgp does not support ${device.platform} yet, only ${supported}`);
|
|
11792
11847
|
if (flags.log && !adapter.bgpLog)
|
|
11793
11848
|
Cli.fail(`circuit bgp --log does not support ${device.platform} yet`);
|
|
11849
|
+
if (flags.clearLimit && !adapter.bgpClearLimit)
|
|
11850
|
+
Cli.fail(`circuit bgp --clear-limit does not support ${device.platform} yet`);
|
|
11851
|
+
if (flags.clearLimit && flags.wait !== undefined)
|
|
11852
|
+
Cli.fail("--clear-limit and --wait go in two runs: clear first, then wait");
|
|
11794
11853
|
let matcher;
|
|
11795
11854
|
try {
|
|
11796
11855
|
matcher = new RegExp(pattern ?? ".");
|
|
@@ -11807,6 +11866,10 @@ ${output.replace(/\r/g, "").trimEnd()}${stoppedAtLimit}
|
|
|
11807
11866
|
const session = await adapter.session(device, this.optionsFor(device));
|
|
11808
11867
|
try {
|
|
11809
11868
|
const read = async () => (await adapter.bgpSessions(device, session)).filter((neighbor) => shown.includes(neighbor.neighbor));
|
|
11869
|
+
if (flags.clearLimit) {
|
|
11870
|
+
await Cli.clearLimits(adapter, session, read);
|
|
11871
|
+
return;
|
|
11872
|
+
}
|
|
11810
11873
|
const limits = { seconds: flags.max, every: BgpWait.COMMAND.every };
|
|
11811
11874
|
const sessions = await BgpWait.until(read, (sessions) => BgpWait.upCount(sessions) >= want, limits, (sessions) => console.log(`${new Date().toLocaleTimeString()}: ${BgpWait.upCount(sessions)}/${want} up, checking again in ${limits.every}s`));
|
|
11812
11875
|
Cli.printBgp(sessions);
|
|
@@ -11818,6 +11881,24 @@ ${output.replace(/\r/g, "").trimEnd()}${stoppedAtLimit}
|
|
|
11818
11881
|
session.close();
|
|
11819
11882
|
}
|
|
11820
11883
|
}
|
|
11884
|
+
static async clearLimits(adapter, session, read) {
|
|
11885
|
+
const sessions = await read();
|
|
11886
|
+
const closed = sessions.filter((bgp) => bgp.limit);
|
|
11887
|
+
if (!closed.length) {
|
|
11888
|
+
Cli.printBgp(sessions);
|
|
11889
|
+
console.error(`
|
|
11890
|
+
no neighbor shown is closed on its prefix limit, so nothing was cleared`);
|
|
11891
|
+
process.exitCode = 1;
|
|
11892
|
+
return;
|
|
11893
|
+
}
|
|
11894
|
+
for (const bgp of sessions)
|
|
11895
|
+
if (!bgp.limit)
|
|
11896
|
+
console.log(`${bgp.neighbor}: left alone, not closed on its limit`);
|
|
11897
|
+
await adapter.bgpClearLimit(session, closed);
|
|
11898
|
+
console.log(`cleared ${closed.map((bgp) => bgp.neighbor).join(", ")}: each connects again, and closes again if it still sends too much
|
|
11899
|
+
`);
|
|
11900
|
+
Cli.printBgp(await read());
|
|
11901
|
+
}
|
|
11821
11902
|
bgpTarget(first, second) {
|
|
11822
11903
|
if (second !== undefined || first && this.network.devices.some((device) => device.name === first)) {
|
|
11823
11904
|
return { device: this.one(first), pattern: second };
|
|
@@ -11834,9 +11915,25 @@ ${output.replace(/\r/g, "").trimEnd()}${stoppedAtLimit}
|
|
|
11834
11915
|
const width = Math.max(...sessions.map((session) => session.neighbor.length));
|
|
11835
11916
|
for (const session of sessions) {
|
|
11836
11917
|
const state = session.established ? "up " : "DOWN";
|
|
11837
|
-
|
|
11918
|
+
const why = session.reason ? ` ${Cli.downReason(session)}` : "";
|
|
11919
|
+
console.log(`${session.neighbor.padEnd(width)} ${state} ${session.prefixes.padStart(7)} ${session.uptime.padEnd(12)} ${session.remote}${why}`);
|
|
11838
11920
|
}
|
|
11839
11921
|
console.log(`${BgpWait.upCount(sessions)}/${sessions.length} up`);
|
|
11922
|
+
for (const burst of BgpWait.limitBursts(sessions)) {
|
|
11923
|
+
const names = burst.map((session) => session.neighbor);
|
|
11924
|
+
const listed = names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names.at(-1)}` : names[0];
|
|
11925
|
+
const seconds = Math.round((burst.at(-1).limit.stoppedAt - burst[0].limit.stoppedAt) / 1000);
|
|
11926
|
+
console.log(`
|
|
11927
|
+
${listed} closed on their prefix limit within ${seconds} s of each other, from ${burst[0].limit.stopped}. Several limits at once is usually a leak upstream, not growth: find the leak before raising a limit, then clear them with --clear-limit.`);
|
|
11928
|
+
}
|
|
11929
|
+
}
|
|
11930
|
+
static downReason(session) {
|
|
11931
|
+
const { limit } = session;
|
|
11932
|
+
if (!limit)
|
|
11933
|
+
return session.reason ?? "";
|
|
11934
|
+
const over = limit.max !== undefined ? `: over ${limit.max} prefixes` : "";
|
|
11935
|
+
const when = limit.stopped ? `, closed ${limit.stopped}` : "";
|
|
11936
|
+
return `${session.reason}${over}${when}`;
|
|
11840
11937
|
}
|
|
11841
11938
|
async printBgpLog(adapter, session, minutes) {
|
|
11842
11939
|
const lines = Cli.foldLog(await adapter.bgpLog(session, minutes));
|
|
@@ -12280,7 +12377,7 @@ Renamed. Run circuit validate, then circuit apply ${to}: the device keeps its ol
|
|
|
12280
12377
|
const port = Number(flags.port);
|
|
12281
12378
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
12282
12379
|
Cli.fail("--port takes a number from 1 to 65535");
|
|
12283
|
-
const { StudioServer } = await import("../chunks/server-
|
|
12380
|
+
const { StudioServer } = await import("../chunks/server-cxr9ynxc.js");
|
|
12284
12381
|
const host = flags.host;
|
|
12285
12382
|
const allowHosts = (flags.allowHost ?? "").split(",").filter(Boolean);
|
|
12286
12383
|
if (!StudioServer.isLoopback(host) && !allowHosts.length) {
|
package/dist/helpers/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Catalog
|
|
3
|
+
} from "../chunks/index-6wb41q67.js";
|
|
1
4
|
import {
|
|
2
5
|
Address2
|
|
3
6
|
} from "../chunks/index-n7w3tg5y.js";
|
|
@@ -9,6 +12,10 @@ class Bgp {
|
|
|
9
12
|
["twice", 2],
|
|
10
13
|
["three_times", 3]
|
|
11
14
|
];
|
|
15
|
+
static DEFAULTS = [
|
|
16
|
+
{ family: "ipv4", prefix: "0.0.0.0/0" },
|
|
17
|
+
{ family: "ipv6", prefix: "::/0" }
|
|
18
|
+
];
|
|
12
19
|
static sanity(lengths = {}) {
|
|
13
20
|
const ipv4 = lengths.ipv4 ?? { min: 8, max: 24 };
|
|
14
21
|
const ipv6 = lengths.ipv6 ?? { min: 16, max: 48 };
|
|
@@ -36,6 +43,49 @@ class Bgp {
|
|
|
36
43
|
{ description: "RPKI invalid", match: { rpki: "invalid" }, action: "reject" }
|
|
37
44
|
];
|
|
38
45
|
}
|
|
46
|
+
static announce(prefixes) {
|
|
47
|
+
return [
|
|
48
|
+
...prefixes.map(({ prefix, description }) => ({
|
|
49
|
+
...description ? { description } : {},
|
|
50
|
+
match: { prefix },
|
|
51
|
+
action: "accept"
|
|
52
|
+
})),
|
|
53
|
+
{ description: "Nothing else", action: "reject" }
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
static learn(options = {}) {
|
|
57
|
+
const { tag } = options;
|
|
58
|
+
return [
|
|
59
|
+
...options.before ?? [],
|
|
60
|
+
...options.sanity ? [{ call: options.sanity }] : Bgp.sanity(),
|
|
61
|
+
...(options.sources ?? []).map((source) => ({ set: { preferred_source: source } })),
|
|
62
|
+
...options.local_pref !== undefined ? [{ set: { local_pref: options.local_pref } }] : [],
|
|
63
|
+
...tag === undefined ? [] : [typeof tag === "object" ? tag : Bgp.adding(tag)],
|
|
64
|
+
{ action: "accept" }
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
static defaultOnly(options = {}) {
|
|
68
|
+
return Bgp.DEFAULTS.map(({ family, prefix }) => {
|
|
69
|
+
const source = options.sources?.find((address) => Address2.family(address) === family);
|
|
70
|
+
const set = {
|
|
71
|
+
...options.local_pref !== undefined ? { local_pref: options.local_pref } : {},
|
|
72
|
+
...source ? { preferred_source: source } : {}
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
description: options.description ?? `Default route, ${family}`,
|
|
76
|
+
match: { prefix },
|
|
77
|
+
...Object.keys(set).length ? { set } : {},
|
|
78
|
+
action: "accept"
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
static adding(community) {
|
|
83
|
+
const large = community.split(":").length === 3;
|
|
84
|
+
return {
|
|
85
|
+
description: `Tag ${community}`,
|
|
86
|
+
set: large ? { add_large_communities: [community] } : { add_communities: [community] }
|
|
87
|
+
};
|
|
88
|
+
}
|
|
39
89
|
static neighborPolicies(device) {
|
|
40
90
|
const bgp = device.routing?.bgp;
|
|
41
91
|
return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
|
|
@@ -181,6 +231,38 @@ class Bgp {
|
|
|
181
231
|
return /^[aeiou]/i.test(word) ? `an ${word}` : `a ${word}`;
|
|
182
232
|
}
|
|
183
233
|
}
|
|
234
|
+
// src/helpers/cables.ts
|
|
235
|
+
class Cables {
|
|
236
|
+
static link(spec) {
|
|
237
|
+
return {
|
|
238
|
+
a: { [spec.a.port]: Cables.end(spec, spec.b, spec.b.port) },
|
|
239
|
+
b: { [spec.b.port]: Cables.end(spec, spec.a, spec.a.port) }
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
static lag(spec) {
|
|
243
|
+
const pairs = Object.entries(spec.cables);
|
|
244
|
+
const taken = new Map;
|
|
245
|
+
for (const [near, far] of pairs) {
|
|
246
|
+
const other = taken.get(far);
|
|
247
|
+
if (other)
|
|
248
|
+
throw new Error(`Cables.lag: ${spec.b.device} ${far} is cabled to both ${other} and ${near} on ${spec.a.device}`);
|
|
249
|
+
taken.set(far, near);
|
|
250
|
+
}
|
|
251
|
+
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b, far), lag: spec.a.lag }]));
|
|
252
|
+
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a, near), lag: spec.b.lag }]));
|
|
253
|
+
return {
|
|
254
|
+
a,
|
|
255
|
+
b
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
static end(shared, far, port) {
|
|
259
|
+
const farEnd = { device: far.device, port, label: Catalog.spell(far.platform, far.model, port) };
|
|
260
|
+
return {
|
|
261
|
+
description: shared.describe ? shared.describe(farEnd) : `${farEnd.device} ${farEnd.label}`,
|
|
262
|
+
link: { device: far.device, port, ...shared.through ? { through: shared.through } : {} }
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
184
266
|
// src/helpers/checks.ts
|
|
185
267
|
class Checks {
|
|
186
268
|
static exportsEndInReject = (devices) => {
|
|
@@ -585,10 +667,154 @@ class Filters {
|
|
|
585
667
|
}));
|
|
586
668
|
}
|
|
587
669
|
}
|
|
670
|
+
// src/helpers/upstreams.ts
|
|
671
|
+
class Upstreams {
|
|
672
|
+
static define(catalog) {
|
|
673
|
+
const all = Object.entries(catalog);
|
|
674
|
+
const nameOf = (key, upstream) => upstream.name ?? key;
|
|
675
|
+
const policy = (key) => key.toUpperCase();
|
|
676
|
+
const grouped = (upstream) => upstream.role === "rs" || Boolean(upstream.multihop);
|
|
677
|
+
const vlans = (device) => {
|
|
678
|
+
const result = {};
|
|
679
|
+
for (const [key, upstream] of all) {
|
|
680
|
+
const handoff = upstream.handoff;
|
|
681
|
+
if (!handoff || device !== undefined && handoff.port?.device !== device)
|
|
682
|
+
continue;
|
|
683
|
+
result[key] = { id: handoff.vlan, description: nameOf(key, upstream) };
|
|
684
|
+
if (device !== undefined && handoff.native !== undefined) {
|
|
685
|
+
result[`${key}-native`] = {
|
|
686
|
+
id: handoff.native,
|
|
687
|
+
description: `Native toward ${nameOf(key, upstream)}, nothing else in it`
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return result;
|
|
692
|
+
};
|
|
693
|
+
const vlansOn = (device) => all.filter(([, upstream]) => upstream.handoff?.port?.device === device).map(([key]) => key);
|
|
694
|
+
const ports = (device) => {
|
|
695
|
+
const result = {};
|
|
696
|
+
for (const [key, upstream] of all) {
|
|
697
|
+
const handoff = upstream.handoff;
|
|
698
|
+
if (!handoff?.port || handoff.port.device !== device)
|
|
699
|
+
continue;
|
|
700
|
+
result[handoff.port.port] = {
|
|
701
|
+
description: nameOf(key, upstream),
|
|
702
|
+
...handoff.native !== undefined ? { trunk_vlans: [key], native_vlan: `${key}-native` } : { access_vlan: key },
|
|
703
|
+
lldp: false,
|
|
704
|
+
stp: false,
|
|
705
|
+
...handoff.port.storm_control ? { storm_control: handoff.port.storm_control } : {}
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return result;
|
|
709
|
+
};
|
|
710
|
+
const interfaces = () => {
|
|
711
|
+
const result = {};
|
|
712
|
+
for (const [key, upstream] of all) {
|
|
713
|
+
const description = nameOf(key, upstream);
|
|
714
|
+
if (upstream.tunnel) {
|
|
715
|
+
const { local, remote, addresses, mtu } = upstream.tunnel;
|
|
716
|
+
result[key] = { type: "gre", description, local, remote, addresses, ...mtu ? { mtu } : {} };
|
|
717
|
+
} else if (upstream.handoff) {
|
|
718
|
+
const { addresses, exchange } = upstream.handoff;
|
|
719
|
+
result[key] = { type: "vlan", vlan: key, description, addresses, ...exchange ? { exchange } : {} };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return result;
|
|
723
|
+
};
|
|
724
|
+
const settings = (key, upstream) => ({
|
|
725
|
+
remote_as: upstream.asn,
|
|
726
|
+
description: nameOf(key, upstream),
|
|
727
|
+
role: upstream.role,
|
|
728
|
+
...upstream.import ? { import: `${policy(key)}-IN` } : {},
|
|
729
|
+
export: `${policy(key)}-OUT`
|
|
730
|
+
});
|
|
731
|
+
const groups = () => Object.fromEntries(all.filter(([, upstream]) => grouped(upstream)).map(([key, upstream]) => {
|
|
732
|
+
const multihop = upstream.multihop;
|
|
733
|
+
return [
|
|
734
|
+
key,
|
|
735
|
+
{
|
|
736
|
+
...settings(key, upstream),
|
|
737
|
+
...multihop ? {
|
|
738
|
+
local_address: multihop.local_address,
|
|
739
|
+
multihop: true,
|
|
740
|
+
...multihop.families ? { families: multihop.families } : {},
|
|
741
|
+
...multihop.password ? { password: multihop.password } : {}
|
|
742
|
+
} : {}
|
|
743
|
+
}
|
|
744
|
+
];
|
|
745
|
+
}));
|
|
746
|
+
const neighbors = () => Object.fromEntries(all.flatMap(([key, upstream]) => Object.entries(upstream.sessions).map(([name, session]) => {
|
|
747
|
+
const limit = upstream.limits?.[Address2.family(session.address)];
|
|
748
|
+
return [
|
|
749
|
+
name,
|
|
750
|
+
{
|
|
751
|
+
...session,
|
|
752
|
+
...grouped(upstream) ? { group: key } : settings(key, upstream),
|
|
753
|
+
...limit ? { max_prefixes: limit } : {}
|
|
754
|
+
}
|
|
755
|
+
];
|
|
756
|
+
})));
|
|
757
|
+
const policies = (options = {}) => {
|
|
758
|
+
const result = {};
|
|
759
|
+
for (const [key, upstream] of all) {
|
|
760
|
+
result[`${policy(key)}-OUT`] = [
|
|
761
|
+
...options.actions?.(key, upstream) ?? [],
|
|
762
|
+
...upstream.export ?? [],
|
|
763
|
+
...Bgp.announce(upstream.announce)
|
|
764
|
+
];
|
|
765
|
+
const taken = upstream.import;
|
|
766
|
+
if (!taken)
|
|
767
|
+
continue;
|
|
768
|
+
const tag = options.tag?.(key, upstream);
|
|
769
|
+
result[`${policy(key)}-IN`] = [
|
|
770
|
+
...taken.default ? Bgp.defaultOnly({ ...taken.default, ...options.sources ? { sources: options.sources } : {} }) : [],
|
|
771
|
+
...taken.learn ? Bgp.learn({
|
|
772
|
+
...taken.learn.before ? { before: taken.learn.before } : {},
|
|
773
|
+
...options.sanity ? { sanity: options.sanity } : {},
|
|
774
|
+
...options.sources ? { sources: options.sources } : {},
|
|
775
|
+
...taken.learn.local_pref !== undefined ? { local_pref: taken.learn.local_pref } : {},
|
|
776
|
+
...tag !== undefined ? { tag } : {}
|
|
777
|
+
}) : [{ description: "Nothing else", action: "reject" }]
|
|
778
|
+
];
|
|
779
|
+
}
|
|
780
|
+
return result;
|
|
781
|
+
};
|
|
782
|
+
const untrusted = () => [...all.filter(([, upstream]) => upstream.handoff).map(([key]) => key), ...natives()];
|
|
783
|
+
const natives = () => all.filter(([, upstream]) => upstream.handoff?.native !== undefined).map(([key]) => `${key}-native`);
|
|
784
|
+
const privateDrops = () => all.filter(([, upstream]) => upstream.handoff && upstream.drop_private).map(([key]) => key);
|
|
785
|
+
const tunnelRules = () => all.flatMap(([key, upstream]) => upstream.tunnel ? [
|
|
786
|
+
{
|
|
787
|
+
description: `GRE from ${nameOf(key, upstream)}`,
|
|
788
|
+
match: {
|
|
789
|
+
protocol: "gre",
|
|
790
|
+
src: `${upstream.tunnel.remote}/${Address2.bits(Address2.family(upstream.tunnel.remote))}`
|
|
791
|
+
},
|
|
792
|
+
action: "accept"
|
|
793
|
+
}
|
|
794
|
+
] : []);
|
|
795
|
+
const addresses = () => all.flatMap(([, upstream]) => Object.values(upstream.sessions).map((session) => session.address));
|
|
796
|
+
return {
|
|
797
|
+
vlans,
|
|
798
|
+
vlansOn,
|
|
799
|
+
ports,
|
|
800
|
+
interfaces,
|
|
801
|
+
groups,
|
|
802
|
+
neighbors,
|
|
803
|
+
policies,
|
|
804
|
+
untrusted,
|
|
805
|
+
natives,
|
|
806
|
+
privateDrops,
|
|
807
|
+
tunnelRules,
|
|
808
|
+
addresses
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
588
812
|
export {
|
|
589
813
|
Bgp,
|
|
814
|
+
Cables,
|
|
590
815
|
Checks,
|
|
591
816
|
Exchange,
|
|
592
817
|
Filters,
|
|
593
|
-
RouteServerConfig
|
|
818
|
+
RouteServerConfig,
|
|
819
|
+
Upstreams
|
|
594
820
|
};
|