@takodotid/circuit 0.41.0 → 0.42.1

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 (50) hide show
  1. package/dist/chunks/index-6wb41q67.js +62 -0
  2. package/dist/chunks/{index-k38bby43.js → index-yzw86rc8.js} +4 -62
  3. package/dist/chunks/{server-ggertnce.js → server-cxr9ynxc.js} +1 -1
  4. package/dist/cli/index.js +226 -55
  5. package/dist/helpers/index.js +14 -9
  6. package/dist/types/core/cabling.d.ts +0 -6
  7. package/dist/types/core/define.d.ts +1 -1
  8. package/dist/types/core/validator.d.ts +2 -0
  9. package/dist/types/helpers/cables.d.ts +14 -11
  10. package/dist/types/helpers/exchange.d.ts +7 -1
  11. package/dist/types/helpers/upstreams.d.ts +11 -3
  12. package/dist/types/platforms/bgp-wait.d.ts +2 -2
  13. package/dist/types/platforms/derive.d.ts +1 -1
  14. package/dist/types/platforms/fortios/routing.d.ts +2 -2
  15. package/dist/types/platforms/junos/bgp.d.ts +2 -2
  16. package/dist/types/platforms/platform.d.ts +3 -0
  17. package/dist/types/platforms/routeros/index.d.ts +14 -1
  18. package/dist/types/platforms/routeros/plan.d.ts +4 -0
  19. package/dist/types/platforms/routeros/render.d.ts +4 -0
  20. package/dist/types/schema/device.d.ts +4 -4
  21. package/dist/types/schema/interfaces.d.ts +1 -1
  22. package/dist/types/schema/routing.d.ts +43 -10
  23. package/package.json +1 -1
  24. package/src/cli/index.ts +26 -29
  25. package/src/core/cabling.ts +0 -30
  26. package/src/core/define.ts +3 -1
  27. package/src/core/validator.ts +64 -1
  28. package/src/helpers/cables.ts +20 -15
  29. package/src/helpers/exchange.ts +9 -3
  30. package/src/helpers/upstreams.ts +9 -4
  31. package/src/platforms/bgp-wait.ts +11 -11
  32. package/src/platforms/derive.ts +23 -1
  33. package/src/platforms/fortios/routing.ts +2 -2
  34. package/src/platforms/junos/bgp.ts +2 -2
  35. package/src/platforms/platform.ts +3 -0
  36. package/src/platforms/routeros/TRAPS.md +6 -0
  37. package/src/platforms/routeros/export.ts +1 -0
  38. package/src/platforms/routeros/index.ts +133 -4
  39. package/src/platforms/routeros/plan.ts +20 -1
  40. package/src/platforms/routeros/render.ts +28 -3
  41. package/src/schema/device.ts +4 -3
  42. package/src/schema/interfaces.ts +1 -1
  43. package/src/schema/routing.ts +46 -10
  44. package/templates/colocation/checks.ts +14 -11
  45. package/templates/colocation/edge-01.ts +2 -2
  46. package/templates/colocation/tenancy.ts +50 -71
  47. package/templates/colocation/tenants/acme.ts +10 -10
  48. package/templates/colocation/tenants/index.ts +2 -1
  49. package/templates/colocation/tor-01.ts +2 -2
  50. package/templates/project/AGENTS.md +15 -15
@@ -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";
@@ -238,8 +241,8 @@ class Bgp {
238
241
  class Cables {
239
242
  static link(spec) {
240
243
  return {
241
- a: { [spec.a.port]: Cables.end(spec, spec.b.device, spec.b.port) },
242
- b: { [spec.b.port]: Cables.end(spec, spec.a.device, spec.a.port) }
244
+ a: { [spec.a.port]: Cables.end(spec, spec.b, spec.b.port) },
245
+ b: { [spec.b.port]: Cables.end(spec, spec.a, spec.a.port) }
243
246
  };
244
247
  }
245
248
  static lag(spec) {
@@ -251,17 +254,18 @@ class Cables {
251
254
  throw new Error(`Cables.lag: ${spec.b.device} ${far} is cabled to both ${other} and ${near} on ${spec.a.device}`);
252
255
  taken.set(far, near);
253
256
  }
254
- const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b.device, far), lag: spec.a.lag }]));
255
- const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a.device, near), lag: spec.b.lag }]));
257
+ const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b, far), lag: spec.a.lag }]));
258
+ const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a, near), lag: spec.b.lag }]));
256
259
  return {
257
260
  a,
258
261
  b
259
262
  };
260
263
  }
261
- static end(shared, device, port) {
264
+ static end(shared, far, port) {
265
+ const label = far.platform && far.model ? Catalog.spell(far.platform, far.model, port) : undefined;
262
266
  return {
263
- link: { device, port, ...shared.through ? { through: shared.through } : {} },
264
- ...shared.describe ? { description: shared.describe({ device, port }) } : {}
267
+ link: { device: far.device, port, ...shared.through ? { through: shared.through } : {} },
268
+ ...shared.describe ? { description: shared.describe({ device: far.device, port, ...label ? { label } : {} }) } : {}
265
269
  };
266
270
  }
267
271
  }
