@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.
Files changed (74) hide show
  1. package/dist/chunks/index-6wb41q67.js +62 -0
  2. package/dist/chunks/{index-2r4bbhac.js → index-yzw86rc8.js} +9 -65
  3. package/dist/chunks/{server-gzsf5me5.js → server-cxr9ynxc.js} +1 -1
  4. package/dist/cli/index.js +110 -13
  5. package/dist/helpers/index.js +227 -1
  6. package/dist/types/core/address-space.d.ts +1 -1
  7. package/dist/types/core/address.d.ts +1 -1
  8. package/dist/types/core/cabling.d.ts +3 -3
  9. package/dist/types/core/cross-checks.d.ts +3 -3
  10. package/dist/types/core/define.d.ts +2 -2
  11. package/dist/types/core/secrets.d.ts +2 -2
  12. package/dist/types/core/topology.d.ts +2 -2
  13. package/dist/types/core/validator.d.ts +1 -1
  14. package/dist/types/helpers/bgp.d.ts +48 -5
  15. package/dist/types/helpers/cables.d.ts +70 -0
  16. package/dist/types/helpers/checks.d.ts +1 -1
  17. package/dist/types/helpers/exchange.d.ts +3 -3
  18. package/dist/types/helpers/filters.d.ts +1 -1
  19. package/dist/types/helpers/index.d.ts +14 -10
  20. package/dist/types/helpers/route-server.d.ts +2 -2
  21. package/dist/types/helpers/upstreams.d.ts +156 -0
  22. package/dist/types/index.d.ts +8 -6
  23. package/dist/types/integrations/librenms.d.ts +2 -2
  24. package/dist/types/integrations/netbox.d.ts +1 -1
  25. package/dist/types/integrations/peeringdb.d.ts +1 -1
  26. package/dist/types/integrations/prometheus.d.ts +2 -2
  27. package/dist/types/integrations/registries.d.ts +2 -2
  28. package/dist/types/platforms/bgp-wait.d.ts +7 -1
  29. package/dist/types/platforms/derive.d.ts +1 -1
  30. package/dist/types/platforms/fortios/firewall.d.ts +2 -2
  31. package/dist/types/platforms/fortios/index.d.ts +4 -4
  32. package/dist/types/platforms/fortios/names.d.ts +1 -1
  33. package/dist/types/platforms/fortios/plan.d.ts +1 -1
  34. package/dist/types/platforms/fortios/render.d.ts +1 -1
  35. package/dist/types/platforms/fortios/routing.d.ts +2 -2
  36. package/dist/types/platforms/index.d.ts +1 -1
  37. package/dist/types/platforms/junos/bgp.d.ts +2 -2
  38. package/dist/types/platforms/junos/index.d.ts +4 -4
  39. package/dist/types/platforms/junos/render.d.ts +1 -1
  40. package/dist/types/platforms/line-diff.d.ts +1 -1
  41. package/dist/types/platforms/platform.d.ts +17 -5
  42. package/dist/types/platforms/raisecom-ros/index.d.ts +4 -4
  43. package/dist/types/platforms/raisecom-ros/render.d.ts +1 -1
  44. package/dist/types/platforms/routeros/index.d.ts +12 -5
  45. package/dist/types/platforms/routeros/plan.d.ts +1 -1
  46. package/dist/types/platforms/routeros/render.d.ts +1 -1
  47. package/dist/types/platforms/vrp/index.d.ts +4 -4
  48. package/dist/types/platforms/vrp/render.d.ts +1 -1
  49. package/dist/types/schema/device.d.ts +6 -6
  50. package/dist/types/schema/firewall.d.ts +1 -1
  51. package/dist/types/schema/index.d.ts +8 -8
  52. package/dist/types/schema/interfaces.d.ts +5 -1
  53. package/dist/types/schema/policy.d.ts +1 -1
  54. package/dist/types/schema/routing.d.ts +1 -1
  55. package/dist/types/schema/system.d.ts +2 -2
  56. package/dist/types/transport/console.d.ts +1 -1
  57. package/dist/types/transport/ssh.d.ts +1 -1
  58. package/package.json +2 -2
  59. package/src/cli/index.ts +55 -4
  60. package/src/core/cabling.ts +4 -1
  61. package/src/core/cross-checks.ts +6 -1
  62. package/src/core/topology.ts +4 -3
  63. package/src/helpers/bgp.ts +104 -2
  64. package/src/helpers/cables.ts +91 -0
  65. package/src/helpers/index.ts +6 -2
  66. package/src/helpers/upstreams.ts +306 -0
  67. package/src/index.ts +3 -0
  68. package/src/platforms/bgp-wait.ts +24 -1
  69. package/src/platforms/platform.ts +9 -0
  70. package/src/platforms/routeros/TRAPS.md +1 -0
  71. package/src/platforms/routeros/index.ts +57 -10
  72. package/src/schema/interfaces.ts +4 -0
  73. package/src/studio/data.ts +3 -0
  74. package/src/studio/public/tables.js +15 -1
