@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.
@@ -1,5 +1,5 @@
1
1
  import type { Community } from "../core/define.js";
2
- import type { Device, PolicyRule, Prefix, StandardCommunity } from "../schema/index.js";
2
+ import type { Device, IP, LargeCommunity, PolicyRule, Prefix, StandardCommunity } from "../schema/index.js";
3
3
  /** The prefix lengths a BGP import accepts. */
4
4
  export type SanityLengths = {
5
5
  /** Shortest and longest IPv4 prefix accepted. /8 to /24 when absent. */
@@ -13,6 +13,33 @@ export type SanityLengths = {
13
13
  max: number;
14
14
  };
15
15
  };
16
+ /** A prefix we announce, and what it is. */
17
+ export type Announced = {
18
+ prefix: Prefix;
19
+ description?: string;
20
+ };
21
+ /** What `Bgp.learn` builds an import from. */
22
+ export type LearnOptions<P extends string> = {
23
+ /** Rules of your own, first. */
24
+ before?: readonly PolicyRule<P, never>[];
25
+ /** A policy holding `Bgp.sanity()` to call. The sanity rules are written in the import itself when absent. */
26
+ sanity?: P;
27
+ /** The source address for routes of its family, one per family. */
28
+ sources?: readonly IP[];
29
+ /** Preference for the routes learned. Higher wins. */
30
+ local_pref?: number;
31
+ /** Where the route came from: a community to add, or a rule, such as `communityScheme(...).tag(...)`. */
32
+ tag?: StandardCommunity | LargeCommunity | PolicyRule<never, never>;
33
+ };
34
+ /** What `Bgp.defaultOnly` sets on the default routes it takes. */
35
+ export type DefaultOnlyOptions = {
36
+ /** Preference for the default route. Higher wins. */
37
+ local_pref?: number;
38
+ /** Shown beside each rule. `Default route, ipv4` and `Default route, ipv6` when absent. */
39
+ description?: string;
40
+ /** The source address for the default of its family, one per family. */
41
+ sources?: readonly IP[];
42
+ };
16
43
  /** Kinds of neighbor and the number each is tagged with, such as `{ transit: 1, customer: 3 }`. */
17
44
  export type CommunityClasses = Readonly<Record<string, number>>;
18
45
  /**
@@ -55,15 +82,31 @@ export type CommunityScheme<Asn extends number = number, LearnedFrom extends num
55
82
  };
56
83
  /** The class names a scheme declares, or any name when it declares none. */
57
84
  type ClassName<Classes extends CommunityClasses> = {} extends Classes ? string : Extract<keyof Classes, string>;
