@takodotid/circuit 0.24.0 → 0.26.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 (42) hide show
  1. package/README.md +3 -3
  2. package/dist/chunks/{index-n4zpd25n.js → index-22sbskdx.js} +22 -6
  3. package/dist/chunks/{server-ckdy25w2.js → server-1ftkqkx5.js} +1 -1
  4. package/dist/cli/index.js +4621 -324
  5. package/dist/presets/index.js +5 -0
  6. package/dist/types/adapters/devices/catalog.d.ts +28 -0
  7. package/dist/types/adapters/devices/fortios/firewall.d.ts +21 -0
  8. package/dist/types/adapters/devices/fortios/index.d.ts +55 -0
  9. package/dist/types/adapters/devices/fortios/models.d.ts +14 -0
  10. package/dist/types/adapters/devices/fortios/names.d.ts +32 -0
  11. package/dist/types/adapters/devices/fortios/plan.d.ts +26 -0
  12. package/dist/types/adapters/devices/fortios/render.d.ts +36 -0
  13. package/dist/types/adapters/devices/fortios/routing.d.ts +32 -0
  14. package/dist/types/adapters/devices/fortios/tree.d.ts +33 -0
  15. package/dist/types/adapters/devices/junos/bgp.d.ts +11 -0
  16. package/dist/types/adapters/devices/junos/index.d.ts +39 -0
  17. package/dist/types/adapters/devices/junos/models.d.ts +14 -0
  18. package/dist/types/adapters/devices/junos/render.d.ts +52 -0
  19. package/dist/types/core/graph.d.ts +2 -0
  20. package/dist/types/presets/index.d.ts +1 -1
  21. package/package.json +7 -2
  22. package/src/adapters/devices/catalog.ts +5 -2
  23. package/src/adapters/devices/fortios/TRAPS.md +40 -0
  24. package/src/adapters/devices/fortios/firewall.ts +352 -0
  25. package/src/adapters/devices/fortios/index.ts +800 -0
  26. package/src/adapters/devices/fortios/models.ts +7 -0
  27. package/src/adapters/devices/fortios/names.ts +78 -0
  28. package/src/adapters/devices/fortios/plan.ts +840 -0
  29. package/src/adapters/devices/fortios/render.ts +734 -0
  30. package/src/adapters/devices/fortios/routing.ts +444 -0
  31. package/src/adapters/devices/fortios/tree.ts +121 -0
  32. package/src/adapters/devices/index.ts +3 -1
  33. package/src/adapters/devices/junos/TRAPS.md +31 -0
  34. package/src/adapters/devices/junos/bgp.ts +263 -0
  35. package/src/adapters/devices/junos/index.ts +784 -0
  36. package/src/adapters/devices/junos/models.ts +7 -0
  37. package/src/adapters/devices/junos/render.ts +868 -0
  38. package/src/core/across.ts +6 -3
  39. package/src/core/graph.ts +3 -1
  40. package/src/core/validate.ts +4 -3
  41. package/src/presets/index.ts +7 -1
  42. package/templates/colocation/checks.ts +8 -1
