@takodotid/circuit 0.40.0 → 0.42.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 (54) 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 +127 -49
  5. package/dist/helpers/index.js +143 -156
  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/bgp.d.ts +53 -9
  10. package/dist/types/helpers/cables.d.ts +14 -11
  11. package/dist/types/helpers/checks.d.ts +4 -0
  12. package/dist/types/helpers/exchange.d.ts +7 -1
  13. package/dist/types/helpers/index.d.ts +2 -2
  14. package/dist/types/helpers/upstreams.d.ts +123 -181
  15. package/dist/types/platforms/derive.d.ts +1 -1
  16. package/dist/types/platforms/fortios/routing.d.ts +2 -2
  17. package/dist/types/platforms/junos/bgp.d.ts +2 -2
  18. package/dist/types/platforms/routeros/render.d.ts +4 -0
  19. package/dist/types/schema/device.d.ts +4 -4
  20. package/dist/types/schema/firewall.d.ts +5 -1
  21. package/dist/types/schema/interfaces.d.ts +1 -1
  22. package/dist/types/schema/policy.d.ts +15 -6
  23. package/dist/types/schema/routing.d.ts +43 -10
  24. package/package.json +1 -1
  25. package/src/cli/index.ts +26 -29
  26. package/src/core/cabling.ts +0 -30
  27. package/src/core/define.ts +3 -1
  28. package/src/core/validator.ts +70 -2
  29. package/src/helpers/bgp.ts +117 -67
  30. package/src/helpers/cables.ts +20 -15
  31. package/src/helpers/checks.ts +23 -0
  32. package/src/helpers/exchange.ts +9 -3
  33. package/src/helpers/index.ts +2 -2
  34. package/src/helpers/upstreams.ts +162 -274
  35. package/src/platforms/derive.ts +23 -1
  36. package/src/platforms/fortios/routing.ts +2 -2
  37. package/src/platforms/junos/bgp.ts +2 -2
  38. package/src/platforms/routeros/TRAPS.md +4 -0
  39. package/src/platforms/routeros/export.ts +1 -0
  40. package/src/platforms/routeros/index.ts +11 -2
  41. package/src/platforms/routeros/plan.ts +3 -0
  42. package/src/platforms/routeros/render.ts +28 -3
  43. package/src/schema/device.ts +4 -3
  44. package/src/schema/firewall.ts +5 -1
  45. package/src/schema/interfaces.ts +1 -1
  46. package/src/schema/policy.ts +17 -6
  47. package/src/schema/routing.ts +46 -10
  48. package/templates/colocation/checks.ts +14 -11
  49. package/templates/colocation/edge-01.ts +2 -2
  50. package/templates/colocation/tenancy.ts +50 -71
  51. package/templates/colocation/tenants/acme.ts +10 -10
  52. package/templates/colocation/tenants/index.ts +2 -1
  53. package/templates/colocation/tor-01.ts +2 -2
  54. package/templates/project/AGENTS.md +15 -15
@@ -6,266 +6,177 @@ import type {
6
6
  Gre,
7
7
  InterfaceAddress,
8
8
  IP,
9
- LargeCommunity,
10
9
  Neighbor,
11
10
  NeighborSettings,
12
11
  PolicyRule,
13
12
  Port,
14
13
  Prefix,
15
14
  Secret,
16
- StandardCommunity,
17
15
  Vlan,
18
16
  VlanInterface,
19
17
  } from "../schema";
20
- import { Bgp, type Announced } from "./bgp";
21
18
 