58
- /** 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. */
59
86
  export declare class Bgp {
60
87
  private static readonly PREPENDS;
88
+ /** The default route of each family. */
89
+ private static readonly DEFAULTS;
61
90
  /** 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. */
62
91
  static sanity(lengths?: SanityLengths): PolicyRule<never, never>[];
92
+ /** 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. */
93
+ static announce(prefixes: readonly Announced[]): PolicyRule<never, never>[];
94
+ /**
95
+ * 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.
96
+ *
97
+ * `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.
98
+ */
99
+ static learn<const P extends string = never>(options?: LearnOptions<P>): PolicyRule<P, never>[];
100
+ /**
101
+ * 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.
102
+ */
103
+ static defaultOnly(options?: DefaultOnlyOptions): PolicyRule<never, never>[];
104
+ /** A rule adding one community, standard or large. */
105
+ private static adding;
63
106
  /** Each BGP neighbor's own policies and role, or its group's. */
64
107
  static neighborPolicies(device: Device): {
65
108
  name: string;
66
- address: import("../index.js").IP;
109
+ address: IP;
67
110
  role: import("../index.js").BgpRole | undefined;
68
111
  import: string | undefined;
69
112
  export: string | undefined;
@@ -0,0 +1,70 @@
1
+ import { type ModelName, type PlatformName, type PortName } from "../platforms/catalog.js";
2
+ /** One end of a cable: the device, and its platform and model, which name its ports. */
3
+ export type CableEnd<P extends PlatformName, M extends ModelName<P>> = {
4
+ device: string;
5
+ platform: P;
6
+ model: M;
7
+ };
8
+ /** The far end of a port, for its description: the device, the port by Circuit's name, and by the name printed on the device. */
9
+ export type FarEnd = {
10
+ device: string;
11
+ port: string;
12
+ label: string;
13
+ };
14
+ /** What every cable of one entry shares. */
15
+ type Shared = {
16
+ /** What is between the two ends, such as a carrier's local loop. Both ends get it on their `link`. */
17
+ through?: string;
18
+ /** A port's description from its far end. The far device and the far port's label when absent, such as `firewall-1 port6`. */
19
+ describe?: (far: FarEnd) => string;
20
+ };
21
+ /** A port a cable builds: what it is, the LAG it is in, and the other end. */
22
+ type CablePort<L extends string> = {
23
+ description: string;
24
+ link: {
25
+ device: string;
26
+ port: string;
27
+ through?: string;
28
+ };
29
+ lag?: L;
30
+ };
31
+ /** Ports for both ends of the cables between two of our devices, each cable written once. */
32
+ export declare class Cables {
33
+ /**
34
+ * One cable, both ends. Spread each end into its device's `ports`:
35
+ *
36
+ * ```ts
37
+ * const uplink = Cables.link({ a: { device: "router-1", platform: "routeros", model: "CCR2216-1G-12XS-2XQ", port: "25g-1" }, b: { ... } });
38
+ * // router-1: ports: { ...uplink.a }
39
+ * ```
40
+ */
41
+ static link<const PA extends PlatformName, const MA extends ModelName<PA>, const NA extends PortName<PA, MA>, const PB extends PlatformName, const MB extends ModelName<PB>, const NB extends PortName<PB, MB>>(spec: Shared & {
42
+ a: CableEnd<PA, MA> & {
43
+ port: NA;
44
+ };
45
+ b: CableEnd<PB, MB> & {
46
+ port: NB;
47
+ };
48
+ }): {
49
+ a: Record<NA, CablePort<never>>;
50
+ b: Record<NB, CablePort<never>>;
51
+ };
52
+ /**
53
+ * The cables of a LAG, both ends, from one list read from end A: `{ "25g-1": "10g-2" }` is a cable from A's 25g-1 to B's 10g-2. Each port is a member of its end's LAG. Spread each end into its device's `ports`; the LAG itself stays on each device.
54
+ */
55
+ static lag<const PA extends PlatformName, const MA extends ModelName<PA>, const LA extends string, const PB extends PlatformName, const MB extends ModelName<PB>, const LB extends string, const C extends Partial<Record<PortName<PA, MA>, PortName<PB, MB>>>>(spec: Shared & {
56
+ a: CableEnd<PA, MA> & {
57
+ lag: LA;
58
+ };
59
+ b: CableEnd<PB, MB> & {
60
+ lag: LB;
61
+ };
62
+ cables: C;
63
+ }): {
64
+ a: Record<keyof C & string, CablePort<LA>>;
65
+ b: Record<NonNullable<C[keyof C]> & string, CablePort<LB>>;
66
+ };
67
+ /** A port facing `port` on the far device. */
68
+ private static end;
69
+ }
70
+ export {};
@@ -1,10 +1,14 @@
1
1
  export { Bgp } from "./bgp.js";
2
- export type { CommunityScheme, SanityLengths } from "./bgp.js";
2
+ export type { Announced, CommunityScheme, DefaultOnlyOptions, LearnOptions, SanityLengths } from "./bgp.js";
3
+ export { Cables } from "./cables.js";
4
+ export type { CableEnd, FarEnd } from "./cables.js";
3
5
  export { Checks } from "./checks.js";
4
6
  export type { TrustBoundary } from "./checks.js";
5
7
  export { Exchange } from "./exchange.js";
6
8
  export type { ExchangeMember, InternetExchange } from "./exchange.js";
7
9
  export { Filters } from "./filters.js";
8
10
  export { RouteServerConfig } from "./route-server.js";
11
+ export { Upstreams } from "./upstreams.js";
12
+ export type { Upstream, UpstreamCatalog, UpstreamPolicies } from "./upstreams.js";
9
13
  export type { RouteServer } from "./route-server.js";
10
14
  export type { AntiAmplificationOptions } from "./filters.js";
@@ -0,0 +1,156 @@
1
+ import type { Exchange as ExchangeLan, Family, FilterRule, Gre, InterfaceAddress, IP, LargeCommunity, Neighbor, NeighborSettings, PolicyRule, Port, Secret, StandardCommunity, Vlan, VlanInterface } from "../schema/index.js";
2
+ import { type Announced } from "./bgp.js";
3
+ /** One network we exchange routes with: a transit we buy, an exchange's route servers, or a peer. */
4
+ export type Upstream = {
5
+ /** Its name as people write it, for descriptions: `Transit A`. The key when absent. */
6
+ name?: string;
7
+ asn: number;
8
+ /** What it is to us: `provider` for a transit, `rs` for an exchange's route servers, `peer` for a direct session. */
9
+ role: "provider" | "rs" | "peer";
10
+ /** Where it lands on our side: a VLAN, our addresses on it, and the switch port it arrives on. A peer over a tunnel, or over an exchange's VLAN, has none. */
11
+ handoff?: {
12
+ /** The VLAN's ID. */
13
+ vlan: number;
14
+ /** It sends its VLAN tagged: its port gets a native VLAN of its own with this ID, which nothing else is in. Untagged when absent. */
15
+ native?: number;
16
+ /** Our addresses on it. `{ address, peer }` for a point-to-point address whose far end is outside its own prefix. */
17
+ addresses: readonly (InterfaceAddress | {
18
+ address: InterfaceAddress;
19
+ peer: IP;
20
+ })[];
21
+ /** The switch port it arrives on. */
22
+ port?: {
23
+ device: string;
24
+ port: string;
25
+ storm_control?: Port<string, string, string, string>["storm_control"];
26
+ };
27
+ /** An exchange's LAN, for `circuit peeringdb`. */
28
+ exchange?: ExchangeLan;
29
+ };
30
+ /** A GRE tunnel to it. */
31
+ tunnel?: {
32
+ local: IP;
33
+ remote: IP;
34
+ addresses: readonly InterfaceAddress[];
35
+ mtu?: number;
36
+ };
37
+ /** Our BGP sessions with it, by name: its address, and ours when it is not the interface's. */
38
+ sessions: Readonly<Record<string, {
39
+ address: IP;
40
+ local_address?: IP;
41
+ }>>;
42
+ /** The sessions are more than one hop away, from this address of ours. They share a group. */
43
+ multihop?: {
44
+ local_address: IP;
45
+ families?: readonly Family[];
46
+ password?: Secret;
47
+ };
48
+ /** The most prefixes a session may send, by its family: about twice what it sends. Over it, the session closes until it is cleared. */
49
+ limits?: {
50
+ ipv4?: number;
51
+ ipv6?: number;
52
+ };
53
+ /** What we take. Nothing when absent. */
54
+ import?: {
55
+ /** Its default route, at this preference. */
56
+ default?: {
57
+ local_pref?: number;
58
+ description?: string;
59
+ };
60
+ /** Its routes, through `Bgp.learn`: the `before` rules, sanity, this preference, the tag. Only the default when absent. */
61
+ learn?: {
62
+ local_pref?: number;
63
+ before?: readonly PolicyRule<never, never>[];
64
+ };
65
+ };
66
+ /** Rules of yours in its export, between the community actions and what we announce, such as a prepend. */
67
+ export?: readonly PolicyRule<never, never>[];
68
+ /** What we announce to it. Everything else is rejected. */
69
+ announce: readonly Announced[];
70
+ /** Drop RFC 1918 sources arriving from it. `privateDrops()` lists the VLANs. */
71
+ drop_private?: true;
72
+ };
73
+ /** Upstreams by key. The key names everything built from it. */
74
+ export type UpstreamCatalog = Readonly<Record<string, Upstream>>;
75
+ /** How each upstream's policies are written: shared by every upstream of a catalog. */
76
+ export type UpstreamPolicies<P extends string> = {
77
+ /** A policy holding `Bgp.sanity()` that each import calls. The sanity rules are written in each import when absent. */
78
+ sanity?: P;
79
+ /** Where a route came from, by upstream: a community or a rule, such as `communityScheme(...).tag(...)`. Nothing is tagged when absent. */
80
+ tag?: (key: string, upstream: Upstream) => StandardCommunity | LargeCommunity | PolicyRule<never, never>;
81
+ /** Rules first in each export, such as `communityScheme(...).actions(upstream.asn)`. */
82
+ actions?: (key: string, upstream: Upstream) => readonly PolicyRule<never, never>[];
83
+ /** The source address for routes of its family, one per family. */
84
+ sources?: readonly IP[];
85
+ };
86
+ type Key<C> = keyof C & string;
87
+ /** The upstreams that land on a VLAN of ours. */
88
+ type Landed<C> = {
89
+ [K in Key<C>]: C[K] extends {
90
+ handoff: object;
91
+ } ? K : never;
92
+ }[Key<C>];
93
+ /** The upstreams that send their VLAN tagged. */
94
+ type Tagged<C> = {
95
+ [K in Key<C>]: C[K] extends {
96
+ handoff: {
97
+ native: number;
98
+ };
99
+ } ? K : never;
100
+ }[Key<C>];
101
+ /** The upstreams reached over a tunnel. */
102
+ type Tunneled<C> = {
103
+ [K in Key<C>]: C[K] extends {
104
+ tunnel: object;
105
+ } ? K : never;
106
+ }[Key<C>];
107
+ /** The upstreams whose sessions share a group: an exchange's route servers, or multihop sessions. */
108
+ type Grouped<C> = {
109
+ [K in Key<C>]: C[K] extends {
110
+ role: "rs";
111
+ } | {
112
+ multihop: object;
113
+ } ? K : never;
114
+ }[Key<C>];
115
+ /** The upstreams we take routes from. */
116
+ type Importing<C> = {
117
+ [K in Key<C>]: C[K] extends {
118
+ import: object;
119
+ } ? K : never;
120
+ }[Key<C>];
121
+ /** A native VLAN's name. */
122
+ type Native<K extends string> = `${K}-native`;
123
+ /** Each upstream's policies: `TRANSIT_A-IN` and `TRANSIT_A-OUT`. */
124
+ type PolicyName<C> = `${Uppercase<Importing<C>>}-IN` | `${Uppercase<Key<C>>}-OUT`;
125
+ /** The catalog as written, with any key the shape does not have refused, at every level. */
126
+ type Exact<S, T> = T extends readonly unknown[] | string | number | boolean | Secret ? T : {
127
+ [K in keyof T]: K extends keyof S ? Exact<NonNullable<S[K]>, T[K]> : never;
128
+ };
129
+ /** A network's upstreams written once, and each device's part built from them. */
130
+ export declare class Upstreams {
131
+ /**
132
+ * Each upstream written once: its AS, where it lands, its sessions, limits and policy. Everything a device needs is built from it, under names made from each key, so a device that names one is checked:
133
+ *
134
+ * - VLAN `<key>`, and `<key>-native` for one that sends its VLAN tagged. The router's interface is `<key>` too, a VLAN interface or a GRE tunnel.
135
+ * - Policies `<KEY>-IN` and `<KEY>-OUT`; a BGP group `<key>` for an exchange's route servers or multihop sessions.
136
+ *
137
+ * On the router: `vlans()`, `interfaces()`, `groups()`, `neighbors()`, `policies({ ... })`, `tunnelRules()`, `addresses()`. On a switch: `vlans(device)`, `ports(device)`, `vlansOn(device)`. For checks and ACLs: `untrusted()`, `natives()`, `privateDrops()`. Lists follow the catalog's order, where order counts on a device.
138
+ */
139
+ static define<const C extends UpstreamCatalog>(catalog: C & {
140
+ [K in keyof C]: Exact<Upstream, C[K]>;
141
+ }): {
142
+ vlans: <const D extends string | undefined = undefined>(device?: D) => D extends string ? { [K in Landed<C>]: Vlan; } & { [K in Tagged<C> as Native<K>]: Vlan; } : { [K in Landed<C>]: Vlan; };
143
+ vlansOn: (device: string) => Landed<C>[];
144
+ ports: (device: string) => Record<string, Port<`${Tagged<C>}-native` | Landed<C>, never, never, never>>;
145
+ interfaces: () => { [K in Landed<C>]: VlanInterface<Landed<C>, never>; } & { [K in Tunneled<C>]: Gre<never>; };
146
+ groups: () => Record<Grouped<C>, NeighborSettings<PolicyName<C>>>;
147
+ neighbors: () => Record<string, Neighbor<PolicyName<C>, Grouped<C>>>;
148
+ policies: <const P extends string = never>(options?: UpstreamPolicies<P>) => Record<PolicyName<C>, PolicyRule<P, never>[]>;
149
+ untrusted: () => (`${Tagged<C>}-native` | Landed<C>)[];
150
+ natives: () => `${Tagged<C>}-native`[];
151
+ privateDrops: () => Landed<C>[];
152
+ tunnelRules: () => FilterRule<never, never>[];
153
+ addresses: () => IP[];
154
+ };
155
+ }
156
+ export {};
@@ -15,4 +15,10 @@ export declare class BgpWait {
15
15
  static until(read: () => Promise<BgpSession[]>, enough: (sessions: BgpSession[]) => boolean, limits: WaitLimits, waiting?: (sessions: BgpSession[]) => void): Promise<BgpSession[]>;
16
16
  /** How many of these sessions are established. */
17
17
  static upCount(sessions: readonly BgpSession[]): number;
18
+ /** Sessions closed on their limit within this many minutes of each other are one burst. */
19
+ static readonly BURST_MINUTES = 5;
20
+ /**
21
+ * Groups of two or more sessions closed on their limit within `BURST_MINUTES` of each other, earliest first. Several limits at once is usually a leak upstream, not growth, so raising them is the wrong fix.
22
+ */
23
+ static limitBursts(sessions: readonly BgpSession[]): BgpSession[][];
18
24
  }
@@ -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
  */
@@ -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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "0.37.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",
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. */
@@ -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;