@@ -1,4 +1,4 @@
1
- import type { Plan } from "./platform";
1
+ import type { Plan } from "./platform.js";
2
2
  export type Dialect = {
3
3
  /** A line at the margin that opens a block, for platforms that do not indent a block's lines. */
4
4
  block: RegExp;
@@ -1,8 +1,8 @@
1
- import type { Neighbor } from "../core/cabling";
2
- import type { Device, Family } from "../schema";
3
- import type { ConsoleDialect } from "../transport/console";
4
- import type { WaitLimits } from "./bgp-wait";
5
- import type { PlatformName } from "./catalog";
1
+ import type { Neighbor } from "../core/cabling.js";
2
+ import type { Device, Family } from "../schema/index.js";
3
+ import type { ConsoleDialect } from "../transport/console.js";
4
+ import type { WaitLimits } from "./bgp-wait.js";
5
+ import type { PlatformName } from "./catalog.js";
6
6
  /** One part of a plan: what a reviewer reads, and what is sent. */
7
7
  export type Step = {
8
8
  title: string;
@@ -57,6 +57,16 @@ export type BgpSession = {
57
57
  prefixes: string;
58
58
  /** How long it has been up, as the device prints it, or `no session`. */
59
59
  uptime: string;
60
+ /** Why it is down, when the device says: `limit exceeded`, `admin down`, `connecting`. */
61
+ reason?: string;
62
+ /** It closed because the neighbor sent more prefixes than its limit, and stays closed until cleared. */
63
+ limit?: BgpLimit;
64
+ };
65
+ /** A session closed on its prefix limit: the limit, and when it closed, as the device prints it and in milliseconds, to compare. */
66
+ export type BgpLimit = {
67
+ max?: number;
68
+ stopped?: string;
69
+ stoppedAt?: number;
60
70
  };
61
71
  /** One BGP log line: when, as the device printed it, such as `2026-10-24 03:12:44`, and the rest of the line. */
62
72
  export type BgpLogLine = {
@@ -113,6 +123,8 @@ export declare abstract class Platform {
113
123
  bgpSessions?(device: Device, session: ReadSession): Promise<BgpSession[]>;
114
124
  /** The device's BGP log lines from the last `minutes`, by the device's own clock, read over a `session()`. */
115
125
  bgpLog?(session: ReadSession, minutes: number): Promise<BgpLogLine[]>;
126
+ /** Open again these sessions, each closed on its prefix limit, read by `bgpSessions`. Only a session still closed on its limit is touched. */
127
+ bgpClearLimit?(session: ReadSession, sessions: readonly BgpSession[]): Promise<void>;
116
128
  /**
117
129
  * Checks that the device is healthy after an apply, not only that it took the commands. Called before the plan is sent, to read what the checks compare against; the CLI runs them after the apply and the snapshot. Absent on a platform with no checks yet.
118
130
  */
@@ -1,8 +1,8 @@
1
1
  import type { ClientChannel } from "ssh2";
2
- import type { Neighbor } from "../../core/cabling";
3
- import type { Device } from "../../schema";
4
- import { type ConsoleDialect } from "../../transport/console";
5
- import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform";
2
+ import type { Neighbor } from "../../core/cabling.js";
3
+ import type { Device } from "../../schema/index.js";
4
+ import { type ConsoleDialect } from "../../transport/console.js";
5
+ import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform.js";
6
6
  /** A session, and how to prove access in a fresh login: a new SSH session, or logging out and in again on the console. */
7
7
  export type Session = {
8
8
  channel: ClientChannel;
@@ -1,4 +1,4 @@
1
- import type { Device, SnmpUser } from "../../schema";
1
+ import type { Device, SnmpUser } from "../../schema/index.js";
2
2
  /** The configuration a Raisecom ROS device should run. */
3
3
  export declare class RaisecomRosRender {
4
4
  /** The access-list that guards every management service. */
@@ -1,7 +1,7 @@
1
- import type { Neighbor } from "../../core/cabling";
2
- import type { Device, Family } from "../../schema";
3
- import { type ConsoleDialect } from "../../transport/console";
4
- import { type ApplyOptions, type BgpLogLine, type BgpSession, type HealthCheck, type HealthOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, type SwitchRules, Platform } from "../platform";
1
+ import type { Neighbor } from "../../core/cabling.js";
2
+ import type { Device, Family } from "../../schema/index.js";
3
+ import { type ConsoleDialect } from "../../transport/console.js";
4
+ import { type ApplyOptions, type BgpLogLine, type BgpSession, type HealthCheck, type HealthOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, type SwitchRules, Platform } from "../platform.js";
5
5
  export declare class RouterOs extends Platform {
6
6
  /** How the RouterOS console asks for a login. The flags after the user turn off colours and terminal detection, without which the console redraws every line. */
7
7
  static readonly CONSOLE: ConsoleDialect;
@@ -13,8 +13,12 @@ export declare class RouterOs extends Platform {
13
13
  private static readonly EXPORT_TIMESTAMP;
14
14
  /** Lists certificates in the form the renderer writes them, since an export does not include them. One line, because a script sent over exec runs as one. */
15
15
  private static readonly LIST_CERTIFICATES;
16
- /** Every BGP session, one line each: name, remote address, established, prefixes received, uptime. One line, because a script sent over exec runs as one. */
16
+ /**
17
+ * Every BGP session, one line each: name, remote address, established, prefixes received, uptime, closed on its limit, the limit, when it last closed. Then each disabled connection, `disabled|<name>|<address>`. One line, because a script sent over exec runs as one.
18
+ */
17
19
  private static readonly LIST_BGP_SESSIONS;
20
+ /** Open again the session from one address, only while it is closed on its limit. */
21
+ private static clearLimit;
18
22
  /**
19
23
  * Every LLDP neighbor, one line each: our interfaces, the port first and then the LAG and bridge it is in, the far system name, and the far port as LLDP reports it. `:tostr` keeps the interface list one value; joined to a string as it is, a list repeats the whole line for each of its items. One line, because a script sent over exec runs as one.
20
24
  */
@@ -51,8 +55,11 @@ export declare class RouterOs extends Platform {
51
55
  session(device: Device, options?: ReadOptions): Promise<ReadSession>;
52
56
  /**
53
57
  * A session is named `<connection>-<n>`, but a renamed connection keeps its session's old name until the session restarts. So a neighbor is matched by its address first, then by name.
58
+ *
59
+ * A session that is down says why: closed on its limit, or still connecting. A neighbor with no session and a disabled connection is admin down.
54
60
  */
55
61
  bgpSessions(device: Device, session: ReadSession): Promise<BgpSession[]>;
62
+ bgpClearLimit(session: ReadSession, sessions: readonly BgpSession[]): Promise<void>;
56
63
  /** A `time>` filter in `/log print` does not filter, so the lines are kept here by comparing each one's time with the device's clock. */
57
64
  bgpLog(session: ReadSession, minutes: number): Promise<BgpLogLine[]>;
58
65
  /**
@@ -1,4 +1,4 @@
1
- import type { ApplyOptions, Plan } from "../platform";
1
+ import type { ApplyOptions, Plan } from "../platform.js";
2
2
  export declare class RouterOsPlan {
3
3
  private readonly sendSecrets;
4
4
  /** A plan line the apply handles itself: upload a certificate and its key, and import them. */
@@ -1,4 +1,4 @@
1
- import type { AclRule, Device, Ethertype, Interface, Port } from "../../schema";
1
+ import type { AclRule, Device, Ethertype, Interface, Port } from "../../schema/index.js";
2
2
  /** A property value as a command takes it. `undefined` and `false` leave the property out. */
3
3
  export type Value = string | number | undefined | false;
4
4
  export type AnyPort = Port<string, string, string, string>;
@@ -1,7 +1,7 @@
1
- import type { Neighbor } from "../../core/cabling";
2
- import type { Device } from "../../schema";
3
- import { type ConsoleDialect } from "../../transport/console";
4
- import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform";
1
+ import type { Neighbor } from "../../core/cabling.js";
2
+ import type { Device } from "../../schema/index.js";
3
+ import { type ConsoleDialect } from "../../transport/console.js";
4
+ import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform.js";
5
5
  export declare class Vrp extends Platform {
6
6
  /** How the console asks for a login: the same question to change the password, always answered no, a system view left with `return`, and the line's banner after a logout or a refused password answered with Enter. Not yet proved on a device. */
7
7
  static readonly CONSOLE: ConsoleDialect;
@@ -1,4 +1,4 @@
1
- import type { Device } from "../../schema";
1
+ import type { Device } from "../../schema/index.js";
2
2
  /** Builds the configuration as blocks: a header line, its indented lines, then `#`. */
3
3
  export declare class VrpRender {
4
4
  private readonly device;
@@ -1,9 +1,9 @@
1
- import type { IP, Secret } from "./common";
2
- import type { AclRule, Firewall } from "./firewall";
3
- import type { Interface, Port, Vlan } from "./interfaces";
4
- import type { PrefixSet, RoutePolicy } from "./policy";
5
- import type { Routing } from "./routing";
6
- import type { Certificate, DhcpRelay, DhcpServer, FlowExport, Management, System, User } from "./system";
1
+ import type { IP, Secret } from "./common.js";
2
+ import type { AclRule, Firewall } from "./firewall.js";
3
+ import type { Interface, Port, Vlan } from "./interfaces.js";
4
+ import type { PrefixSet, RoutePolicy } from "./policy.js";
5
+ import type { Routing } from "./routing.js";
6
+ import type { Certificate, DhcpRelay, DhcpServer, FlowExport, Management, System, User } from "./system.js";
7
7
  /** How the framework reaches the device. */
8
8
  export type Connection<U extends string> = {
9
9
  /** Management address. Absent for a device reached only through its console, `console`. */
@@ -1,4 +1,4 @@
1
- import type { Family, IP, Prefix, Rate } from "./common";
1
+ import type { Family, IP, Prefix, Rate } from "./common.js";
2
2
  /** An IP protocol. `icmp` means ICMPv6 in an IPv6 rule. */
3
3
  export type Protocol = "tcp" | "udp" | "icmp" | "gre" | "esp" | "ah" | "ospf" | "vrrp";
4
4
  /** The state connection tracking assigns a packet. */
@@ -1,8 +1,8 @@
1
- export type * from "./common";
2
- export type * from "./device";
3
- export type * from "./firewall";
4
- export type * from "./interfaces";
5
- export type * from "./policy";
6
- export type * from "./routing";
7
- export type * from "./system";
8
- export type * from "./time-zones";
1
+ export type * from "./common.js";
2
+ export type * from "./device.js";
3
+ export type * from "./firewall.js";
4
+ export type * from "./interfaces.js";
5
+ export type * from "./policy.js";
6
+ export type * from "./routing.js";
7
+ export type * from "./system.js";
8
+ export type * from "./time-zones.js";
@@ -1,4 +1,4 @@
1
- import type { InterfaceAddress, IP, MacAddress, Prefix, Secret } from "./common";
1
+ import type { InterfaceAddress, IP, MacAddress, Prefix, Secret } from "./common.js";
2
2
  /** A VLAN. Its key in `vlans` is its name. */
3
3
  export type Vlan = {
4
4
  /** The VLAN's number on the wire, 1 to 4094. */
@@ -90,6 +90,10 @@ export type Link = {
90
90
  device: string;
91
91
  /** The port on that device. */
92
92
  port: string;
93
+ /**
94
+ * What is between the two ends when the cable is not direct, such as a carrier's local loop or L2 transport: `"Carrier A local loop"`. LLDP then sees the carrier's switches, not the far end, so `validate` does not compare LLDP on this port. Both ends name the same carrier.
95
+ */
96
+ through?: string;
93
97
  };
94
98
  /** A physical port. Ports a device does not declare are shut down. */
95
99
  export type Port<V extends string, A extends string, L extends string, F extends string> = Routed<F> & Switched<V> & {
@@ -1,4 +1,4 @@
1
- import type { Family, IP, Prefix } from "./common";
1
+ import type { Family, IP, Prefix } from "./common.js";
2
2
  /** A standard BGP community, `asn:value`, for example `64500:100`. Each part is 0 to 65535. */
3
3
  export type StandardCommunity = `${number}:${number}`;
4
4
  /** A large BGP community, `asn:function:parameter`, for example `64500:1:6939`. Each part is 0 to 4294967295. */
@@ -1,4 +1,4 @@
1
- import type { Family, IP, Prefix, Secret } from "./common";
1
+ import type { Family, IP, Prefix, Secret } from "./common.js";
2
2
  /** A route the device has without learning it. */
3
3
  export type StaticRoute<I extends string, F extends string> = {
4
4
  /** Destination. */
@@ -1,5 +1,5 @@
1
- import type { IP, MacAddress, Prefix, Secret, Severity } from "./common";
2
- import type { TimeZone } from "./time-zones";
1
+ import type { IP, MacAddress, Prefix, Secret, Severity } from "./common.js";
2
+ import type { TimeZone } from "./time-zones.js";
3
3
  /** A software release track, as RouterOS names them. `long-term` changes least; `development` is the newest and least tested. */
4
4
  export type ReleaseChannel = "long-term" | "stable" | "testing" | "development";
5
5
  /** How bad an alarm is, most severe first, as ITU-T X.733 ranks them. */
@@ -1,5 +1,5 @@
1
1
  import { type ClientChannel } from "ssh2";
2
- import type { Console, Device } from "../schema";
2
+ import type { Console, Device } from "../schema/index.js";
3
3
  /** An open console: the stream to the device's CLI, and how to let it go. `close` waits for the console to go quiet, then lets the port go. */
4
4
  export type ConsoleSession = {
5
5
  channel: ClientChannel;
@@ -1,5 +1,5 @@
1
1
  import { Client, type ClientChannel } from "ssh2";
2
- import type { Device } from "../schema";
2
+ import type { Device } from "../schema/index.js";
3
3
  export type Login = {
4
4
  host: string;
5
5
  port?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "Declarative, vendor-neutral network configuration: describe each device in TypeScript, and Circuit makes the device match. Alpha.",
5
5
  "keywords": [
6
6
  "network",
@@ -89,7 +89,7 @@
89
89
  "release": "bun scripts/release.ts",
90
90
  "prepare": "husky || true",
91
91
  "prepack": "bun run build",
92
- "check": "bun run typecheck && bun run test && bun run format:check && bun run docs:build && bun run build && node dist/cli/index.js validate --config templates/single-site/circuit.config.ts && node dist/cli/index.js validate --config templates/edge-router/circuit.config.ts && node dist/cli/index.js validate --config templates/colocation/circuit.config.ts",
92
+ "check": "bun run typecheck && bun run test && bun run format:check && bun run docs:build && bun run build && bunx tsc -p test/package && node dist/cli/index.js validate --config templates/single-site/circuit.config.ts && node dist/cli/index.js validate --config templates/edge-router/circuit.config.ts && node dist/cli/index.js validate --config templates/colocation/circuit.config.ts",
93
93
  "agents:setup": "npx -y skills experimental_install"
94
94
  },
95
95
  "dependencies": {
package/src/cli/index.ts CHANGED
@@ -228,7 +228,7 @@ class Cli {
228
228
 
229
229
  program
230
230
  .command("bgp")
231
- .description("each BGP neighbor in the config next to its live session: up or down, prefixes, uptime")
231
+ .description("each BGP neighbor in the config next to its live session: up or down and why, prefixes, uptime")
232
232
  .argument("[device]", "may be left out when only one device has BGP neighbors")
233
233
  .argument("[name-regex]", "only the neighbors whose name matches")
234
234
  .option(
@@ -238,6 +238,7 @@ class Cli {
238
238
  )
239
239
  .option("--max <seconds>", "how long --wait waits at most", Cli.seconds, BgpWait.COMMAND.seconds)
240
240
  .option("--log [minutes]", "also print the device's BGP log lines from the last minutes, 5 by default", Cli.minutes)
241
+ .option("--clear-limit", "open again the neighbors shown that closed on their prefix limit; any other is left alone")
241
242
  .option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP)
242
243
  .action(async (first, second, flags) => (await Cli.open(flags.config)).bgp(first, second, flags));
243
244
 
@@ -604,12 +605,12 @@ class Cli {
604
605
  }
605
606
 
606
607
  /**
607
- * Each BGP neighbor in the config next to its live session. `first` is the device, or, when it names none and only one device has BGP neighbors, the name pattern. With `wait`, reads again until every neighbor shown, or that many, is up, and fails when time runs out.
608
+ * Each BGP neighbor in the config next to its live session. `first` is the device, or, when it names none and only one device has BGP neighbors, the name pattern. With `wait`, reads again until every neighbor shown, or that many, is up, and fails when time runs out. With `clearLimit`, opens again the neighbors shown that closed on their prefix limit, and fails when none did.
608
609
  */
609
610
  private async bgp(
610
611
  first: string | undefined,
611
612
  second: string | undefined,
612
- flags: { wait?: true | number; max: number; log?: true | number }
613
+ flags: { wait?: true | number; max: number; log?: true | number; clearLimit?: true }
613
614
  ): Promise<void> {
614
615
  const { device, pattern } = this.bgpTarget(first, second);
615
616
  const adapter = this.adapterOf(device);
@@ -620,6 +621,8 @@ class Cli {
620
621
  .join(", ");
621
622
  if (!adapter.bgpSessions) Cli.fail(`circuit bgp does not support ${device.platform} yet, only ${supported}`);
622
623
  if (flags.log && !adapter.bgpLog) Cli.fail(`circuit bgp --log does not support ${device.platform} yet`);
624
+ if (flags.clearLimit && !adapter.bgpClearLimit) Cli.fail(`circuit bgp --clear-limit does not support ${device.platform} yet`);
625
+ if (flags.clearLimit && flags.wait !== undefined) Cli.fail("--clear-limit and --wait go in two runs: clear first, then wait");
623
626
 
624
627
  let matcher: RegExp;
625
628
  try {
@@ -640,6 +643,11 @@ class Cli {
640
643
  try {
641
644
  const read = async () => (await adapter.bgpSessions!(device, session)).filter((neighbor) => shown.includes(neighbor.neighbor));
642
645
 
646
+ if (flags.clearLimit) {
647
+ await Cli.clearLimits(adapter, session, read);
648
+ return;
649
+ }
650
+
643
651
  const limits = { seconds: flags.max, every: BgpWait.COMMAND.every };
644
652
  const sessions = await BgpWait.until(
645
653
  read,
@@ -660,6 +668,27 @@ class Cli {
660
668
  }
661
669
  }
662
670
 
671
+ /** Open again the sessions closed on their limit, say which others were left alone, and show them all after. */
672
+ private static async clearLimits(adapter: Platform, session: ReadSession, read: () => Promise<BgpSession[]>): Promise<void> {
673
+ const sessions = await read();
674
+ const closed = sessions.filter((bgp) => bgp.limit);
675
+
676
+ if (!closed.length) {
677
+ Cli.printBgp(sessions);
678
+ console.error("\nno neighbor shown is closed on its prefix limit, so nothing was cleared");
679
+ process.exitCode = 1;
680
+ return;
681
+ }
682
+
683
+ for (const bgp of sessions) if (!bgp.limit) console.log(`${bgp.neighbor}: left alone, not closed on its limit`);
684
+ await adapter.bgpClearLimit!(session, closed);
685
+ console.log(
686
+ `cleared ${closed.map((bgp) => bgp.neighbor).join(", ")}: each connects again, and closes again if it still sends too much\n`
687
+ );
688
+
689
+ Cli.printBgp(await read());
690
+ }
691
+
663
692
  /** The device `circuit bgp` reads, and the name pattern, from its two optional arguments. */
664
693
  private bgpTarget(first: string | undefined, second: string | undefined): { device: Device; pattern: string | undefined } {
665
694
  if (second !== undefined || (first && this.network.devices.some((device) => device.name === first))) {
@@ -681,12 +710,34 @@ class Cli {
681
710
 
682
711
  for (const session of sessions) {
683
712
  const state = session.established ? "up " : "DOWN";
713
+ const why = session.reason ? ` ${Cli.downReason(session)}` : "";
684
714
  console.log(
685
- `${session.neighbor.padEnd(width)} ${state} ${session.prefixes.padStart(7)} ${session.uptime.padEnd(12)} ${session.remote}`
715
+ `${session.neighbor.padEnd(width)} ${state} ${session.prefixes.padStart(7)} ${session.uptime.padEnd(12)} ${session.remote}${why}`
686
716
  );
687
717
  }
688
718
 
689
719
  console.log(`${BgpWait.upCount(sessions)}/${sessions.length} up`);
720
+
721
+ for (const burst of BgpWait.limitBursts(sessions)) {
722
+ const names = burst.map((session) => session.neighbor);
723
+ const listed = names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names.at(-1)}` : names[0];
724
+ const seconds = Math.round((burst.at(-1)!.limit!.stoppedAt! - burst[0]!.limit!.stoppedAt!) / 1000);
725
+
726
+ console.log(
727
+ `
728
+ ${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.`
729
+ );
730
+ }
731
+ }
732
+
733
+ /** Why a session is down, with its limit and when it closed when that is why. */
734
+ private static downReason(session: BgpSession): string {
735
+ const { limit } = session;
736
+ if (!limit) return session.reason ?? "";
737
+
738
+ const over = limit.max !== undefined ? `: over ${limit.max} prefixes` : "";
739
+ const when = limit.stopped ? `, closed ${limit.stopped}` : "";
740
+ return `${session.reason}${over}${when}`;
690
741
  }
691
742
 
692
743
  /** The device's BGP log lines from the last `minutes`, a message repeated folded into one line, secrets hidden. */
@@ -67,7 +67,7 @@ export class Cabling {
67
67
  /**
68
68
  * Warnings where LLDP and `link` disagree, from each device's own view: a cable in another port than `link` names, a cable `link` names that LLDP does not see, and another of our devices on a port without `link`.
69
69
  *
70
- * Only ports where both ends run LLDP, by the config, are compared, and only on devices whose neighbors were read. LLDP can be off at the far end, so each is a warning.
70
+ * Only ports where both ends run LLDP, by the config, are compared, and only on devices whose neighbors were read. A link `through` a carrier is not compared. LLDP can be off at the far end, so each is a warning.
71
71
  */
72
72
  static compare(devices: readonly Device[], neighbors: ReadonlyMap<string, readonly Neighbor[]>): Finding[] {
73
73
  const findings: Finding[] = [];
@@ -97,6 +97,9 @@ export class Cabling {
97
97
  continue;
98
98
  }
99
99
 
100
+ // Through a carrier, LLDP sees the carrier's switches, not the far end.
101
+ if (link.through) continue;
102
+
100
103
  const far = devices.find((candidate) => candidate.name === link.device);
101
104
  if (!far || !Cabling.runsOn(far, link.port)) continue;
102
105
 
@@ -97,7 +97,7 @@ export class CrossChecks {
97
97
  return undefined;
98
98
  }
99
99
 
100
- /** A link names a device and port that exist, the far end links back, and both ends carry the same VLANs the same way, with the same MTU, speed and addresses. */
100
+ /** A link names a device and port that exist, the far end links back through the same carrier, and both ends carry the same VLANs the same way, with the same MTU, speed and addresses. */
101
101
  private static checkLinks(devices: readonly Device[]): Finding[] {
102
102
  const findings: Finding[] = [];
103
103
  const byName = new Map(devices.map((device) => [device.name, device]));
@@ -132,6 +132,11 @@ export class CrossChecks {
132
132
  continue;
133
133
  }
134
134
 
135
+ if (link.through !== farPort.link?.through) {
136
+ const say = (through: string | undefined) => (through ? `through ${through}` : "direct");
137
+ report(`${say(link.through)} here, and ${say(farPort.link?.through)} on ${link.device} ${link.port}`);
138
+ }
139
+
135
140
  const near = CrossChecks.carrierOf(device, portName, port);
136
141
  const away = CrossChecks.carrierOf(far, link.port, farPort);
137
142
 
@@ -203,7 +203,8 @@ export class Topology {
203
203
  ? Topology.deviceId(far.name)
204
204
  : graph.node(`missing:${link.device}`, "outside", link.device, ["named by a link, and not in the network"]);
205
205
 
206
- const cableLine = `${here} to ${there}`;
206
+ const through = link.through ? `, through ${link.through}` : "";
207
+ const cableLine = `${here} to ${there}${through}`;
207
208
 
208
209
  // A LAG's member cables to one LAG on the far side are one edge.
209
210
  const lag = port.lag;
@@ -214,7 +215,7 @@ export class Topology {
214
215
  if (bundle) {
215
216
  bundle.members++;
216
217
  bundle.cable.detail.push(cableLine);
217
- bundle.cable.label = `${lag} to ${farLag}, ${bundle.members} cables`;
218
+ bundle.cable.label = `${lag} to ${farLag}, ${bundle.members} cables${through}`;
218
219
  continue;
219
220
  }
220
221
 
@@ -224,7 +225,7 @@ export class Topology {
224
225
  layer: "l1",
225
226
  from: Topology.deviceId(device.name),
226
227
  to,
227
- label: isBundle ? `${lag} to ${farLag}` : `${portName} to ${link.port}`,
228
+ label: `${isBundle ? `${lag} to ${farLag}` : `${portName} to ${link.port}`}${through}`,
228
229
  detail: [cableLine],
229
230
  ...(linksBack ? {} : { oneSided: true as const }),
230
231
  });
@@ -1,5 +1,15 @@
1
+ import { Address } from "../core/address";
1
2
  import type { Community } from "../core/define";
2
- import type { Device, LargeCommunityPattern, PolicyRule, Prefix, StandardCommunity, StandardCommunityPattern } from "../schema";
3
+ import type {
4
+ Device,
5
+ IP,
6
+ LargeCommunity,
7
+ LargeCommunityPattern,
8
+ PolicyRule,
9
+ Prefix,
10
+ StandardCommunity,
11
+ StandardCommunityPattern,
12
+ } from "../schema";
3
13
 
4
14
  /** The prefix lengths a BGP import accepts. */
5
15
  export type SanityLengths = {
@@ -9,6 +19,33 @@ export type SanityLengths = {
9
19
  ipv6?: { min: number; max: number };
10
20
  };
11
21
 
22
+ /** A prefix we announce, and what it is. */
23
+ export type Announced = { prefix: Prefix; description?: string };
24
+
25
+ /** What `Bgp.learn` builds an import from. */
26
+ export type LearnOptions<P extends string> = {
27
+ /** Rules of your own, first. */
28
+ before?: readonly PolicyRule<P, never>[];
29
+ /** A policy holding `Bgp.sanity()` to call. The sanity rules are written in the import itself when absent. */
30
+ sanity?: P;
31
+ /** The source address for routes of its family, one per family. */
32
+ sources?: readonly IP[];
33
+ /** Preference for the routes learned. Higher wins. */
34
+ local_pref?: number;
35
+ /** Where the route came from: a community to add, or a rule, such as `communityScheme(...).tag(...)`. */
36
+ tag?: StandardCommunity | LargeCommunity | PolicyRule<never, never>;
37
+ };
38
+
39
+ /** What `Bgp.defaultOnly` sets on the default routes it takes. */
40
+ export type DefaultOnlyOptions = {
41
+ /** Preference for the default route. Higher wins. */
42
+ local_pref?: number;
43
+ /** Shown beside each rule. `Default route, ipv4` and `Default route, ipv6` when absent. */
44
+ description?: string;
45
+ /** The source address for the default of its family, one per family. */
46
+ sources?: readonly IP[];
47
+ };
48
+
12
49
  /** Kinds of neighbor and the number each is tagged with, such as `{ transit: 1, customer: 3 }`. */
13
50
  export type CommunityClasses = Readonly<Record<string, number>>;
14
51
 
@@ -45,7 +82,7 @@ export type CommunityScheme<
45
82
  /** The class names a scheme declares, or any name when it declares none. */
46
83
  type ClassName<Classes extends CommunityClasses> = {} extends Classes ? string : Extract<keyof Classes, string>;
47
84
 
48
- /** BGP policy: an import sanity policy, a community scheme, and helpers that read a device's BGP neighbors. */
85
+ /** BGP policy: an import sanity policy, the shapes most imports and exports take, a community scheme, and helpers that read a device's BGP neighbors. */
49
86
  export class Bgp {
50
87
  private static readonly PREPENDS = [
51
88
  ["once", 1],
@@ -53,6 +90,12 @@ export class Bgp {
53
90
  ["three_times", 3],
54
91
  ] as const;
55
92
 
93
+ /** The default route of each family. */
94
+ private static readonly DEFAULTS = [
95
+ { family: "ipv4", prefix: "0.0.0.0/0" },
96
+ { family: "ipv6", prefix: "::/0" },
97
+ ] as const;
98
+
56
99
  /** Route policy rules rejecting what no neighbor should send: prefixes too short or too long for the global table, and RPKI invalid. Place it first in an import, or as a policy other imports call. */
57
100
  static sanity(lengths: SanityLengths = {}): PolicyRule<never, never>[] {
58
101
  const ipv4 = lengths.ipv4 ?? { min: 8, max: 24 };
@@ -83,6 +126,65 @@ export class Bgp {
83
126
  ];
84
127
  }
85
128
 
129
+ /** An export policy that announces each prefix, at exactly its length, then rejects everything else. The last rule is the unconditional reject `Checks.exportsEndInReject` looks for. */
130
+ static announce(prefixes: readonly Announced[]): PolicyRule<never, never>[] {
131
+ return [
132
+ ...prefixes.map(({ prefix, description }) => ({
133
+ ...(description ? { description } : {}),
134
+ match: { prefix },
135
+ action: "accept" as const,
136
+ })),
137
+ { description: "Nothing else", action: "reject" },
138
+ ];
139
+ }
140
+
141
+ /**
142
+ * An import policy that takes a neighbor's routes: `before` first, then the sanity rules, then each source address and the local preference, then the tag, then accept.
143
+ *
144
+ * `sanity` names a policy holding `Bgp.sanity()` to call, so several imports share one; without it, the sanity rules are written here. `tag` is a community to add, or a rule such as `communityScheme(...).tag(...)`. Each of `sources` is the source address for routes of its own family.
145
+ */
146
+ static learn<const P extends string = never>(options: LearnOptions<P> = {}): PolicyRule<P, never>[] {
147
+ const { tag } = options;
148
+
149
+ return [
150
+ ...(options.before ?? []),
151
+ ...(options.sanity ? [{ call: options.sanity }] : Bgp.sanity()),
152
+ ...(options.sources ?? []).map((source) => ({ set: { preferred_source: source } })),
153
+ ...(options.local_pref !== undefined ? [{ set: { local_pref: options.local_pref } }] : []),
154
+ ...(tag === undefined ? [] : [typeof tag === "object" ? tag : Bgp.adding(tag)]),
155
+ { action: "accept" },
156
+ ];
157
+ }
158
+
159
+ /**
160
+ * Import rules that take the default route, `0.0.0.0/0` and `::/0`, from a transit. Any other route reaches the next rule, or is rejected when there is none. Each of `sources` is the source address for the default of its own family.
161
+ */
162
+ static defaultOnly(options: DefaultOnlyOptions = {}): PolicyRule<never, never>[] {
163
+ return Bgp.DEFAULTS.map(({ family, prefix }) => {
164
+ const source = options.sources?.find((address) => Address.family(address) === family);
165
+ const set = {
166
+ ...(options.local_pref !== undefined ? { local_pref: options.local_pref } : {}),
167
+ ...(source ? { preferred_source: source } : {}),
168
+ };
169
+
170
+ return {
171
+ description: options.description ?? `Default route, ${family}`,
172
+ match: { prefix },
173
+ ...(Object.keys(set).length ? { set } : {}),
174
+ action: "accept" as const,
175
+ };
176
+ });
177
+ }
178
+
179
+ /** A rule adding one community, standard or large. */
180
+ private static adding(community: StandardCommunity | LargeCommunity): PolicyRule<never, never> {
181
+ const large = community.split(":").length === 3;
182
+ return {
183
+ description: `Tag ${community}`,
184
+ set: large ? { add_large_communities: [community as LargeCommunity] } : { add_communities: [community as StandardCommunity] },
185
+ };
186
+ }
187
+
86
188
  /** Each BGP neighbor's own policies and role, or its group's. */
87
189
  static neighborPolicies(device: Device) {
88
190
  const bgp = device.routing?.bgp;