@@ -0,0 +1,263 @@
1
+ // BGP, route policies, prefix sets and route origin validation, as Junos `set` statements.
2
+
3
+ import { familyOf, host } from "../../../core/addr";
4
+ import { marker } from "../../../core/secrets";
5
+ import type { Device, Family, Neighbor, NeighborSettings, PolicyRule } from "../../../schema";
6
+ import { BFD_INTERVAL, quote, type Statements } from "./render";
7
+
8
+ type AnyRule = PolicyRule<string, string>;
9
+ type AnyNeighbor = Neighbor<string, string>;
10
+
11
+ /** The policy a session uses when it states none, so that it learns or announces nothing. */
12
+ export const REJECT_ALL = "circuit-reject-all";
13
+
14
+ /** The policy every export starts with. BGP's own routes and the prefixes the device originates go on to the session's policy; the device's other routes never reach it. */
15
+ const ORIGIN = "circuit-bgp-origin";
16
+
17
+ /** The validator group every RPKI server joins. */
18
+ const RPKI_GROUP = "circuit";
19
+
20
+ /** A comment in the render naming a path whose statements `refresh` fills, so the plan keeps what the device holds there. */
21
+ export const GENERATED = "# generated ";
22
+
23
+ const FAMILY = { ipv4: "inet", ipv6: "inet6" } as const;
24
+
25
+ const VALIDATION = { valid: "valid", invalid: "invalid", "not-found": "unknown" } as const;
26
+
27
+ /** A neighbor's settings with its group's beneath them. */
28
+ export function settingsOf(device: Device, neighbor: AnyNeighbor): NeighborSettings<string> {
29
+ const group = neighbor.group ? device.routing?.bgp?.groups?.[neighbor.group] : undefined;
30
+ return { ...group, ...neighbor };
31
+ }
32
+
33
+ /** A name made of a value's words, for a community or a path-length list: `64500:*` becomes `64500-any`. */
34
+ const slug = (value: string) => value.replace(/\*/g, "any").replace(/[^A-Za-z0-9]+/g, "-");
35
+
36
+ /** A community as the platform writes a member: a large community carries the `large:` prefix. */
37
+ const member = (community: string, large: boolean) => (large ? `large:${community}` : community);
38
+
39
+ export function renderBgp(out: Statements, device: Device): void {
40
+ prefixSets(out, device);
41
+ rpki(out, device);
42
+
43
+ const bgp = device.routing?.bgp;
44
+ if (!bgp) return;
45
+
46
+ out.set("routing-options autonomous-system", bgp.asn);
47
+
48
+ const used = new Set<string>();
49
+ let rejectAll = false;
50
+ let exports = false;
51
+
52
+ // One group per session: a group holds one type, internal or external, and the config's groups are only shared settings.
53
+ for (const [name, neighbor] of Object.entries(bgp.neighbors ?? {})) {
54
+ const settings = settingsOf(device, neighbor);
55
+ const group = `protocols bgp group ${name}`;
56
+
57
+ out.set(group, "type", settings.remote_as === bgp.asn ? "internal" : "external");
58
+ out.set(group, "peer-as", settings.remote_as!);
59
+ if (settings.description) out.set(group, "description", quote(settings.description));
60
+ if (settings.local_address) out.set(group, "local-address", settings.local_address);
61
+ if (settings.multihop) out.set(group, "multihop");
62
+ if (settings.passive) out.set(group, "passive");
63
+ if (settings.hold_time !== undefined) out.set(group, "hold-time", settings.hold_time);
64
+ if (settings.password) out.set(group, "authentication-key", marker(settings.password));
65
+ if (settings.bfd) out.set(group, "bfd-liveness-detection minimum-interval", BFD_INTERVAL);
66
+
67
+ out.set(group, "import", settings.import ?? REJECT_ALL);
68
+
69
+ if (settings.export) {
70
+ exports = true;
71
+ out.set(group, "export", ORIGIN);
72
+ out.set(group, "export", settings.export);
73
+ } else out.set(group, "export", REJECT_ALL);
74
+
75
+ for (const policy of [settings.import, settings.export]) {
76
+ if (policy) used.add(policy);
77
+ else rejectAll = true;
78
+ }
79
+
80
+ for (const family of settings.families ?? [familyOf(neighbor.address)]) {
81
+ const nlri = `${group} family ${FAMILY[family]} unicast`;
82
+ out.set(nlri);
83
+
84
+ // A session over its limit stays down until it is cleared by hand.
85
+ if (settings.max_prefixes !== undefined) {
86
+ out.set(nlri, "prefix-limit maximum", settings.max_prefixes);
87
+ out.set(nlri, "prefix-limit teardown idle-timeout forever");
88
+ }
89
+ }
90
+
91
+ out.set(group, "neighbor", neighbor.address);
92
+ }
93
+
94
+ if (rejectAll) out.set(`policy-options policy-statement ${REJECT_ALL} then reject`);
95
+
96
+ if (exports) origin(out, device);
97
+
98
+ for (const policy of [...used].sort()) policyStatement(out, device, policy);
99
+ }
100
+
101
+ /** BGP's routes and the originated prefixes go on to the session's policy; anything else is rejected. */
102
+ function origin(out: Statements, device: Device): void {
103
+ const statement = `policy-options policy-statement ${ORIGIN}`;
104
+ out.set(statement, "term bgp from protocol bgp");
105
+ out.set(statement, "term bgp then next policy");
106
+
107
+ const networks = device.routing?.bgp?.networks ?? [];
108
+ for (const prefix of networks) out.set(statement, "term networks from route-filter", prefix, "exact");
109
+ if (networks.length) out.set(statement, "term networks then next policy");
110
+
111
+ out.set(statement, "term others then reject");
112
+ }
113
+
114
+ function prefixSets(out: Statements, device: Device): void {
115
+ for (const [name, set] of Object.entries(device.prefix_sets ?? {})) {
116
+ const list = `policy-options prefix-list ${name}`;
117
+
118
+ if ("prefixes" in set && set.prefixes.length) for (const prefix of set.prefixes) out.set(list, prefix);
119
+ else out.set(list);
120
+
121
+ if ("source" in set) out.note(`${GENERATED}${list}`);
122
+ }
123
+ }
124
+
125
+ function rpki(out: Statements, device: Device): void {
126
+ for (const server of device.routing?.rpki ?? []) {
127
+ out.set(`routing-options validation group ${RPKI_GROUP} session`, server.address, "port", server.port);
128
+ }
129
+ }
130
+
131
+ /** The family a rule is limited to, by its match: a family, a prefix, or a prefix set of one family. */
132
+ function familyOfRule(device: Device, rule: AnyRule): Family | undefined {
133
+ const match = rule.match;
134
+ if (match?.family) return match.family;
135
+
136
+ if (match?.prefix) return familyOf(host(match.prefix));
137
+
138
+ const set = match?.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
139
+ if (set && "source" in set) return set.source.family;
140
+
141
+ if (set?.prefixes[0]) return familyOf(host(set.prefixes[0]));
142
+
143
+ return undefined;
144
+ }
145
+
146
+ /** A policy's rules with each call replaced by the rules it calls, in order. A called policy that decides nothing hands back to the caller, as it does on every platform. */
147
+ function flatten(device: Device, policy: string, label: string, seen: string[]): { label: string; rule: AnyRule }[] {
148
+ const rules = device.policies?.[policy] ?? [];
149
+ const flat: { label: string; rule: AnyRule }[] = [];
150
+
151
+ rules.forEach((rule, index) => {
152
+ const here = label ? `${label}.${index + 1}` : `${index + 1}`;
153
+
154
+ if (rule.call && !seen.includes(rule.call)) flat.push(...flatten(device, rule.call, here, [...seen, rule.call]));
155
+
156
+ if (rule.match || rule.set || rule.action) flat.push({ label: here, rule });
157
+ });
158
+
159
+ return flat;
160
+ }
161
+
162
+ /** A term's name: its place, then the rule's description so a reader sees it on the device. */
163
+ const termName = (label: string, rule: AnyRule) => quote(rule.description ? `${label} ${rule.description}` : label);
164
+
165
+ /** One policy as a policy statement: a term per rule, then a reject when the last rule does not decide on every route. */
166
+ function policyStatement(out: Statements, device: Device, policy: string): void {
167
+ const statement = `policy-options policy-statement ${policy}`;
168
+ const flat = flatten(device, policy, "", [policy]);
169
+
170
+ for (const { label, rule } of flat) {
171
+ const family = familyOfRule(device, rule);
172
+ // A next hop only means something to routes of its own family, and one term cannot filter prefixes of both, so a rule for both families splits into a term for each.
173
+ const split = !family && (rule.set?.next_hop || rule.match?.prefix_length) ? (["ipv4", "ipv6"] as const) : [family];
174
+
175
+ for (const part of split) {
176
+ const name = split.length > 1 ? termName(`${label}-${part === "ipv4" ? "v4" : "v6"}`, rule) : termName(label, rule);
177
+ term(out, device, `${statement} term ${name}`, rule, part, split.length > 1);
178
+ }
179
+ }
180
+
181
+ const last = flat.at(-1)?.rule;
182
+ const decides = last !== undefined && !last.match && last.action !== undefined;
183
+ if (!decides) out.set(statement, "term circuit-default then reject");
184
+ }
185
+
186
+ function term(out: Statements, device: Device, term: string, rule: AnyRule, family: Family | undefined, splitFamily: boolean): void {
187
+ const match = rule.match ?? {};
188
+ const set = rule.set ?? {};
189
+ const from: (string | number)[][] = [];
190
+ const then: (string | number)[][] = [];
191
+
192
+ if (match.family || splitFamily) from.push(["family", FAMILY[(match.family ?? family)!]]);
193
+ if (match.prefix) from.push(["route-filter", match.prefix, "exact"]);
194
+ if (match.prefix_set) from.push(["prefix-list-filter", match.prefix_set, "orlonger"]);
195
+
196
+ if (match.prefix_length) {
197
+ const families = family ? [family] : (["ipv4", "ipv6"] as const);
198
+
199
+ for (const each of families) {
200
+ const longest = each === "ipv4" ? 32 : 128;
201
+ const range = `/${match.prefix_length.min ?? 0}-/${match.prefix_length.max ?? longest}`;
202
+ from.push(["route-filter", each === "ipv4" ? "0.0.0.0/0" : "::/0", "prefix-length-range", range]);
203
+ }
204
+ }
205
+
206
+ if (match.as_path_length) {
207
+ const { min = 0, max } = match.as_path_length;
208
+ const name = `circuit-path-length-${min}-${max ?? "any"}`;
209
+ out.set(`policy-options as-path ${name}`, quote(`.{${min},${max ?? ""}}`));
210
+ from.push(["as-path", name]);
211
+ }
212
+
213
+ if (match.rpki) from.push(["validation-database", VALIDATION[match.rpki]]);
214
+
215
+ // Members of one named community must all be present, so a rule matching both kinds names them together.
216
+ const wanted = [
217
+ ...(match.community ? [member(match.community, false)] : []),
218
+ ...(match.large_community ? [member(match.large_community, true)] : []),
219
+ ];
220
+
221
+ if (wanted.length) from.push(["community", community(out, "match", wanted)]);
222
+
223
+ if (set.local_pref !== undefined) then.push(["local-preference", set.local_pref]);
224
+ if (set.med !== undefined) then.push(["metric", set.med]);
225
+
226
+ if (set.prepend !== undefined) {
227
+ const asn = device.routing!.bgp!.asn;
228
+ then.push(["as-path-prepend", quote(Array(set.prepend).fill(asn).join(" "))]);
229
+ }
230
+
231
+ // Replacing communities of one kind is removing every one of that kind, then adding the new ones.
232
+ const removed = [
233
+ ...(set.communities ? ["*:*"] : (set.remove_communities ?? [])).map((pattern) => member(pattern, false)),
234
+ ...(set.large_communities ? ["*:*:*"] : (set.remove_large_communities ?? [])).map((pattern) => member(pattern, true)),
235
+ ];
236
+
237
+ const added = [
238
+ ...(set.communities ?? set.add_communities ?? []).map((value) => member(value, false)),
239
+ ...(set.large_communities ?? set.add_large_communities ?? []).map((value) => member(value, true)),
240
+ ];
241
+
242
+ if (set.communities && set.add_communities) added.push(...set.add_communities.map((value) => member(value, false)));
243
+ if (set.large_communities && set.add_large_communities) added.push(...set.add_large_communities.map((value) => member(value, true)));
244
+
245
+ for (const pattern of removed) then.push(["community delete", community(out, "remove", [pattern])]);
246
+ if (added.length) then.push(["community add", community(out, "add", added)]);
247
+
248
+ if (set.next_hop && (!family || familyOf(set.next_hop) === family)) then.push(["next-hop", set.next_hop]);
249
+ if (set.blackhole) then.push(["next-hop discard"]);
250
+ if (rule.action) then.push([rule.action]);
251
+
252
+ for (const words of from) out.set(term, "from", ...words);
253
+ for (const words of then) out.set(term, "then", ...words);
254
+
255
+ if (!from.length && !then.length) out.set(term);
256
+ }
257
+
258
+ /** A named community holding these members, named after them. */
259
+ function community(out: Statements, purpose: string, members: string[]): string {
260
+ const name = `circuit-${purpose}-${slug(members.join(" "))}`;
261
+ for (const value of members) out.set(`policy-options community ${name} members`, quote(value));
262
+ return name;
263
+ }