22
- /** One network we exchange routes with: a transit we buy, an exchange's route servers, or a peer. */
19
+ /** A route policy an upstream's sessions use: its name on the device, and its rules, in order. */
20
+ export type UpstreamPolicy = {
21
+ /** The policy's name, as the device carries it. */
22
+ name: string;
23
+ /** Its rules, in order. Nothing is added to them unless `mapImports` or `mapExports` adds it. */
24
+ rules: readonly PolicyRule<string, string>[];
25
+ };
26
+
27
+ /** A VLAN by its name and its ID. */
28
+ export type NamedVlan = { name: string; id: number };
29
+
30
+ /** One network we exchange routes with: a transit we buy, an exchange's route servers, or a peer. Every name it puts on a device is written here. */
23
31
  export type Upstream = {
24
32
  /** Its name as people write it, for descriptions: `Transit A`. The key when absent. */
25
33
  name?: string;
26
34
  asn: number;
27
35
  /** What it is to us: `provider` for a transit, `rs` for an exchange's route servers, `peer` for a direct session. */
28
36
  role: "provider" | "rs" | "peer";
29
- /** 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. */
37
+ /** Where it lands on our side: a VLAN, the router's interface on it, and the switch port it arrives on. A peer over a tunnel, or over an exchange's VLAN, has none. */
30
38
  handoff?: {
31
- /** The VLAN's ID. */
32
- vlan: number;
33
- /** 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. */
34
- native?: number;
35
- /** Its number in names with `{number}`, such as `core-ipt01-transita`. Written here, so removing another upstream renumbers nothing. */
36
- number?: number;
39
+ /** The VLAN it arrives on. */
40
+ vlan: NamedVlan;
41
+ /** It sends its VLAN tagged: its switch port gets this native VLAN, which nothing else is in. Untagged when absent. */
42
+ native?: NamedVlan;
43
+ /** The router's VLAN interface on it. */
44
+ interface: string;
37
45
  /** Our addresses on it. `{ address, peer }` for a point-to-point address whose far end is outside its own prefix. */
38
46
  addresses: readonly (InterfaceAddress | { address: InterfaceAddress; peer: IP })[];
39
- /** The switch port it arrives on. */
40
- port?: { device: string; port: string; storm_control?: Port<string, string, string, string>["storm_control"] };
47
+ /**
48
+ * The switch port it arrives on. LLDP and spanning tree are off there unless `lldp` or `stp` says otherwise: an upstream's port faces someone else's network, which should neither learn ours nor join our spanning tree.
49
+ */
50
+ port?: {
51
+ device: string;
52
+ port: string;
53
+ storm_control?: Port<string, string, string, string>["storm_control"];
54
+ /** LLDP on the port. Off when absent. */
55
+ lldp?: boolean;
56
+ /** Spanning tree on the port. Off when absent. */
57
+ stp?: boolean;
58
+ };
41
59
  /** An exchange's LAN, for `circuit peeringdb`. */
42
60
  exchange?: ExchangeLan;
43
61
  };
44
- /** A GRE tunnel to it. `number` is its number in names with `{number}`. */
45
- tunnel?: { local: IP; remote: IP; addresses: readonly InterfaceAddress[]; mtu?: number; number?: number };
62
+ /** A GRE tunnel to it, and the router's interface for it. */
63
+ tunnel?: { interface: string; local: IP; remote: IP; addresses: readonly InterfaceAddress[]; mtu?: number };
46
64
  /** Our BGP sessions with it, by name: its address, and ours when it is not the interface's. */
47
65
  sessions: Readonly<Record<string, { address: IP; local_address?: IP }>>;
48
- /** The sessions are more than one hop away, from this address of ours. They share a group. */
66
+ /** 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. */
67
+ group?: string;
68
+ /** The sessions are more than one hop away, from this address of ours. */
49
69
  multihop?: { local_address: IP; families?: readonly Family[]; password?: Secret };
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. */
71
+ table?: string;
50
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. */
51
73
  limits?: { ipv4?: number; ipv6?: number };
52
- /** What we take. Nothing when absent. */
53
- import?: {
54
- /** Its default route, at this preference. */
55
- default?: { local_pref?: number; description?: string };
56
- /** Its routes, through `Bgp.learn`: the `before` rules, sanity, this preference, the tag. Only the default when absent. */
57
- learn?: { local_pref?: number; before?: readonly PolicyRule<never, never>[] };
58
- };
59
- /** Rules of yours in its export, between the community actions and what we announce, such as a prepend. */
60
- export?: readonly PolicyRule<never, never>[];
61
- /** What we announce to it. Everything else is rejected. */
62
- announce: readonly Announced[];
63
- /** Drop RFC 1918 sources arriving from it. `privateDrops()` lists the VLANs. */
74
+ /** What we take, as its policy. Nothing when absent. */
75
+ import?: UpstreamPolicy;
76
+ /** What we send it, as its policy. */
77
+ export: UpstreamPolicy;
78
+ /** Its VLAN is listed by `privateDrops()`, for an ACL that drops RFC 1918 sources arriving from it. Nothing else is done with it. */
64
79
  drop_private?: true;
65
80
  };
66
81
 
67
- /** Upstreams by key. The key names everything built from it. */
82
+ /** Upstreams by key. */
68
83
  export type UpstreamCatalog = Readonly<Record<string, Upstream>>;
69
84
 
