@takodotid/circuit 0.37.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.
@@ -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 { Catalog, Topology, AddressSpace, StudioModel };
921
+ export { Topology, AddressSpace, StudioModel };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  StudioModel
3
- } from "./index-k38bby43.js";
3
+ } from "./index-yzw86rc8.js";
4
4
 
5
5
  // src/studio/server.ts
6
6
  import { execFile as execFile2, spawn } from "node:child_process";
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-k38bby43.js";
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";
@@ -6596,6 +6598,20 @@ class BgpWait {
6596
6598
  static upCount(sessions) {
6597
6599
  return sessions.filter((session) => session.established).length;
6598
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
+ }
6599
6615
  }
6600
6616
 
6601
6617
  // src/platforms/routeros/export.ts
@@ -8381,9 +8397,15 @@ class RouterOs extends Platform {
8381
8397
  static LIST_BGP_SESSIONS = [
8382
8398
  ":foreach s in=[/routing bgp session find] do={",
8383
8399
  ":local g [/routing bgp session get $s];",
8384
- ':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])',
8385
8404
  "}"
8386
8405
  ].join(" ");
8406
+ static clearLimit(address) {
8407
+ return `/routing bgp session clear [find remote.address=${address} and limit-exceeded] flag=limit-exceeded`;
8408
+ }
8387
8409
  static LIST_NEIGHBORS = [
8388
8410
  ':foreach n in=[/ip neighbor find where discovered-by~"lldp"] do={',
8389
8411
  ":local g [/ip neighbor get $n];",
@@ -8578,21 +8600,48 @@ ${certificates}` : "";
8578
8600
  }
8579
8601
  async bgpSessions(device, session) {
8580
8602
  const output = await session.run(RouterOs.LIST_BGP_SESSIONS, 60000);
8581
- const live = output.split(/\r?\n/).filter((line) => line.includes("|")).map((line) => line.trim().split("|"));
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] }));
8582
8606
  return Object.entries(device.routing?.bgp?.neighbors ?? {}).map(([name, neighbor]) => {
8583
8607
  const found = live.find(([, remote]) => remote === neighbor.address) ?? live.find(([sessionName]) => sessionName === name || sessionName?.replace(/-\d+$/, "") === name);
8584
- if (!found)
8585
- return { neighbor: name, remote: "-", established: false, prefixes: "-", uptime: "no session" };
8586
- const [, remote, established, prefixes, uptime] = found;
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";
8587
8622
  return {
8588
8623
  neighbor: name,
8589
8624
  remote: remote || "-",
8590
- established: established === "true",
8625
+ established: up,
8591
8626
  prefixes: prefixes || "-",
8592
- 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
+ } : {}
8593
8635
  };
8594
8636
  });
8595
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
+ }
8596
8645
  async bgpLog(session, minutes) {
8597
8646
  const now = (await session.run(RouterOs.CLOCK)).trim();
8598
8647
  const nowTime = RouterOs.logTime(now);
@@ -8688,7 +8737,7 @@ ${certificates}` : "";
8688
8737
  console.log(` BGP: all ${neighbors.length} established again`);
8689
8738
  return [];
8690
8739
  }
8691
- 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`);
8692
8741
  }
8693
8742
  static async invalidSwitchRules(session, device) {
8694
8743
  const answer = (await session.run(RouterOs.COUNT_INVALID_SWITCH_RULES)).trim();
@@ -11567,7 +11616,7 @@ class Cli {
11567
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));
11568
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));
11569
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));
11570
- 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));
11571
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));
11572
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));
11573
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));
@@ -11797,6 +11846,10 @@ ${output.replace(/\r/g, "").trimEnd()}${stoppedAtLimit}
11797
11846
  Cli.fail(`circuit bgp does not support ${device.platform} yet, only ${supported}`);
11798
11847
  if (flags.log && !adapter.bgpLog)
11799
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");
11800
11853
  let matcher;
11801
11854
  try {
11802
11855
  matcher = new RegExp(pattern ?? ".");
@@ -11813,6 +11866,10 @@ ${output.replace(/\r/g, "").trimEnd()}${stoppedAtLimit}
11813
11866
  const session = await adapter.session(device, this.optionsFor(device));
11814
11867
  try {
11815
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
+ }
11816
11873
  const limits = { seconds: flags.max, every: BgpWait.COMMAND.every };
11817
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`));
11818
11875
  Cli.printBgp(sessions);
@@ -11824,6 +11881,24 @@ ${output.replace(/\r/g, "").trimEnd()}${stoppedAtLimit}
11824
11881
  session.close();
11825
11882
  }
11826
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
+ }
11827
11902
  bgpTarget(first, second) {
11828
11903
  if (second !== undefined || first && this.network.devices.some((device) => device.name === first)) {
11829
11904
  return { device: this.one(first), pattern: second };
@@ -11840,9 +11915,25 @@ ${output.replace(/\r/g, "").trimEnd()}${stoppedAtLimit}
11840
11915
  const width = Math.max(...sessions.map((session) => session.neighbor.length));
11841
11916
  for (const session of sessions) {
11842
11917
  const state = session.established ? "up " : "DOWN";
11843
- console.log(`${session.neighbor.padEnd(width)} ${state} ${session.prefixes.padStart(7)} ${session.uptime.padEnd(12)} ${session.remote}`);
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}`);
11844
11920
  }
11845
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}`;
11846
11937
  }
11847
11938
  async printBgpLog(adapter, session, minutes) {
11848
11939
  const lines = Cli.foldLog(await adapter.bgpLog(session, minutes));
@@ -12286,7 +12377,7 @@ Renamed. Run circuit validate, then circuit apply ${to}: the device keeps its ol
12286
12377
  const port = Number(flags.port);
12287
12378
  if (!Number.isInteger(port) || port < 1 || port > 65535)
12288
12379
  Cli.fail("--port takes a number from 1 to 65535");
12289
- const { StudioServer } = await import("../chunks/server-ggertnce.js");
12380
+ const { StudioServer } = await import("../chunks/server-cxr9ynxc.js");
12290
12381
  const host = flags.host;
12291
12382
  const allowHosts = (flags.allowHost ?? "").split(",").filter(Boolean);
12292
12383
  if (!StudioServer.isLoopback(host) && !allowHosts.length) {
@@ -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
  };