@@ -560,8 +564,8 @@ class Exchange {
560
564
  result[port] = {
561
565
  description: `${member.name}, AS${member.asn}`,
562
566
  access_vlan: exchange.vlan,
563
- stp: false,
564
- lldp: false,
567
+ stp: exchange.stp ?? false,
568
+ lldp: exchange.lldp ?? false,
565
569
  storm_control: { broadcast: { percent }, multicast: { percent }, unknown_unicast: { percent } },
566
570
  ...exchange.mac_limit !== undefined ? { mac_limit: exchange.mac_limit } : {},
567
571
  ...exchange.ethertypes ? { ethertypes: exchange.ethertypes } : {}
@@ -767,6 +771,7 @@ class Upstreams {
767
771
  role: upstream.role,
768
772
  ...upstream.import ? { import: upstream.import.name } : {},
769
773
  export: upstream.export.name,
774
+ ...upstream.table ? { table: upstream.table } : {},
770
775
  ...multihop ? {
771
776
  local_address: multihop.local_address,
772
777
  multihop: true,
@@ -31,12 +31,6 @@ export declare class Cabling {
31
31
  * 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.
32
32
  */
33
33
  static compare(devices: readonly Device[], neighbors: ReadonlyMap<string, readonly Neighbor[]>): Finding[];
34
- /**
35
- * Each linked port without a description gets one from its far end: the far device and the far port as printed on it, such as `firewall-1 port6`. The far device's own file has its platform and model, so the label is never written twice. A port whose far device is not in the network keeps none.
36
- */
37
- static describeLinks(devices: readonly Device[]): Device[];
38
- /** A port as printed on the device, or Circuit's name for it when the model has no spelling for it. */
39
- private static labelOf;
40
34
  /** A system name is the device's name, alone or with a domain after it. */
41
35
  private static isDevice;
42
36
  private static deviceOf;
@@ -9,7 +9,7 @@ export declare const secretFile: (path: string) => Secret;
9
9
  *
10
10
  * The names it declares (VLANs, interfaces, policies and the rest) are the only names its other fields accept, and the editor suggests them. The port names come from the model.
11
11
  */
12
- export declare function defineDevice<const Platform extends PlatformName, const Model extends ModelName<Platform>, const Vlans extends string = never, const Interfaces extends string = never, const Policies extends string = never, const PrefixSets extends string = never, const BgpGroups extends string = never, const Acls extends string = never, const AddressSets extends string = never, const Users extends string = never, const Vrfs extends string = never, const Certificates extends string = never>(device: Device<Platform, Model, PortName<Platform, Model>, Vlans, Interfaces, Policies, PrefixSets, BgpGroups, Acls, AddressSets, Users, Vrfs, Certificates>): Device;
12
+ export declare function defineDevice<const Platform extends PlatformName, const Model extends ModelName<Platform>, const Vlans extends string = never, const Interfaces extends string = never, const Policies extends string = never, const PrefixSets extends string = never, const BgpGroups extends string = never, const Acls extends string = never, const AddressSets extends string = never, const Users extends string = never, const Vrfs extends string = never, const Certificates extends string = never, const Tables extends string = never>(device: Device<Platform, Model, PortName<Platform, Model>, Vlans, Interfaces, Policies, PrefixSets, BgpGroups, Acls, AddressSets, Users, Vrfs, Certificates, Tables>): Device;
13
13
  /** Something a check found. */
14
14
  export type Finding = {
15
15
  /** An error stops `diff` and `apply`; a warning is reported only. */
@@ -14,6 +14,8 @@ export declare class Validator {
14
14
  private static checkFrames;
15
15
  private static checkInterfaces;
16
16
  private static checkRouting;
17
+ /** Routing tables have names of their own and something installing routes in them, and rules name a table and can each be reached. */
18
+ private static checkTables;
17
19
  private static checkPolicies;
18
20
  private static checkFirewall;
19
21
  private static checkServices;
@@ -1,13 +1,20 @@
1
- /** The far end of a port, for a description of your own: the device and its port by Circuit's name. */
1
+ /** The far end of a port, for a description of your own: the device, its port by Circuit's name, and the port as printed on the device when that end names its platform and model. */
2
2
  export type FarEnd = {
3
3
  device: string;
4
4
  port: string;
5
+ label?: string;
6
+ };
7
+ /** One end of a cable entry: the device, and its platform and model if a description should name its ports as printed on it. */
8
+ type End = {
9
+ device: string;
10
+ platform?: string;
11
+ model?: string;
5
12
  };
6
13
  /** What every cable of one entry shares. */
7
14
  type Shared = {
8
15
  /** What is between the two ends, such as a carrier's local loop. Both ends get it on their `link`. */
9
16
  through?: string;
10
- /** A port's description from its far end. When absent, Circuit writes the far device and the far port as printed on it, such as `firewall-1 port6`, from the far device's own file. */
17
+ /** A port's description from its far end, such as `(far) => \`${far.device} ${far.label}\``. No description when absent. */
11
18
  describe?: (far: FarEnd) => string;
12
19
  };
13
20
  /** A port a cable builds: the other end, the LAG it is in, and a description when `describe` gives one. */
@@ -31,12 +38,10 @@ export declare class Cables {
31
38
  * ```
32
39
  */
33
40
  static link<const NA extends string, const NB extends string>(spec: Shared & {
34
- a: {
35
- device: string;
41
+ a: End & {
36
42
  port: NA;
37
43
  };
38
- b: {
39
- device: string;
44
+ b: End & {
40
45
  port: NB;
41
46
  };
42
47
  }): {
@@ -47,12 +52,10 @@ export declare class Cables {
47
52
  * 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.
48
53
  */
49
54
  static lag<const LA extends string, const LB extends string, const C extends Readonly<Record<string, string>>>(spec: Shared & {
50
- a: {
51
- device: string;
55
+ a: End & {
52
56
  lag: LA;
53
57
  };
54
- b: {
55
- device: string;
58
+ b: End & {
56
59
  lag: LB;
57
60
  };
58
61
  cables: C;
@@ -60,7 +63,7 @@ export declare class Cables {
60
63
  a: Record<keyof C & string, CablePort<LA>>;
61
64
  b: Record<C[keyof C], CablePort<LB>>;
62
65
  };
63
- /** A port facing `port` on the far device. */
66
+ /** A port facing `port` on the far end. */
64
67
  private static end;
65
68
  }
66
69
  export {};
@@ -29,13 +29,19 @@ export type InternetExchange<V extends string> = {
29
29
  mac_limit?: number;
30
30
  /** The only kinds of frame a member port takes in, usually `ipv4`, `ipv6` and `arp`. Every kind when absent. No platform renders it yet, so `validate` refuses it. */
31
31
  ethertypes?: readonly Ethertype[];
32
+ /** LLDP on member ports. Off when absent: a member's router has no business learning the fabric. */
33
+ lldp?: boolean;
34
+ /** Spanning tree on member ports. Off when absent: a member's port faces a router, which must not join the fabric's spanning tree. */
35
+ stp?: boolean;
32
36
  };
33
37
  /** An internet exchange's fabric: its member ports, a check of its members, and its route servers' config. */
34
38
  export declare class Exchange {
35
39
  /**
36
40
  * Member ports and a check for an internet exchange's fabric.
37
41
  *
38
- * - `ports(device)`: every member's port on one switch. A member port is in the peering VLAN and nothing else, takes no part in spanning tree or LLDP, and lets only a trickle of broadcast, multicast and unknown unicast through. With `mac_limit` and `ethertypes`, it also learns that many MAC addresses at most and takes in only those kinds of frame.
42
+ * - `ports(device)`: every member's port on one switch, each written as
43
+ * `{ description: "<name>, AS<asn>", access_vlan: vlan, stp: false, lldp: false, storm_control: { broadcast, multicast, unknown_unicast: { percent: storm_percent } } }`,
44
+ * plus `mac_limit` and `ethertypes` when the exchange sets them. `stp` and `lldp` are the exchange's own when it sets them, and storm control is 1 % when `storm_percent` is absent.
39
45
  * - `check`: members never share a name, an AS, an address or a port; every address is inside the peering LAN and not reserved; every member port carries the peering VLAN alone.
40
46
  * - `bird(server)`: one route server's BIRD 2 config, with a session to every member. Write it to a file for the server; Circuit never logs in to a route server.
41
47
  * - `openbgpd(server)`: the same route server's config for OpenBGPD, its `bgpd.conf`, with the same sessions and the same rules.
@@ -59,7 +59,7 @@ export type Upstream = {
59
59
  address: IP;
60
60
  local_address?: IP;
61
61
  }>>;
62
- /** A BGP group its sessions share, by this name: the group holds the AS, role, policies and multihop settings, and each session names it. Each session holds them itself when absent. */
62
+ /** A BGP group its sessions share, by this name: the group holds the AS, role, policies, table and multihop settings, and each session names it. Each session holds them itself when absent. */
63
63
  group?: string;
64
64
  /** The sessions are more than one hop away, from this address of ours. */
65
65
  multihop?: {
@@ -67,6 +67,8 @@ export type Upstream = {
67
67
  families?: readonly Family[];
68
68
  password?: Secret;
69
69
  };
70
+ /** The routing table its sessions install the routes they learn in, one of the router's `routing.tables`: a transit that carries only one block's traffic out. The main table when absent. On RouterOS the sessions also announce from that table, so what they announce needs a route there too. */
71
+ table?: string;
70
72
  /** The most prefixes a session may send, by its family: about twice what it sends. Over it, the session closes until it is cleared. */
71
73
  limits?: {
72
74
  ipv4?: number;
@@ -143,6 +145,12 @@ type GroupName<C> = Values<{
143
145
  group: infer N extends string;
144
146
  } ? N : never;
145
147
  }>;
148
+ /** Each routing table name. */
149
+ type TableName<C> = Values<{
150
+ [K in keyof C]: C[K] extends {
151
+ table: infer N extends string;
152
+ } ? N : never;
153
+ }>;
146
154
  /** A network's upstreams written once, and each device's part built from them. */
147
155
  export declare class Upstreams {
148
156
  /**
@@ -160,8 +168,8 @@ export declare class Upstreams {
160
168
  ports: (device: string) => Record<string, Port<NativeName<C> | VlanName<C>, never, never, never>>;
161
169
  interfaces: () => { [K in HandoffInterface<C>]: VlanInterface<VlanName<C>, never>; } & { [K in TunnelInterface<C>]: Gre<never>; };
162
170
  uplinks: () => HandoffInterface<C>[];
163
- groups: () => Record<GroupName<C>, NeighborSettings<PolicyName<C>>>;
164
- neighbors: () => Record<string, Neighbor<PolicyName<C>, GroupName<C>>>;
171
+ groups: () => Record<GroupName<C>, NeighborSettings<PolicyName<C>, TableName<C>>>;
172
+ neighbors: () => Record<string, Neighbor<PolicyName<C>, GroupName<C>, TableName<C>>>;
165
173
  policies: () => Record<PolicyName<C>, PolicyRule<never, never>[]>;
166
174
  untrusted: () => (NativeName<C> | VlanName<C>)[];
167
175
  natives: () => NativeName<C>[];
@@ -11,8 +11,8 @@ export declare class BgpWait {
11
11
  * After an apply, the sessions established before have 180 seconds to be established again, read every 10 seconds. A session reset by a change opens again when one side retries; three minutes leaves room for a slow peer without holding the apply up for long. A session that never went down is seen at the first read, so a healthy apply does not wait.
12
12
  */
13
13
  static readonly AFTER_APPLY: WaitLimits;
14
- /** Read the sessions until `enough` holds or time runs out, and give the last read. `waiting` is told of each read that fell short, before the pause. */
15
- static until(read: () => Promise<BgpSession[]>, enough: (sessions: BgpSession[]) => boolean, limits: WaitLimits, waiting?: (sessions: BgpSession[]) => void): Promise<BgpSession[]>;
14
+ /** Read until `enough` holds or time runs out, and give the last read: the sessions, or what they announce. `waiting` is told of each read that fell short, before the pause. */
15
+ static until<T>(read: () => Promise<T>, enough: (value: T) => boolean, limits: WaitLimits, waiting?: (value: T) => void): Promise<T>;
16
16
  /** How many of these sessions are established. */
17
17
  static upCount(sessions: readonly BgpSession[]): number;
18
18
  /** Sessions closed on their limit within this many minutes of each other are one burst. */
@@ -1,6 +1,6 @@
1
1
  import type { Device } from "../schema/index.js";
2
2
  /** A feature a platform renders once proved on one of its devices. */
3
- export type ProvedFeature = "bfd-timers" | "static-route-bfd" | "multihop-bfd" | "alarm-severity" | "mac-limit" | "ethertype-filter" | "management-vdom";
3
+ export type ProvedFeature = "bfd-timers" | "static-route-bfd" | "multihop-bfd" | "alarm-severity" | "mac-limit" | "ethertype-filter" | "management-vdom" | "routing-tables";
4
4
  export declare class Derive {
5
5
  /** Each LAG's number: its own `id`, or its position among the device's LAGs. */
6
6
  static lagIds(device: Device): Map<string, number>;
@@ -1,7 +1,7 @@
1
1
  import type { Device, Family, Neighbor, NeighborSettings, PolicyRule } from "../../schema/index.js";
2
2
  import { FortiOsBlock } from "./tree.js";
3
3
  type AnyRule = PolicyRule<string, string>;
4
- type AnyNeighbor = Neighbor<string, string>;
4
+ type AnyNeighbor = Neighbor<string, string, string>;
5
5
  /** The lengths a prefix list's rule takes within its prefix. */
6
6
  type Lengths = {
7
7
  ge?: number;
@@ -23,7 +23,7 @@ export declare class FortiOsRouting {
23
23
  readonly ipv6: 128;
24
24
  };
25
25
  /** A neighbor's settings with its group's beneath them. */
26
- static neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string>;
26
+ static neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string, string>;
27
27
  /** The families a session carries: its own, or the family of its address. */
28
28
  static familiesOfNeighbor(device: Device, neighbor: AnyNeighbor): Family[];
29
29
  /** Whether a static route, OSPF or BGP detects a dead neighbor with BFD, which the device then runs. */
@@ -1,6 +1,6 @@
1
1
  import type { Device, Neighbor, NeighborSettings } from "../../schema/index.js";
2
2
  import { type JunosStatements } from "./render.js";
3
- type AnyNeighbor = Neighbor<string, string>;
3
+ type AnyNeighbor = Neighbor<string, string, string>;
4
4
  /** BGP sessions, the policies they use, prefix sets and RPKI servers, added to one device's statements. */
5
5
  export declare class JunosBgp {
6
6
  private readonly out;
@@ -19,7 +19,7 @@ export declare class JunosBgp {
19
19
  /** Add the device's prefix sets, RPKI servers, BGP sessions and the policies they use. */
20
20
  static render(out: JunosStatements, device: Device): void;
21
21
  /** A neighbor's settings with its group's beneath them. */
22
- static settingsOf(device: Device, neighbor: AnyNeighbor): NeighborSettings<string>;
22
+ static settingsOf(device: Device, neighbor: AnyNeighbor): NeighborSettings<string, string>;
23
23
  /** A name made of a value's words, for a community or a path-length list: `64500:*` becomes `64500-any`. */
24
24
  private static slug;
25
25
  /** A community as the platform writes a member: a large community carries the `large:` prefix. */
@@ -1,4 +1,5 @@
1
1
  import type { Neighbor } from "../core/cabling.js";
2
+ import type { Finding } from "../core/define.js";
2
3
  import type { Device, Family } from "../schema/index.js";
3
4
  import type { ConsoleDialect } from "../transport/console.js";
4
5
  import type { WaitLimits } from "./bgp-wait.js";
@@ -101,6 +102,8 @@ export declare abstract class Platform {
101
102
  readonly console: ConsoleDialect | undefined;
102
103
  /** Fields this platform cannot express. Validation fails on any of them, so nothing is skipped silently. */
103
104
  abstract unsupported(device: Device): string[];
105
+ /** What the platform does with a config that it may not do elsewhere, each as `validate` reports it: an error stops `diff` and `apply`, a warning is reported only. Absent on a platform with none. */
106
+ checks?(device: Device): Finding[];
104
107
  /** The switch rules the render makes from the device's hardware ACLs, so `validate` can hold them to the model's limits. Absent on a platform whose rules are not counted. */
105
108
  switchRules?(device: Device): SwitchRules;
106
109
  /** The whole configuration the device should run. Secrets appear as markers, never values. */
@@ -1,4 +1,5 @@
1
1
  import type { Neighbor } from "../../core/cabling.js";
2
+ import type { Finding } from "../../core/define.js";
2
3
  import type { Device, Family } from "../../schema/index.js";
3
4
  import { type ConsoleDialect } from "../../transport/console.js";
4
5
  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";
@@ -18,6 +19,8 @@ export declare class RouterOs extends Platform {
18
19
  */
19
20
  private static readonly LIST_BGP_SESSIONS;
20
21
  /** Open again the session from one address, only while it is closed on its limit. */
22
+ /** Each advertisement of these prefixes, one line each: `ADV|<session>|<prefix>`. One line, because a script sent over exec runs as one. */
23
+ private static listAdvertisements;
21
24
  private static clearLimit;
22
25
  /**
23
26
  * 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.
@@ -41,6 +44,10 @@ export declare class RouterOs extends Platform {
41
44
  readonly os: OsSupport;
42
45
  readonly extension = ".rsc";
43
46
  readonly console: ConsoleDialect;
47
+ /**
48
+ * A BGP session with a `table` announces from that table, not from main: each prefix of `networks` it announces needs a route in the table too, or it is not announced (TRAPS.md). A session left with nothing to announce is an error; a prefix missing from its table is a warning, since its export may not announce it anyway.
49
+ */
50
+ checks(device: Device): Finding[];
44
51
  unsupported(device: Device): string[];
45
52
  render(device: Device): string;
46
53
  switchRules(device: Device): SwitchRules;
@@ -63,7 +70,7 @@ export declare class RouterOs extends Platform {
63
70
  /** 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. */
64
71
  bgpLog(session: ReadSession, minutes: number): Promise<BgpLogLine[]>;
65
72
  /**
66
- * Two checks after an apply. When the plan touched switch rules, none may be invalid. BGP sessions established before the apply must be established again, within `BgpWait.AFTER_APPLY`.
73
+ * Three checks after an apply. When the plan touched switch rules, none may be invalid. BGP sessions established before the apply must be established again, and announce again each of the device's `networks` they announced before, within `BgpWait.AFTER_APPLY`. A session can stay up and stop announcing (TRAPS.md), which the session check alone would not see.
67
74
  */
68
75
  health(device: Device, plan: Plan, options?: HealthOptions): Promise<HealthCheck>;
69
76
  /** The LLDP neighbors the device sees, by Circuit's port names. */
@@ -80,6 +87,12 @@ export declare class RouterOs extends Platform {
80
87
  private establishedSessions;
81
88
  /** Wait until every one of these neighbors is established again, and give a problem for each that is not when time runs out. */
82
89
  private bgpBack;
90
+ /** What each session announces of the device's own `networks`, as `neighbor prefix`. None, without logging in, on a device without networks. */
91
+ private announcedNow;
92
+ /** The `networks` each session announces, as `neighbor prefix`, sorted. An advertisement names its session, which is matched to its neighbor by address first, as in `bgpSessions`. */
93
+ private announced;
94
+ /** Wait until each session announces again what it announced before, and give a problem for each prefix a session no longer announces when time runs out. */
95
+ private announcedBack;
83
96
  /** A problem when any switch rule is invalid, saying what to do. */
84
97
  private static invalidSwitchRules;
85
98
  /** Log in over SSH. `flags` go after the user, as RouterOS reads them: `+ct300w`. */
@@ -9,6 +9,10 @@ export declare class RouterOsPlan {
9
9
  private static readonly DYNAMIC;
10
10
  /** Menus where an entry that keeps its name but changes what it is known by is changed with a `set`, so what points at it by name, such as an address on a VLAN interface, stays attached. In every other menu it is removed and added again. */
11
11
  private static readonly CHANGED_IN_PLACE;
12
+ /**
13
+ * Properties a running entry takes only when it restarts. A BGP session given a new `routing-table` stops announcing at once and keeps its routes in the old table until it restarts (TRAPS.md), so the plan disables the connection, changes it and enables it again: one restart, written in the plan.
14
+ */
15
+ private static readonly RESTART_ON;
12
16
  /** The variable holding the old rules of an ordered menu while the new ones are added. Removed when the rewrite ends. */
13
17
  private static readonly OLD_RULES;
14
18
  /** What blocks an apply: something this plan cannot converge. */
@@ -199,6 +199,10 @@ export declare class RouterOsRender {
199
199
  /** Every switch rule the device's hardware ACLs make, in order: one for each port with an ACL, each rule of it, and each source and destination port or range of the rule. */
200
200
  static switchRules(device: Device): SwitchRule[];
201
201
  private routing;
202
+ /** A table beside the main one. `fib` puts its routes in the forwarding table, so traffic can be routed by them. */
203
+ private routingTables;
204
+ /** Rules in the order written. `lookup-only-in-table` drops what the table has no route for, and `lookup` falls back to the main table. */
205
+ private routingRules;
202
206
  private staticRoutes;
203
207
  private rpki;
204
208
  /** The default instance exists on every device; it is stated in the form the export prints it. */
@@ -65,9 +65,9 @@ export type Vrf = {
65
65
  /**
66
66
  * Everything one device runs. What is not written here is removed from the device or returned to its default.
67
67
  *
68
- * Type parameters are the names the device declares, so a reference to a name that does not exist fails to compile: N ports, V VLANs, I interfaces, P policies, S prefix sets, G BGP groups, A ACLs, AS address sets, U users, F VRFs, C certificates.
68
+ * Type parameters are the names the device declares, so a reference to a name that does not exist fails to compile: N ports, V VLANs, I interfaces, P policies, S prefix sets, G BGP groups, A ACLs, AS address sets, U users, F VRFs, C certificates, T routing tables.
69
69
  */
70
- export type Device<Pl extends string = string, M extends string = string, N extends string = string, V extends string = string, I extends string = string, P extends string = string, S extends string = string, G extends string = string, A extends string = string, AS extends string = string, U extends string = string, F extends string = string, C extends string = string> = {
70
+ export type Device<Pl extends string = string, M extends string = string, N extends string = string, V extends string = string, I extends string = string, P extends string = string, S extends string = string, G extends string = string, A extends string = string, AS extends string = string, U extends string = string, F extends string = string, C extends string = string, T extends string = string> = {
71
71
  /** Hostname. */
72
72
  name: string;
73
73
  /** Operating system. It decides which adapter renders and applies the device. */
@@ -92,8 +92,8 @@ export type Device<Pl extends string = string, M extends string = string, N exte
92
92
  interfaces?: Record<I, Interface<NoInfer<V>, NoInfer<F>>>;
93
93
  /** Separate routing tables, keyed by name. */
94
94
  vrfs?: Record<F, Vrf>;
95
- /** Static routes, BGP, OSPF and route validation. */
96
- routing?: Routing<NoInfer<P>, G, NoInfer<I | N>, NoInfer<F>>;
95
+ /** Routing tables and rules, static routes, BGP, OSPF and route validation. */
96
+ routing?: Routing<NoInfer<P>, G, NoInfer<I | N>, NoInfer<F>, T>;
97
97
  /** Route policies, keyed by name. */
98
98
  policies?: Record<P, RoutePolicy<NoInfer<P>, NoInfer<S>>>;
99
99
  /** Prefix sets that policies match on, keyed by name. */
@@ -109,7 +109,7 @@ export type Port<V extends string, A extends string, L extends string, F extends
109
109
  acl?: A;
110
110
  /** The LAG this port is in. Every other setting goes on the LAG. */
111
111
  lag?: L;
112
- /** Another of our devices at the other end of the cable. Without a `description`, the port is described from it: the far device and the far port as printed on it, such as `firewall-1 port6`. */
112
+ /** Another of our devices at the other end of the cable. */
113
113
  link?: Link;
114
114
  };
115
115
  /** Settings every interface that is not a physical port shares. */
@@ -1,6 +1,6 @@
1
1
  import type { Family, IP, Prefix, Secret } from "./common.js";
2
2
  /** A route the device has without learning it. */
3
- export type StaticRoute<I extends string, F extends string> = {
3
+ export type StaticRoute<I extends string, F extends string, T extends string = never> = {
4
4
  /** Destination. */
5
5
  prefix: Prefix;
6
6
  /** Next-hop address. */
@@ -13,8 +13,10 @@ export type StaticRoute<I extends string, F extends string> = {
13
13
  bfd?: true;
14
14
  /** Preference against other routes to the same prefix. Lower wins. 1 when absent. */
15
15
  distance?: number;
16
- /** The VRF the route is in. The default table when absent. */
16
+ /** The VRF the route is in. The main table when absent. */
17
17
  vrf?: F;
18
+ /** The table the route is in, one of `routing.tables`. The main table when absent. A route has a `vrf` or a `table`, not both. */
19
+ table?: T;
18
20
  /** Why the route exists. */
19
21
  description?: string;
20
22
  };
@@ -23,7 +25,7 @@ export type StaticRoute<I extends string, F extends string> = {
23
25
  */
24
26
  export type BgpRole = "provider" | "customer" | "peer" | "rs" | "rs-client";
25
27
  /** Settings a BGP neighbor can set itself or take from its group. */
26
- export type NeighborSettings<P extends string> = {
28
+ export type NeighborSettings<P extends string, T extends string = never> = {
27
29
  /** The neighbor's AS. Our own AS makes the session internal. */
28
30
  remote_as?: number;
29
31
  /** Who the neighbor is. */
@@ -52,24 +54,26 @@ export type NeighborSettings<P extends string> = {
52
54
  passive?: boolean;
53
55
  /** Detect a dead link in under a second with BFD. */
54
56
  bfd?: boolean;
57
+ /** The table the routes learned are installed in, one of `routing.tables`. The main table when absent. On RouterOS the session also announces from that table, so each prefix of `networks` it announces needs a route in the table too, and a running session takes a new table only when it restarts. */
58
+ table?: T;
55
59
  };
56
60
  /** One BGP session. Settings it does not set come from its group. */
57
- export type Neighbor<P extends string, G extends string> = NeighborSettings<P> & {
61
+ export type Neighbor<P extends string, G extends string, T extends string = never> = NeighborSettings<P, T> & {
58
62
  /** The neighbor's address. */
59
63
  address: IP;
60
64
  /** The group this neighbor takes a setting from when it does not set that setting itself. */
61
65
  group?: NoInfer<G>;
62
66
  };
63
67
  /** BGP on this device. */
64
- export type Bgp<P extends string, G extends string> = {
68
+ export type Bgp<P extends string, G extends string, T extends string = never> = {
65
69
  /** Our AS number. */
66
70
  asn: number;
67
71
  /** Prefixes this device originates. Each needs a route, for example a static blackhole. Export policy still decides who receives them. */
68
72
  networks?: readonly Prefix[];
69
73
  /** Settings shared by several neighbors, keyed by group name. */
70
- groups?: Record<G, NeighborSettings<P>>;
74
+ groups?: Record<G, NeighborSettings<P, T>>;
71
75
  /** Sessions keyed by name. */
72
- neighbors?: Record<string, Neighbor<P, G>>;
76
+ neighbors?: Record<string, Neighbor<P, G, T>>;
73
77
  };
74
78
  /** How fast BFD notices a dead neighbor: after `multiplier` packets in a row are missed, so in `interval` times `multiplier` milliseconds. */
75
79
  export type Bfd = {
@@ -111,14 +115,43 @@ export type Ospf<I extends string> = {
111
115
  /** Routes from other sources announced into OSPF. */
112
116
  redistribute?: readonly ("connected" | "static")[];
113
117
  };
118
+ /** A routing table beside the main one. It holds no interfaces: routes go in from static routes and BGP neighbors that name it in `table`, and `routing.rules` send traffic to it. */
119
+ export type RoutingTable = {
120
+ /** What the table is for. */
121
+ description?: string;
122
+ };
123
+ /** Traffic a routing rule matches: all of what is given. Every packet when absent. */
124
+ export type RoutingRuleMatch<I extends string> = {
125
+ /** Source prefix. */
126
+ src?: Prefix;
127
+ /** Destination prefix. */
128
+ dst?: Prefix;
129
+ /** The interface the traffic arrives on. */
130
+ in_interface?: I;
131
+ };
132
+ /** One routing rule: traffic it matches is routed by `table` instead of the main table. */
133
+ export type RoutingRule<I extends string, T extends string> = {
134
+ /** Why the rule exists. */
135
+ description?: string;
136
+ /** What the traffic must match. Every packet when absent, so such a rule is the last one. */
137
+ match?: RoutingRuleMatch<I>;
138
+ /** The table that routes the traffic: one of `routing.tables`, or `main`. */
139
+ table: T | "main";
140
+ /** When `table` has no route for a packet: `none` drops it as unroutable, `main` routes it by the main table instead. `none` when absent. Not for a rule whose table is `main`. */
141
+ fallback?: "none" | "main";
142
+ };
114
143
  /** Routing on this device. */
115
- export type Routing<P extends string, G extends string, I extends string, F extends string> = {
144
+ export type Routing<P extends string, G extends string, I extends string, F extends string, T extends string = never> = {
116
145
  /** Identifies this router to its neighbors. */
117
146
  router_id?: IP;
147
+ /** Routing tables beside the main one, keyed by name. Not `main`, and not the name of a VRF. */
148
+ tables?: Record<T, RoutingTable>;
149
+ /** Rules that route matching traffic by another table, such as traffic from one block by its own transits' default. In order: the first that matches decides, and traffic no rule matches is routed by the main table. */
150
+ rules?: readonly RoutingRule<I, NoInfer<T>>[];
118
151
  /** Routes the device has without learning them. */
119
- static?: readonly StaticRoute<I, F>[];
152
+ static?: readonly StaticRoute<I, F, NoInfer<T>>[];
120
153
  /** BGP sessions and what they exchange. */
121
- bgp?: Bgp<P, G>;
154
+ bgp?: Bgp<P, G, NoInfer<T>>;
122
155
  /** OSPF areas and interfaces. */
123
156
  ospf?: Ospf<I>;
124
157
  /** Validators answering route origin validation. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "0.41.0",
3
+ "version": "0.42.1",
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
@@ -103,20 +103,12 @@ class Cli {
103
103
  return new Cli(configPath, network, options);
104
104
  }
105
105
 
106
- /**
107
- * The config, a TypeScript file. Bun imports it as it is; Node needs jiti, which also resolves imports written without an extension. A linked port without a description is given one from its far end.
108
- */
106
+ /** The config, a TypeScript file. Bun imports it as it is; Node needs jiti, which also resolves imports written without an extension. */
109
107
  private static async load(path: string): Promise<Network> {
110
- let network: Network;
111
-
112
- if (process.versions.bun) {
113
- network = (await import(path)).default;
114
- } else {
115
- const { createJiti } = await import("jiti");
116
- network = await createJiti(import.meta.url).import<Network>(path, { default: true });
117
- }
108
+ if (process.versions.bun) return (await import(path)).default;
118
109
 
119
- return { ...network, devices: Cabling.describeLinks(network.devices) };
110
+ const { createJiti } = await import("jiti");
111
+ return createJiti(import.meta.url).import(path, { default: true });
120
112
  }
121
113
 
122
114
  private static fail(message: string): never {
@@ -238,7 +230,9 @@ class Cli {
238
230
  .command("bgp")
239
231
  .description("each BGP neighbor in the config next to its live session: up or down and why, prefixes, uptime")
240
232
  .argument("[device]", "may be left out when only one device has BGP neighbors")
241
- .argument("[name-regex]", "only the neighbors whose name matches")
233
+ // A second argument was the name filter before 0.42; it is refused below with the way to write it now.
234
+ .allowExcessArguments()
235
+ .option("--neighbors <regex>", "only the neighbors whose name matches")
242
236
  .option(
243
237
  "--wait [count]",
244
238
  `read again every ${BgpWait.COMMAND.every} seconds until every neighbor shown, or this many, is up`,
@@ -248,7 +242,14 @@ class Cli {
248
242
  .option("--log [minutes]", "also print the device's BGP log lines from the last minutes, 5 by default", Cli.minutes)
249
243
  .option("--clear-limit", "open again the neighbors shown that closed on their prefix limit; any other is left alone")
250
244
  .option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP)
251
- .action(async (first, second, flags) => (await Cli.open(flags.config)).bgp(first, second, flags));
245
+ .action(async (device, flags, command) => {
246
+ const [, filter] = command.args;
247
+ if (filter !== undefined)
248
+ Cli.fail(
249
+ `circuit bgp takes one device; filter the neighbors with --neighbors: circuit bgp ${device} --neighbors '${filter}'`
250
+ );
251
+ (await Cli.open(flags.config)).bgp(device, flags);
252
+ });
252
253
 
253
254
  program
254
255
  .command("console")
@@ -613,14 +614,14 @@ class Cli {
613
614
  }
614
615
 
615
616
  /**
616
- * 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.
617
+ * Each BGP neighbor in the config next to its live session, on the device named or the only one with BGP neighbors, and only those whose name matches `neighbors` when given. 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.
617
618
  */
618
619
  private async bgp(
619
- first: string | undefined,
620
- second: string | undefined,
621
- flags: { wait?: true | number; max: number; log?: true | number; clearLimit?: true }
620
+ deviceName: string | undefined,
621
+ flags: { neighbors?: string; wait?: true | number; max: number; log?: true | number; clearLimit?: true }
622
622
  ): Promise<void> {
623
- const { device, pattern } = this.bgpTarget(first, second);
623
+ const device = this.bgpDevice(deviceName);
624
+ const pattern = flags.neighbors;
624
625
  const adapter = this.adapterOf(device);
625
626
 
626
627
  const supported = Platforms.all()
@@ -697,19 +698,15 @@ class Cli {
697
698
  Cli.printBgp(await read());
698
699
  }
699
700
 
700
- /** The device `circuit bgp` reads, and the name pattern, from its two optional arguments. */
701
- private bgpTarget(first: string | undefined, second: string | undefined): { device: Device; pattern: string | undefined } {
702
- if (second !== undefined || (first && this.network.devices.some((device) => device.name === first))) {
703
- return { device: this.one(first!), pattern: second };
704
- }
701
+ /** The device `circuit bgp` reads: the one named, or the only device with BGP neighbors. */
702
+ private bgpDevice(name: string | undefined): Device {
703
+ if (name !== undefined) return this.one(name);
705
704
 
706
705
  const withBgp = this.network.devices.filter((device) => Object.keys(device.routing?.bgp?.neighbors ?? {}).length);
707
- const named = first ? `no device ${first}, and ` : "";
708
-
709
- if (withBgp.length === 1) return { device: withBgp[0]!, pattern: first };
710
- if (!withBgp.length) Cli.fail(`${named}no device has BGP neighbors`);
706
+ if (withBgp.length === 1) return withBgp[0]!;
707
+ if (!withBgp.length) Cli.fail("no device has BGP neighbors");
711
708
 
712
- return Cli.fail(`${named}several devices have BGP neighbors, so name one: ${withBgp.map((device) => device.name).join(", ")}`);
709
+ return Cli.fail(`several devices have BGP neighbors, so name one: ${withBgp.map((device) => device.name).join(", ")}`);
713
710
  }
714
711
 
715
712
  /** One line per neighbor: name, up or down, prefixes received, uptime, remote address. Then how many are up. */