70
- /** A name made from an upstream: `{key}` is its key, `{KEY}` the key in capitals, `{number}` its `handoff.number` or `tunnel.number`. */
71
- type Pattern = string;
72
- /** A pattern for every upstream, or one per role. A role left out takes the default. */
73
- type ByRole = Pattern | { readonly provider?: Pattern; readonly rs?: Pattern; readonly peer?: Pattern };
74
-
75
- /** How the names built from each upstream are made. Each one left out takes its default. */
76
- export type UpstreamNames = {
77
- /** The VLAN a handoff is on. `{key}` when absent. */
78
- vlan?: ByRole;
79
- /** The native VLAN of a handoff sent tagged. `{key}-native` when absent. */
80
- native?: Pattern;
81
- /** The router's interface: per role, and `tunnel` for one reached over a tunnel. `{key}` when absent. */
82
- interface?: Pattern | { readonly provider?: Pattern; readonly rs?: Pattern; readonly peer?: Pattern; readonly tunnel?: Pattern };
83
- /** The import policy. `{KEY}-IN` when absent. */
84
- import?: Pattern;
85
- /** The export policy. `{KEY}-OUT` when absent. */
86
- export?: Pattern;
87
- /** The BGP group of an exchange's route servers or of multihop sessions. `{key}` when absent. */
88
- group?: Pattern;
89
- /** The description of a handoff's switch port. Its `name` when absent. No device names it, so it may be any function. */
90
- port?: (key: string, upstream: Upstream) => string;
85
+ /** What every upstream's policies share, written once: plain functions over each policy's rules. */
86
+ export type UpstreamOptions = {
87
+ /** Each import's rules as the device gets them, from the rules the upstream writes. The rules as written when absent. */
88
+ mapImports?: (rules: readonly PolicyRule<string, string>[], key: string, upstream: Upstream) => readonly PolicyRule<string, string>[];
89
+ /** Each export's rules as the device gets them, from the rules the upstream writes. The rules as written when absent. */
90
+ mapExports?: (rules: readonly PolicyRule<string, string>[], key: string, upstream: Upstream) => readonly PolicyRule<string, string>[];
91
91
  };
92
92
 
93
- /** How each upstream's policies are written: shared by every upstream of a catalog. */
94
- export type UpstreamPolicies<P extends string> = {
95
- /** A policy holding `Bgp.sanity()` that each import calls. The sanity rules are written in each import when absent. */
96
- sanity?: P;
97
- /** Where a route came from, by upstream: a community or a rule, such as `communityScheme(...).tag(...)`. Nothing is tagged when absent. */
98
- tag?: (key: string, upstream: Upstream) => StandardCommunity | LargeCommunity | PolicyRule<never, never>;
99
- /** Rules first in each export, such as `communityScheme(...).actions(upstream.asn)`. */
100
- actions?: (key: string, upstream: Upstream) => readonly PolicyRule<never, never>[];
101
- /** The source address for routes of its family, one per family. */
102
- sources?: readonly IP[];
103
- };
104
-
105
- /** The names a device can refer to, and their patterns when the project gives none. */
106
- const DEFAULTS = {
107
- vlan: "{key}",
108
- native: "{key}-native",
109
- interface: "{key}",
110
- import: "{KEY}-IN",
111
- export: "{KEY}-OUT",
112
- group: "{key}",
113
- } as const;
114
-
115
- type Defaults = typeof DEFAULTS;
116
- type Field = keyof Defaults;
117
-
118
- type Key<C> = keyof C & string;
119
- /** The upstreams that land on a VLAN of ours. */
120
- type Landed<C> = { [K in Key<C>]: C[K] extends { handoff: object } ? K : never }[Key<C>];
121
- /** The upstreams that send their VLAN tagged. */
122
- type Tagged<C> = { [K in Key<C>]: C[K] extends { handoff: { native: number } } ? K : never }[Key<C>];
123
- /** The upstreams reached over a tunnel. */
124
- type Tunneled<C> = { [K in Key<C>]: C[K] extends { tunnel: object } ? K : never }[Key<C>];
125
- /** The upstreams whose sessions share a group: an exchange's route servers, or multihop sessions. */
126
- type Grouped<C> = { [K in Key<C>]: C[K] extends { role: "rs" } | { multihop: object } ? K : never }[Key<C>];
127
- /** The upstreams we take routes from. */
128
- type Importing<C> = { [K in Key<C>]: C[K] extends { import: object } ? K : never }[Key<C>];
129
-
130
- /** The project's pattern for a name, or the default. */
131
- type PatternOf<N, F extends Field> = N extends { [X in F]: infer P } ? P : Defaults[F];
132
- /** The pattern an upstream takes: the only one, its tunnel's, its role's, or the default. */
133
- type Chosen<P, U, F extends Field> = P extends string
134
- ? P
135
- : U extends { tunnel: object }
136
- ? P extends { tunnel: infer T extends string }
137
- ? T
138
- : Defaults[F]
139
- : U extends { role: infer R extends string }
140
- ? P extends { [X in R]: infer T extends string }
141
- ? T
142
- : Defaults[F]
143
- : Defaults[F];
144
- /** Every `From` in `S` replaced. */
145
- type Replace<S extends string, From extends string, To extends string> = S extends `${infer A}${From}${infer B}`
146
- ? `${A}${To}${Replace<B, From, To>}`
147
- : S;
148
- /** An upstream's `handoff.number` or `tunnel.number`. */
149
- type NumberOf<U> = U extends { handoff: { number: infer N extends number } }
150
- ? N
151
- : U extends { tunnel: { number: infer N extends number } }
152
- ? N
153
- : number;
154
- /** The name built for upstream `K`: its pattern with the key, the key in capitals and its number filled in. */
155
- type Named<C, N, F extends Field, K extends Key<C>> = Replace<
156
- Replace<Replace<Chosen<PatternOf<N, F>, C[K], F>, "{key}", K>, "{KEY}", Uppercase<K>>,
157
- "{number}",
158
- `${NumberOf<C[K]>}`
159
- >;
160
- /** Each of these upstreams' names. */
161
- type Names<C, N, F extends Field, Ks extends Key<C>> = { [K in Ks]: Named<C, N, F, K> }[Ks];
162
-
163
- /** The catalog as written, with any key the shape does not have refused, at every level. */
164
- type Exact<S, T> = T extends readonly unknown[] | string | number | boolean | Secret | ((...args: never[]) => unknown)
165
- ? T
166
- : // A shape that is a union, such as a storm-control threshold, is each of its members in turn: its keys are any member's.
167
- S extends unknown
168
- ? { [K in keyof T]: K extends keyof S ? Exact<NonNullable<S[K]>, T[K]> : never }
169
- : never;
170
-
171
- /** The names as written, with a name or a role Upstreams does not make refused. */
172
- type ExactNames<N> = {
173
- [F in keyof N]: F extends keyof UpstreamNames
174
- ? N[F] extends string | ((...args: never[]) => unknown)
175
- ? N[F]
176
- : { [R in keyof N[F]]: R extends "provider" | "rs" | "peer" | "tunnel" ? N[F][R] : never }
177
- : never;
178
- };
93
+ type Values<T> = T[keyof T];
94
+ /** Each VLAN name the catalog writes. */
95
+ type VlanName<C> = Values<{ [K in keyof C]: C[K] extends { handoff: { vlan: { name: infer N extends string } } } ? N : never }>;
96
+ /** Each native VLAN name. */
97
+ type NativeName<C> = Values<{ [K in keyof C]: C[K] extends { handoff: { native: { name: infer N extends string } } } ? N : never }>;
98
+ /** Each router interface on a handoff. */
99
+ type HandoffInterface<C> = Values<{ [K in keyof C]: C[K] extends { handoff: { interface: infer N extends string } } ? N : never }>;
100
+ /** Each router interface for a tunnel. */
101
+ type TunnelInterface<C> = Values<{ [K in keyof C]: C[K] extends { tunnel: { interface: infer N extends string } } ? N : never }>;
102
+ /** Each policy name. */
103
+ type PolicyName<C> = Values<{
104
+ [K in keyof C]:
105
+ | (C[K] extends { import: { name: infer N extends string } } ? N : never)
106
+ | (C[K] extends { export: { name: infer N extends string } } ? N : never);
107
+ }>;
108
+ /** Each BGP group name. */
109
+ type GroupName<C> = Values<{ [K in keyof C]: C[K] extends { group: infer N extends string } ? N : never }>;
110
+ /** Each routing table name. */
111
+ type TableName<C> = Values<{ [K in keyof C]: C[K] extends { table: infer N extends string } ? N : never }>;
179
112
 
180
113
  /** A network's upstreams written once, and each device's part built from them. */
181
114
  export class Upstreams {
182
115
  /**
183
- * 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. By default:
184
- *
185
- * - 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.
186
- * - Policies `<KEY>-IN` and `<KEY>-OUT`; a BGP group `<key>` for an exchange's route servers or multihop sessions.
116
+ * Each upstream written once: its AS, where it lands, its sessions, limits and policies, and every name it puts on a device. Everything a device needs is built from it:
187
117
  *
188
- * `names` gives patterns of your own, such as `{ interface: { provider: "core-ipt0{number}-{key}" } }`; see `UpstreamNames`.
118
+ * - On the router: `vlans()`, `interfaces()`, `uplinks()`, `groups()`, `neighbors()`, `policies()`, `tunnelRules()`, `addresses()`.
119
+ * - On a switch: `vlans(device)`, `ports(device)`, `vlansOn(device)`.
120
+ * - For checks and ACLs: `untrusted()`, `natives()`, `privateDrops()`.
189
121
  *
190
- * On the router: `vlans()`, `interfaces()`, `uplinks()`, `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.
122
+ * An import or export is the rules the upstream writes, in order, passed through `mapImports` or `mapExports` when given: what every upstream shares, such as a call to a sanity policy or the community actions, goes there. Nothing else is added. Lists follow the catalog's order, where order counts on a device. A key the shape doesn't have is refused by the type, by its name.
191
123
  */
192
- static define<const C extends UpstreamCatalog, const N extends UpstreamNames = {}>(
193
- catalog: C & { [K in keyof C]: Exact<Upstream, C[K]> },
194
- options: { names?: N & ExactNames<N> } = {}
195
- ) {
196
- type V = Names<C, N, "vlan", Landed<C>>;
197
- type T = Names<C, N, "native", Tagged<C>>;
198
- type I = Names<C, N, "interface", Landed<C>>;
199
- type G = Names<C, N, "interface", Tunneled<C>>;
200
- type PolicyName = Names<C, N, "import", Importing<C>> | Names<C, N, "export", Key<C>>;
201
- type Group = Names<C, N, "group", Grouped<C>>;
202
-
203
- const names: UpstreamNames = options.names ?? {};
204
- const all = Object.entries(catalog) as [Key<C>, Upstream][];
124
+ static define<const C extends UpstreamCatalog>(catalog: C & UpstreamCatalog, options: UpstreamOptions = {}) {
125
+ type V = VlanName<C>;
126
+ type T = NativeName<C>;
127
+
128
+ const all = Object.entries(catalog) as [string, Upstream][];
205
129
  const nameOf = (key: string, upstream: Upstream) => upstream.name ?? key;
206
- const grouped = (upstream: Upstream) => upstream.role === "rs" || Boolean(upstream.multihop);
207
-
208
- /** The name built for one upstream: its pattern with the key, the key in capitals and its number filled in. */
209
- const named = (field: Field, key: string, upstream: Upstream): string => {
210
- const given = names[field];
211
- const pattern =
212
- given === undefined || typeof given === "string"
213
- ? (given ?? DEFAULTS[field])
214
- : ((upstream.tunnel && "tunnel" in given ? given.tunnel : given[upstream.role]) ?? DEFAULTS[field]);
215
-
216
- return pattern
217
- .replaceAll("{key}", key)
218
- .replaceAll("{KEY}", key.toUpperCase())
219
- .replaceAll("{number}", () => {
220
- const number = upstream.handoff?.number ?? upstream.tunnel?.number;
221
- if (number === undefined) throw new Error(`Upstreams: ${key} needs a number for ${field} name ${pattern}`);
222
- return String(number);
223
- });
224
- };
225
130
 
226
131
  // Two upstreams under one name would replace each other on the device.
227
- for (const field of Object.keys(DEFAULTS) as Field[]) {
132
+ const unique = (what: string, names: [string, string | undefined][]) => {
228
133
  const taken = new Map<string, string>();
229
-
230
- for (const [key, upstream] of all) {
231
- if (field === "native" && upstream.handoff?.native === undefined) continue;
232
- if (field === "import" && !upstream.import) continue;
233
- if (field === "group" && !grouped(upstream)) continue;
234
- if ((field === "vlan" && !upstream.handoff) || (field === "interface" && !upstream.handoff && !upstream.tunnel)) continue;
235
-
236
- const name = named(field, key, upstream);
134
+ for (const [key, name] of names) {
135
+ if (name === undefined) continue;
237
136
  const other = taken.get(name);
238
- if (other) throw new Error(`Upstreams: ${other} and ${key} both have the ${field} name ${name}`);
137
+ if (other) throw new Error(`Upstreams: ${other} and ${key} both have the ${what} ${name}`);
239
138
  taken.set(name, key);
240
139
  }
241
- }
242
-
243
- const vlan = (key: string, upstream: Upstream) => named("vlan", key, upstream) as V;
244
- const native = (key: string, upstream: Upstream) => named("native", key, upstream) as T;
245
- const policy = (direction: "import" | "export", key: string, upstream: Upstream) => named(direction, key, upstream) as PolicyName;
246
-
247
- /** Each landed upstream's VLAN; with a device, the VLANs and natives of those arriving on its ports. */
248
- const vlans = <const D extends string | undefined = undefined>(device?: D) => {
140
+ };
141
+ unique("VLAN", [
142
+ ...all.map(([key, upstream]): [string, string | undefined] => [key, upstream.handoff?.vlan.name]),
143
+ ...all.map(([key, upstream]): [string, string | undefined] => [key, upstream.handoff?.native?.name]),
144
+ ]);
145
+ unique(
146
+ "interface",
147
+ all.map(([key, upstream]) => [key, upstream.handoff?.interface ?? upstream.tunnel?.interface])
148
+ );
149
+ unique("policy", [
150
+ ...all.map(([key, upstream]): [string, string | undefined] => [key, upstream.import?.name]),
151
+ ...all.map(([key, upstream]): [string, string | undefined] => [key, upstream.export.name]),
152
+ ]);
153
+ unique(
154
+ "BGP group",
155
+ all.map(([key, upstream]) => [key, upstream.group])
156
+ );
157
+
158
+ /** Each landed upstream's VLAN and native, or with a device, those of the upstreams arriving on its ports. */
159
+ const vlans = (device?: string) => {
249
160
  const result: Record<string, Vlan> = {};
250
161
 
251
162
  for (const [key, upstream] of all) {
252
163
  const handoff = upstream.handoff;
253
164
  if (!handoff || (device !== undefined && handoff.port?.device !== device)) continue;
254
165
 
255
- result[vlan(key, upstream)] = { id: handoff.vlan, description: nameOf(key, upstream) };
256
- if (device !== undefined && handoff.native !== undefined) {
257
- result[native(key, upstream)] = {
258
- id: handoff.native,
166
+ result[handoff.vlan.name] = { id: handoff.vlan.id, description: nameOf(key, upstream) };
167
+ if (handoff.native) {
168
+ result[handoff.native.name] = {
169
+ id: handoff.native.id,
259
170
  description: `Native toward ${nameOf(key, upstream)}, nothing else in it`,
260
171
  };
261
172
  }
262
173
  }
263
174
 
264
- return result as D extends string ? { [K in V]: Vlan } & { [K in T]: Vlan } : { [K in V]: Vlan };
175
+ return result as { [K in V | T]: Vlan };
265
176
  };
266
177
 
267
178
  const vlansOn = (device: string) =>
268
- all.filter(([, upstream]) => upstream.handoff?.port?.device === device).map(([key, upstream]) => vlan(key, upstream));
179
+ all.flatMap(([, upstream]) => (upstream.handoff?.port?.device === device ? [upstream.handoff.vlan.name as V] : []));
269
180
 
270
181
  const ports = (device: string) => {
271
182
  const result: Record<string, Port<V | T, never, never, never>> = {};
@@ -275,12 +186,12 @@ export class Upstreams {
275
186
  if (!handoff?.port || handoff.port.device !== device) continue;
276
187
 
277
188
  result[handoff.port.port] = {
278
- description: names.port ? names.port(key, upstream) : nameOf(key, upstream),
279
- ...(handoff.native !== undefined
280
- ? { trunk_vlans: [vlan(key, upstream)], native_vlan: native(key, upstream) }
281
- : { access_vlan: vlan(key, upstream) }),
282
- lldp: false,
283
- stp: false,
189
+ description: nameOf(key, upstream),
190
+ ...(handoff.native
191
+ ? { trunk_vlans: [handoff.vlan.name as V], native_vlan: handoff.native.name as T }
192
+ : { access_vlan: handoff.vlan.name as V }),
193
+ lldp: handoff.port.lldp ?? false,
194
+ stp: handoff.port.stp ?? false,
284
195
  ...(handoff.port.storm_control ? { storm_control: handoff.port.storm_control } : {}),
285
196
  };
286
197
  }
@@ -293,55 +204,47 @@ export class Upstreams {
293
204
 
294
205
  for (const [key, upstream] of all) {
295
206
  const description = nameOf(key, upstream);
296
- const name = named("interface", key, upstream);
297
207
 
298
208
  if (upstream.tunnel) {
299
- const { local, remote, addresses, mtu } = upstream.tunnel;
209
+ const { interface: name, local, remote, addresses, mtu } = upstream.tunnel;
300
210
  result[name] = { type: "gre", description, local, remote, addresses, ...(mtu ? { mtu } : {}) };
301
211
  } else if (upstream.handoff) {
302
- const { addresses, exchange } = upstream.handoff;
303
- result[name] = { type: "vlan", vlan: vlan(key, upstream), description, addresses, ...(exchange ? { exchange } : {}) };
212
+ const { interface: name, vlan, addresses, exchange } = upstream.handoff;
213
+ result[name] = { type: "vlan", vlan: vlan.name as V, description, addresses, ...(exchange ? { exchange } : {}) };
304
214
  }
305
215
  }
306
216
 
307
- return result as { [K in I]: VlanInterface<V, never> } & { [K in G]: Gre<never> };
217
+ return result as { [K in HandoffInterface<C>]: VlanInterface<V, never> } & { [K in TunnelInterface<C>]: Gre<never> };
308
218
  };
309
219
 
310
- /** The router's interfaces toward the internet: every landed upstream's, for NAT and lists like it. */
311
- const uplinks = () =>
312
- all.filter(([, upstream]) => upstream.handoff).map(([key, upstream]) => named("interface", key, upstream) as I);
313
-
314
- /** What a session or its group takes from its upstream. */
315
- const settings = (key: string, upstream: Upstream): NeighborSettings<PolicyName> => ({
316
- remote_as: upstream.asn,
317
- description: nameOf(key, upstream),
318
- role: upstream.role,
319
- ...(upstream.import ? { import: policy("import", key, upstream) } : {}),
320
- export: policy("export", key, upstream),
321
- });
220
+ /** The router's interface toward each handoff, for NAT and lists like it. */
221
+ const uplinks = () => all.flatMap(([, upstream]) => (upstream.handoff ? [upstream.handoff.interface as HandoffInterface<C>] : []));
222
+
223
+ /** What a session, or the group it is in, takes from its upstream. */
224
+ const settings = (key: string, upstream: Upstream): NeighborSettings<PolicyName<C>, TableName<C>> => {
225
+ const multihop = upstream.multihop;
226
+ return {
227
+ remote_as: upstream.asn,
228
+ description: nameOf(key, upstream),
229
+ role: upstream.role,
230
+ ...(upstream.import ? { import: upstream.import.name as PolicyName<C> } : {}),
231
+ export: upstream.export.name as PolicyName<C>,
232
+ ...(upstream.table ? { table: upstream.table as TableName<C> } : {}),
233
+ ...(multihop
234
+ ? {
235
+ local_address: multihop.local_address,
236
+ multihop: true,
237
+ ...(multihop.families ? { families: multihop.families } : {}),
238
+ ...(multihop.password ? { password: multihop.password } : {}),
239
+ }
240
+ : {}),
241
+ };
242
+ };
322
243
 
323
244
  const groups = () =>
324
245
  Object.fromEntries(
325
- all
326
- .filter(([, upstream]) => grouped(upstream))
327
- .map(([key, upstream]) => {
328
- const multihop = upstream.multihop;
329
- return [
330
- named("group", key, upstream),
331
- {
332
- ...settings(key, upstream),
333
- ...(multihop
334
- ? {
335
- local_address: multihop.local_address,
336
- multihop: true,
337
- ...(multihop.families ? { families: multihop.families } : {}),
338
- ...(multihop.password ? { password: multihop.password } : {}),
339
- }
340
- : {}),
341
- },
342
- ];
343
- })
344
- ) as Record<Group, NeighborSettings<PolicyName>>;
246
+ all.flatMap(([key, upstream]) => (upstream.group ? [[upstream.group, settings(key, upstream)]] : []))
247
+ ) as Record<GroupName<C>, NeighborSettings<PolicyName<C>, TableName<C>>>;
345
248
 
346
249
  const neighbors = () =>
347
250
  Object.fromEntries(
@@ -351,59 +254,44 @@ export class Upstreams {
351
254
  return [
352
255
  name,
353
256
  {
257
+ ...(upstream.group ? { group: upstream.group } : settings(key, upstream)),
354
258
  ...session,
355
- ...(grouped(upstream) ? { group: named("group", key, upstream) } : settings(key, upstream)),
356
259
  ...(limit ? { max_prefixes: limit } : {}),
357
260
  },
358
261
  ];
359
262
  })
360
263
  )
361
- ) as Record<string, Neighbor<PolicyName, Group>>;
264
+ ) as Record<string, Neighbor<PolicyName<C>, GroupName<C>, TableName<C>>>;
362
265
 
363
- const policies = <const P extends string = never>(options: UpstreamPolicies<P> = {}) => {
364
- const result: Record<string, PolicyRule<P, never>[]> = {};
266
+ /**
267
+ * Each upstream's import and export, by name. The policies and prefix sets their rules name are checked by `validate`, not by the editor, so the rules are typed to fit any device.
268
+ */
269
+ const policies = () => {
270
+ const result: Record<string, readonly PolicyRule<string, string>[]> = {};
365
271
 
366
272
  for (const [key, upstream] of all) {
367
- result[policy("export", key, upstream)] = [
368
- ...(options.actions?.(key, upstream) ?? []),
369
- ...(upstream.export ?? []),
370
- ...Bgp.announce(upstream.announce),
371
- ];
372
-
373
- const taken = upstream.import;
374
- if (!taken) continue;
375
-
376
- const tag = options.tag?.(key, upstream);
377
- result[policy("import", key, upstream)] = [
378
- ...(taken.default
379
- ? Bgp.defaultOnly({ ...taken.default, ...(options.sources ? { sources: options.sources } : {}) })
380
- : []),
381
- ...(taken.learn
382
- ? Bgp.learn<P>({
383
- ...(taken.learn.before ? { before: taken.learn.before } : {}),
384
- ...(options.sanity ? { sanity: options.sanity } : {}),
385
- ...(options.sources ? { sources: options.sources } : {}),
386
- ...(taken.learn.local_pref !== undefined ? { local_pref: taken.learn.local_pref } : {}),
387
- ...(tag !== undefined ? { tag } : {}),
388
- })
389
- : [{ description: "Nothing else", action: "reject" as const }]),
390
- ];
273
+ if (upstream.import) {
274
+ const rules = upstream.import.rules;
275
+ result[upstream.import.name] = options.mapImports ? options.mapImports(rules, key, upstream) : rules;
276
+ }
277
+
278
+ const rules = upstream.export.rules;
279
+ result[upstream.export.name] = options.mapExports ? options.mapExports(rules, key, upstream) : rules;
391
280
  }
392
281
 
393
- return result as Record<PolicyName, PolicyRule<P, never>[]>;
282
+ return result as Record<PolicyName<C>, PolicyRule<never, never>[]>;
394
283
  };
395
284
 
396
285
  /** The VLANs straight off an upstream, natives included: `untrusted` for `Checks.trustBoundary`. */
397
286
  const untrusted = () => [
398
- ...all.filter(([, upstream]) => upstream.handoff).map(([key, upstream]) => vlan(key, upstream)),
287
+ ...all.flatMap(([, upstream]) => (upstream.handoff ? [upstream.handoff.vlan.name as V] : [])),
399
288
  ...natives(),
400
289
  ];
401
290
 
402
- const natives = () =>
403
- all.filter(([, upstream]) => upstream.handoff?.native !== undefined).map(([key, upstream]) => native(key, upstream));
291
+ const natives = () => all.flatMap(([, upstream]) => (upstream.handoff?.native ? [upstream.handoff.native.name as T] : []));
404
292
 
405
293
  const privateDrops = () =>
406
- all.filter(([, upstream]) => upstream.handoff && upstream.drop_private).map(([key, upstream]) => vlan(key, upstream));
294
+ all.flatMap(([, upstream]) => (upstream.handoff && upstream.drop_private ? [upstream.handoff.vlan.name as V] : []));
407
295
 
408
296
  const tunnelRules = (): FilterRule<never, never>[] =>
409
297
  all.flatMap(([key, upstream]) =>
@@ -5,7 +5,14 @@ import type { Device } from "../schema";
5
5
 
6
6
  /** A feature a platform renders once proved on one of its devices. */
7
7
  export type ProvedFeature =
8
- "bfd-timers" | "static-route-bfd" | "multihop-bfd" | "alarm-severity" | "mac-limit" | "ethertype-filter" | "management-vdom";
8
+ | "bfd-timers"
9
+ | "static-route-bfd"
10
+ | "multihop-bfd"
11
+ | "alarm-severity"
12
+ | "mac-limit"
13
+ | "ethertype-filter"
14
+ | "management-vdom"
15
+ | "routing-tables";
9
16
 
10
17
  export class Derive {
11
18
  /** Each LAG's number: its own `id`, or its position among the device's LAGs. */
@@ -47,11 +54,26 @@ export class Derive {
47
54
  for (const route of routing?.static ?? []) {
48
55
  if (route.bfd && !has("static-route-bfd"))
49
56
  found.push(`routing.static ${route.prefix} bfd, BFD on a static route is ${unproved}`);
57
+ if (route.table && !has("routing-tables"))
58
+ found.push(`routing.static ${route.prefix} table, a routing table beside the main one is ${unproved}`);
59
+ }
60
+
61
+ if (!has("routing-tables")) {
62
+ if (routing?.tables) found.push(`routing.tables, a routing table beside the main one is ${unproved}`);
63
+ if (routing?.rules) found.push(`routing.rules, routing rules are ${unproved}`);
50
64
  }
51
65
 
52
66
  const bgp = routing?.bgp;
53
67
 
68
+ for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
69
+ if (group.table && !has("routing-tables"))
70
+ found.push(`routing.bgp.groups.${name}.table, a routing table beside the main one is ${unproved}`);
71
+ }
72
+
54
73
  for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
74
+ if (neighbor.table && !has("routing-tables"))
75
+ found.push(`routing.bgp.neighbors.${name}.table, a routing table beside the main one is ${unproved}`);
76
+
55
77
  const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
56
78
  const bfd = neighbor.bfd ?? group?.bfd;
57
79
  const multihop = neighbor.multihop ?? group?.multihop;
@@ -7,7 +7,7 @@ import { FortiOsNames } from "./names";
7
7
  import { FortiOsBlock, FortiOsTree } from "./tree";
8
8
 
9
9
  type AnyRule = PolicyRule<string, string>;
10
- type AnyNeighbor = Neighbor<string, string>;
10
+ type AnyNeighbor = Neighbor<string, string, string>;
11
11
 
12
12
  /** The lengths a prefix list's rule takes within its prefix. */
13
13
  type Lengths = { ge?: number; le?: number };
@@ -167,7 +167,7 @@ export class FortiOsRouting {
167
167
  static readonly BITS = { ipv4: 32, ipv6: 128 } as const;
168
168
 
169
169
  /** A neighbor's settings with its group's beneath them. */
170
- static neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string> {
170
+ static neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string, string> {
171
171
  const group = neighbor.group ? device.routing?.bgp?.groups?.[neighbor.group] : undefined;
172
172
  return { ...group, ...neighbor };
173
173
  }