@takodotid/circuit 0.43.0 → 1.0.0-rc.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 (257) hide show
  1. package/README.md +115 -37
  2. package/package.json +21 -58
  3. package/src/checks/across.ts +402 -0
  4. package/src/checks/built-in.ts +256 -0
  5. package/src/checks/device.ts +436 -0
  6. package/src/checks/index.ts +53 -0
  7. package/src/cli/advisories.ts +108 -0
  8. package/src/cli/app.ts +67 -0
  9. package/src/cli/editor-types.generated.ts +30 -0
  10. package/src/cli/editor-types.ts +69 -0
  11. package/src/cli/format.ts +124 -0
  12. package/src/cli/git-hooks.ts +79 -0
  13. package/src/cli/index.ts +348 -1170
  14. package/src/cli/migrate.ts +554 -0
  15. package/src/cli/project-commands.ts +656 -0
  16. package/src/cli/releases.ts +374 -0
  17. package/src/cli/remember.ts +339 -0
  18. package/src/cli/rename.ts +27 -33
  19. package/src/cli/secrets-password.ts +60 -0
  20. package/src/cli/secrets.ts +258 -0
  21. package/src/cli/setup.ts +300 -0
  22. package/src/cli/signing.ts +23 -0
  23. package/src/cli/text-editor.ts +268 -0
  24. package/src/cli/ui.ts +298 -0
  25. package/src/cli/version.ts +2 -0
  26. package/src/cli/versions.ts +158 -0
  27. package/src/config/address.ts +102 -0
  28. package/src/config/circuit.d.ts +30 -0
  29. package/src/config/common.ts +65 -0
  30. package/src/config/communities.ts +479 -0
  31. package/src/config/define.ts +684 -0
  32. package/src/config/device.ts +164 -0
  33. package/src/config/firewall.ts +150 -0
  34. package/src/config/index.ts +22 -0
  35. package/src/{schema → config}/interfaces.ts +121 -85
  36. package/src/config/keys.ts +434 -0
  37. package/src/config/management.ts +61 -0
  38. package/src/config/merge.ts +335 -0
  39. package/src/config/models.ts +70 -0
  40. package/src/config/monitoring.ts +64 -0
  41. package/src/config/network.ts +180 -0
  42. package/src/config/presets.ts +149 -0
  43. package/src/config/routing.ts +401 -0
  44. package/src/config/services.ts +51 -0
  45. package/src/config/switching.ts +84 -0
  46. package/src/config/system.ts +103 -0
  47. package/src/{schema → config}/time-zones.ts +1 -1
  48. package/src/core/address.ts +1 -1
  49. package/src/core/cabling.ts +8 -43
  50. package/src/core/graph.ts +40 -0
  51. package/src/{integrations → core}/registries.ts +5 -3
  52. package/src/core/secrets-file.ts +539 -0
  53. package/src/core/secrets.ts +83 -64
  54. package/src/core/topology.ts +103 -146
  55. package/src/core/utils.ts +12 -1
  56. package/src/model.ts +83 -0
  57. package/src/platforms/derive.ts +64 -37
  58. package/src/platforms/finding.ts +9 -0
  59. package/src/platforms/fortios/firewall.ts +76 -64
  60. package/src/platforms/fortios/index.ts +189 -125
  61. package/src/platforms/fortios/names.ts +51 -18
  62. package/src/platforms/fortios/plan.ts +132 -44
  63. package/src/platforms/fortios/render.ts +69 -62
  64. package/src/platforms/fortios/routing.ts +89 -58
  65. package/src/platforms/fortios/vdoms.ts +47 -16
  66. package/src/platforms/junos/TRAPS.md +2 -2
  67. package/src/platforms/junos/bgp.ts +63 -42
  68. package/src/platforms/junos/index.ts +122 -89
  69. package/src/platforms/junos/render.ts +139 -104
  70. package/src/platforms/platform.ts +10 -8
  71. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  72. package/src/platforms/raisecom-ros/index.ts +89 -70
  73. package/src/platforms/raisecom-ros/render.ts +49 -45
  74. package/src/platforms/routeros/TRAPS.md +3 -3
  75. package/src/platforms/routeros/index.ts +121 -60
  76. package/src/platforms/routeros/render.ts +214 -195
  77. package/src/platforms/vrp/TRAPS.md +1 -1
  78. package/src/platforms/vrp/index.ts +74 -50
  79. package/src/platforms/vrp/render.ts +67 -56
  80. package/src/project/project.ts +725 -0
  81. package/src/studio/app/env.d.ts +9 -0
  82. package/src/studio/app/index.html +18 -0
  83. package/src/studio/app/src/App.vue +95 -0
  84. package/src/studio/app/src/components/AddressesView.vue +38 -0
  85. package/src/studio/app/src/components/CablesView.vue +29 -0
  86. package/src/studio/app/src/components/CompareView.vue +160 -0
  87. package/src/studio/app/src/components/DataTable.vue +49 -0
  88. package/src/studio/app/src/components/DeviceView.vue +237 -0
  89. package/src/studio/app/src/components/DevicesView.vue +32 -0
  90. package/src/studio/app/src/components/Faceplate.vue +75 -0
  91. package/src/studio/app/src/components/GraphView.vue +282 -0
  92. package/src/studio/app/src/components/LineDiff.vue +24 -0
  93. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  94. package/src/studio/app/src/components/SearchView.vue +72 -0
  95. package/src/studio/app/src/components/SecretsView.vue +112 -0
  96. package/src/studio/app/src/components/SessionsView.vue +35 -0
  97. package/src/studio/app/src/components/Settings.vue +95 -0
  98. package/src/studio/app/src/components/ToolsView.vue +118 -0
  99. package/src/studio/app/src/components/VlansView.vue +50 -0
  100. package/src/studio/app/src/lib/csv.ts +19 -0
  101. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  102. package/src/studio/app/src/lib/format.ts +38 -0
  103. package/src/studio/app/src/lib/net.ts +68 -0
  104. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  105. package/src/studio/app/src/lib/store.ts +71 -0
  106. package/src/studio/app/src/lib/tools.ts +217 -0
  107. package/src/studio/app/src/main.ts +9 -0
  108. package/src/studio/{public → app/src}/style.css +14 -4
  109. package/src/studio/app/tsconfig.json +17 -0
  110. package/src/studio/app/vite.config.ts +16 -0
  111. package/src/studio/assets.generated.ts +3 -0
  112. package/src/studio/data.ts +96 -188
  113. package/src/studio/git.ts +27 -43
  114. package/src/studio/server.ts +196 -107
  115. package/src/studio/types.ts +141 -0
  116. package/src/transport/console.ts +3 -3
  117. package/src/transport/ssh.ts +1 -1
  118. package/dist/chunks/index-3b343xva.js +0 -177
  119. package/dist/chunks/index-6wb41q67.js +0 -62
  120. package/dist/chunks/index-n7w3tg5y.js +0 -149
  121. package/dist/chunks/index-yzw86rc8.js +0 -921
  122. package/dist/chunks/new-w3zys5y4.js +0 -172
  123. package/dist/chunks/server-cxr9ynxc.js +0 -259
  124. package/dist/cli/index.js +0 -12872
  125. package/dist/helpers/index.js +0 -881
  126. package/dist/index.js +0 -28
  127. package/dist/types/core/address-space.d.ts +0 -62
  128. package/dist/types/core/address.d.ts +0 -44
  129. package/dist/types/core/cabling.d.ts +0 -41
  130. package/dist/types/core/cross-checks.d.ts +0 -28
  131. package/dist/types/core/define.d.ts +0 -79
  132. package/dist/types/core/project-files.d.ts +0 -12
  133. package/dist/types/core/schema-docs.d.ts +0 -25
  134. package/dist/types/core/secrets.d.ts +0 -62
  135. package/dist/types/core/topology.d.ts +0 -96
  136. package/dist/types/core/utils.d.ts +0 -10
  137. package/dist/types/core/validator.d.ts +0 -23
  138. package/dist/types/helpers/bgp.d.ts +0 -195
  139. package/dist/types/helpers/cables.d.ts +0 -69
  140. package/dist/types/helpers/checks.d.ts +0 -36
  141. package/dist/types/helpers/exchange.d.ts +0 -55
  142. package/dist/types/helpers/filters.d.ts +0 -51
  143. package/dist/types/helpers/index.d.ts +0 -14
  144. package/dist/types/helpers/route-server.d.ts +0 -52
  145. package/dist/types/helpers/upstreams.d.ts +0 -192
  146. package/dist/types/index.d.ts +0 -10
  147. package/dist/types/integrations/librenms.d.ts +0 -60
  148. package/dist/types/integrations/netbox.d.ts +0 -89
  149. package/dist/types/integrations/peeringdb.d.ts +0 -24
  150. package/dist/types/integrations/prometheus.d.ts +0 -24
  151. package/dist/types/integrations/registries.d.ts +0 -8
  152. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  153. package/dist/types/platforms/catalog.d.ts +0 -109
  154. package/dist/types/platforms/derive.d.ts +0 -17
  155. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  156. package/dist/types/platforms/fortios/index.d.ts +0 -140
  157. package/dist/types/platforms/fortios/models.d.ts +0 -14
  158. package/dist/types/platforms/fortios/names.d.ts +0 -52
  159. package/dist/types/platforms/fortios/plan.d.ts +0 -104
  160. package/dist/types/platforms/fortios/render.d.ts +0 -77
  161. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  162. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  163. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  164. package/dist/types/platforms/index.d.ts +0 -13
  165. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  166. package/dist/types/platforms/junos/index.d.ts +0 -114
  167. package/dist/types/platforms/junos/models.d.ts +0 -14
  168. package/dist/types/platforms/junos/render.d.ts +0 -154
  169. package/dist/types/platforms/line-diff.d.ts +0 -67
  170. package/dist/types/platforms/platform.d.ts +0 -135
  171. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  172. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  173. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  174. package/dist/types/platforms/routeros/export.d.ts +0 -59
  175. package/dist/types/platforms/routeros/index.d.ts +0 -116
  176. package/dist/types/platforms/routeros/models.d.ts +0 -18
  177. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  178. package/dist/types/platforms/routeros/render.d.ts +0 -250
  179. package/dist/types/platforms/vrp/index.d.ts +0 -83
  180. package/dist/types/platforms/vrp/models.d.ts +0 -12
  181. package/dist/types/platforms/vrp/render.d.ts +0 -96
  182. package/dist/types/schema/common.d.ts +0 -30
  183. package/dist/types/schema/device.d.ts +0 -126
  184. package/dist/types/schema/firewall.d.ts +0 -131
  185. package/dist/types/schema/index.d.ts +0 -8
  186. package/dist/types/schema/interfaces.d.ts +0 -205
  187. package/dist/types/schema/policy.d.ts +0 -107
  188. package/dist/types/schema/routing.d.ts +0 -161
  189. package/dist/types/schema/system.d.ts +0 -198
  190. package/dist/types/schema/time-zones.d.ts +0 -2
  191. package/dist/types/transport/console.d.ts +0 -115
  192. package/dist/types/transport/path-check.d.ts +0 -32
  193. package/dist/types/transport/ssh.d.ts +0 -27
  194. package/src/cli/graph-format.ts +0 -91
  195. package/src/cli/new.ts +0 -234
  196. package/src/core/address-space.ts +0 -320
  197. package/src/core/cross-checks.ts +0 -369
  198. package/src/core/define.ts +0 -124
  199. package/src/core/project-files.ts +0 -35
  200. package/src/core/schema-docs.ts +0 -174
  201. package/src/core/validator.ts +0 -508
  202. package/src/helpers/bgp.ts +0 -473
  203. package/src/helpers/cables.ts +0 -74
  204. package/src/helpers/checks.ts +0 -201
  205. package/src/helpers/exchange.ts +0 -123
  206. package/src/helpers/filters.ts +0 -107
  207. package/src/helpers/index.ts +0 -25
  208. package/src/helpers/route-server.ts +0 -212
  209. package/src/helpers/upstreams.ts +0 -369
  210. package/src/index.ts +0 -13
  211. package/src/integrations/librenms.ts +0 -291
  212. package/src/integrations/netbox.ts +0 -620
  213. package/src/integrations/peeringdb.ts +0 -173
  214. package/src/integrations/prometheus.ts +0 -90
  215. package/src/schema/common.ts +0 -41
  216. package/src/schema/device.ts +0 -145
  217. package/src/schema/firewall.ts +0 -140
  218. package/src/schema/index.ts +0 -8
  219. package/src/schema/policy.ts +0 -115
  220. package/src/schema/routing.ts +0 -173
  221. package/src/schema/system.ts +0 -203
  222. package/src/studio/public/app.js +0 -186
  223. package/src/studio/public/compare.js +0 -250
  224. package/src/studio/public/dom.js +0 -138
  225. package/src/studio/public/evaluate.js +0 -687
  226. package/src/studio/public/export.js +0 -130
  227. package/src/studio/public/graph.js +0 -1541
  228. package/src/studio/public/index.html +0 -83
  229. package/src/studio/public/net.js +0 -134
  230. package/src/studio/public/tables.js +0 -1289
  231. package/src/studio/public/tools.js +0 -610
  232. package/templates/colocation/checks.ts +0 -53
  233. package/templates/colocation/circuit.config.ts +0 -9
  234. package/templates/colocation/edge-01.ts +0 -133
  235. package/templates/colocation/routing.ts +0 -13
  236. package/templates/colocation/tenancy.ts +0 -76
  237. package/templates/colocation/tenants/acme.ts +0 -19
  238. package/templates/colocation/tenants/index.ts +0 -6
  239. package/templates/colocation/tor-01.ts +0 -33
  240. package/templates/edge-router/checks.ts +0 -20
  241. package/templates/edge-router/circuit.config.ts +0 -8
  242. package/templates/edge-router/edge-01.ts +0 -110
  243. package/templates/edge-router/routing.ts +0 -13
  244. package/templates/project/AGENTS.md +0 -51
  245. package/templates/project/CLAUDE.md +0 -1
  246. package/templates/project/README.md +0 -16
  247. package/templates/project/gitignore +0 -5
  248. package/templates/project/tsconfig.json +0 -13
  249. package/templates/single-site/checks.ts +0 -26
  250. package/templates/single-site/circuit.config.ts +0 -8
  251. package/templates/single-site/router.ts +0 -86
  252. package/templates/single-site/site.ts +0 -18
  253. package/templates/single-site/switch.ts +0 -34
  254. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  256. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  257. /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
@@ -1,687 +0,0 @@
1
- // @ts-check
2
- // What Studio's tools work out from the config: where an address is and how each device would reach it, the VLAN path between two devices, which firewall rule a packet meets, and what a route policy does to a route.
3
- // They follow the schema's own description of each field, read from the files: a device can still behave otherwise, so they say what the config means, not what the device did.
4
-
5
- import { Net } from "./net.js";
6
-
7
- /** @typedef {import("../../schema").Device} Device */
8
- /** @typedef {import("../../schema").FilterMatch<string, string>} FilterMatch */
9
- /** @typedef {import("../../schema").FilterRule<string, string>} FilterRule */
10
- /** @typedef {import("../../schema").PolicyRule<string, string>} PolicyRule */
11
- /** @typedef {import("../../schema").Match<string>} RouteMatch */
12
- /** @typedef {import("../../schema").Sets} Sets */
13
- /** @typedef {import("../../schema").Ports} Ports */
14
- /** @typedef {import("../../core/topology").GraphEdge} GraphEdge */
15
- /** @typedef {import("../data").StudioData} StudioData */
16
-
17
- /** Whether a condition holds: yes, no, or not known from what was given. @typedef {"yes" | "no" | "unknown"} Answer */
18
- /** @typedef {{ answer: Answer, why: string[] }} Check */
19
- /** @typedef {{ device: string, table: string, kind: "local" | "connected" | "static" | "none", prefix?: string, via?: string, through?: string, note?: string }} RouteFound */
20
- /** @typedef {{ device: string, where: string, rule: number, description?: string, action: string, side: string }} RuleFound */
21
- /** @typedef {{ family?: "ipv4" | "ipv6", protocol?: string, src?: string, dst?: string, srcPort?: number, dstPort?: number, inInterface?: string, outInterface?: string, state?: string, flags?: string[] }} Packet */
22
- /** @typedef {{ prefix: string, asPathLength?: number, rpki?: string, communities: string[], largeCommunities: string[], inRegistrySets: string[] }} Route */
23
- /** @typedef {{ localPref?: number, med?: number, prepend: number, communities: string[], largeCommunities: string[], nextHop?: string, preferredSource?: string, blackhole?: boolean }} Attributes */
24
- /** @typedef {{ policy: string, rule: number, description?: string, check: Check, changed: string[], decision?: string, call?: string }} PolicyStep */
25
-
26
- export class Evaluate {
27
- /**
28
- * Every condition must hold. One that does not decides; otherwise one not known leaves the whole not known.
29
- *
30
- * @param {Check[]} checks
31
- * @returns {Check}
32
- */
33
- static #all(checks) {
34
- const no = checks.filter((check) => check.answer === "no");
35
- if (no.length) return { answer: "no", why: no.flatMap((check) => check.why) };
36
-
37
- const unknown = checks.filter((check) => check.answer === "unknown");
38
- if (unknown.length) return { answer: "unknown", why: unknown.flatMap((check) => check.why) };
39
-
40
- return { answer: "yes", why: [] };
41
- }
42
-
43
- /**
44
- * @param {boolean} holds
45
- * @param {string} why
46
- * @returns {Check}
47
- */
48
- static #test(holds, why) {
49
- return { answer: holds ? "yes" : "no", why: holds ? [] : [why] };
50
- }
51
-
52
- /**
53
- * @param {string} why
54
- * @returns {Check}
55
- */
56
- static #unknown(why) {
57
- return { answer: "unknown", why: [why] };
58
- }
59
-
60
- // Where an address is.
61
-
62
- /**
63
- * The addresses on a device's ports and interfaces, with where each is and the VRF it routes in.
64
- *
65
- * @param {Device} device
66
- */
67
- static #addressesOf(device) {
68
- const routed =
69
- /** @type {[string, { addresses?: readonly (string | { address: string, peer: string })[], vrf?: string } | undefined][]} */ ([
70
- ...Object.entries(device.ports ?? {}),
71
- ...Object.entries(device.interfaces ?? {}),
72
- ]);
73
-
74
- return routed.flatMap(([name, settings]) =>
75
- (settings?.addresses ?? []).map((entry) => ({
76
- name,
77
- address: typeof entry === "string" ? entry : entry.address,
78
- peer: typeof entry === "string" ? undefined : entry.peer,
79
- vrf: settings?.vrf,
80
- }))
81
- );
82
- }
83
-
84
- /**
85
- * Where an address is in the network, how each device would reach it from what the files say, and every rule that names it.
86
- *
87
- * @param {StudioData} data
88
- * @param {string} text an address, such as `10.0.10.5`
89
- */
90
- static trace(data, text) {
91
- const target = Net.parseRange(text);
92
- if (!target || target.first !== target.last) return undefined;
93
-
94
- // Who holds it, and the segment or block it is in.
95
- const held = data.devices.flatMap((device) =>
96
- Evaluate.#addressesOf(device)
97
- .filter((entry) => Net.samePrefix(entry.address.split("/")[0] ?? "", text))
98
- .map((entry) => ({ device: device.name, where: entry.name, address: entry.address }))
99
- );
100
-
101
- const { spaces, elsewhere } = data.addresses;
102
- const segments = [...spaces.flatMap((space) => space.segments), ...elsewhere.segments];
103
- const blocks = [...spaces.flatMap((space) => space.blocks), ...elsewhere.blocks];
104
- const segment = segments.find((candidate) => Net.covers(candidate.prefix, text));
105
- const block = blocks.find((candidate) => Net.covers(candidate.prefix, text));
106
- const space = spaces.find((candidate) => Net.covers(candidate.prefix, text));
107
- const uses = segment?.uses.filter((use) => {
108
- const [first, last] = use.address.split("-");
109
- if (!last) return Net.samePrefix(first ?? "", text);
110
- const from = Net.parseRange(first ?? "");
111
- const to = Net.parseRange(last);
112
- return Boolean(from && to && from.first <= target.first && target.first <= to.first);
113
- });
114
-
115
- // How each device would reach it: the longest prefix that covers it, in each routing table, among its own addresses and its static routes.
116
- /** @type {RouteFound[]} */
117
- const routes = [];
118
-
119
- for (const device of data.devices) {
120
- /** @type {Map<string, RouteFound & { length: number, rank: number }>} */
121
- const best = new Map();
122
-
123
- /** @param {RouteFound & { length: number, rank: number }} found */
124
- const offer = (found) => {
125
- const current = best.get(found.table);
126
- const better = !current || found.length > current.length || (found.length === current.length && found.rank < current.rank);
127
- if (better) best.set(found.table, found);
128
- };
129
-
130
- for (const entry of Evaluate.#addressesOf(device)) {
131
- const table = entry.vrf ? `VRF ${entry.vrf}` : "main";
132
- const range = Net.parseRange(entry.address);
133
- if (!range) continue;
134
-
135
- if (Net.samePrefix(entry.address.split("/")[0] ?? "", text)) {
136
- offer({ device: device.name, table, kind: "local", prefix: entry.address, through: entry.name, length: 129, rank: 0 });
137
- } else if (entry.peer && Net.samePrefix(entry.peer, text)) {
138
- offer({
139
- device: device.name,
140
- table,
141
- kind: "connected",
142
- prefix: `${entry.peer} as the peer of ${entry.address}`,
143
- through: entry.name,
144
- length: 128,
145
- rank: 1,
146
- });
147
- } else if (!entry.peer && Net.covers(entry.address, text)) {
148
- const network = Net.prefixOf(entry.address);
149
- offer({
150
- device: device.name,
151
- table,
152
- kind: "connected",
153
- prefix: network,
154
- through: entry.name,
155
- length: range.length,
156
- rank: 1,
157
- });
158
- }
159
- }
160
-
161
- for (const route of device.routing?.static ?? []) {
162
- const range = Net.parseRange(route.prefix);
163
- if (!range || !Net.covers(route.prefix, text)) continue;
164
-
165
- const table = route.vrf ? `VRF ${route.vrf}` : "main";
166
- offer({
167
- device: device.name,
168
- table,
169
- kind: "static",
170
- prefix: route.prefix,
171
- via: route.via,
172
- through: route.interface,
173
- note: route.blackhole ? "discarded: a blackhole route" : route.description,
174
- length: range.length,
175
- rank: 2 + (route.distance ?? 1),
176
- });
177
- }
178
-
179
- if (!best.size) routes.push({ device: device.name, table: "main", kind: "none" });
180
- for (const found of best.values()) {
181
- const { length: _length, rank: _rank, ...route } = found;
182
- routes.push(route);
183
- }
184
- }
185
-
186
- // Every rule that names it, by address, prefix or address set.
187
- /** @type {RuleFound[]} */
188
- const rules = [];
189
-
190
- for (const device of data.devices) {
191
- const sets = device.firewall?.address_sets ?? {};
192
- const inSet = (/** @type {string | undefined} */ name) =>
193
- Boolean(name && (sets[name] ?? []).some((entry) => Net.covers(entry, text)));
194
-
195
- /**
196
- * @param {string} where
197
- * @param {readonly { description?: string, match?: { src?: string, dst?: string, src_set?: string, dst_set?: string }, action: string }[]} list
198
- */
199
- const look = (where, list) => {
200
- for (const [index, rule] of list.entries()) {
201
- const match = rule.match ?? {};
202
- const sides = [
203
- (match.src && Net.covers(match.src, text)) || inSet(match.src_set) ? "source" : "",
204
- (match.dst && Net.covers(match.dst, text)) || inSet(match.dst_set) ? "destination" : "",
205
- ].filter(Boolean);
206
- if (sides.length)
207
- rules.push({
208
- device: device.name,
209
- where,
210
- rule: index + 1,
211
- description: rule.description,
212
- action: rule.action,
213
- side: sides.join(" and "),
214
- });
215
- }
216
- };
217
-
218
- for (const [chain, settings] of Object.entries(device.firewall?.filter ?? {})) look(`filter ${chain}`, settings?.rules ?? []);
219
- for (const [chain, list] of Object.entries(device.firewall?.nat ?? {})) look(`NAT ${chain}`, list ?? []);
220
- for (const [name, list] of Object.entries(device.acls ?? {})) look(`ACL ${name}`, list);
221
- }
222
-
223
- return { address: text, family: target.family, held, segment, uses: uses ?? [], block, space, routes, rules };
224
- }
225
-
226
- // The VLAN path between two devices.
227
-
228
- /**
229
- * The fewest cables that carry a VLAN from one device to another, from the graph's VLAN layer. Undefined when no such path exists.
230
- *
231
- * @param {StudioData} data
232
- * @param {number} vlan
233
- * @param {string} from device name
234
- * @param {string} to device name
235
- * @returns {GraphEdge[] | undefined}
236
- */
237
- static vlanPath(data, vlan, from, to) {
238
- const start = `device:${from}`;
239
- const goal = `device:${to}`;
240
- const cables = data.graph.edges.filter((edge) => edge.layer === "l2" && edge.vlans?.some((carried) => carried.id === vlan));
241
-
242
- /** @type {Map<string, GraphEdge | null>} */
243
- const cameBy = new Map([[start, null]]);
244
- const queue = [start];
245
-
246
- for (let at = queue.shift(); at !== undefined; at = queue.shift()) {
247
- if (at === goal) break;
248
-
249
- for (const cable of cables) {
250
- const next = cable.from === at ? cable.to : cable.to === at ? cable.from : undefined;
251
- if (next === undefined || cameBy.has(next)) continue;
252
-
253
- cameBy.set(next, cable);
254
- queue.push(next);
255
- }
256
- }
257
-
258
- if (!cameBy.has(goal)) return undefined;
259
-
260
- /** @type {GraphEdge[]} */
261
- const path = [];
262
- for (let at = goal, cable = cameBy.get(goal); cable; cable = cameBy.get(at)) {
263
- path.unshift(cable);
264
- at = cable.from === at ? cable.to : cable.from;
265
- }
266
-
267
- return path;
268
- }
269
-
270
- // Which firewall rule a packet meets.
271
-
272
- /**
273
- * @param {Ports} ports
274
- * @param {number} port
275
- */
276
- static #portIn(ports, port) {
277
- const list = Array.isArray(ports) ? ports : [ports];
278
- return list.some((entry) => (typeof entry === "number" ? entry === port : entry.min <= port && port <= entry.max));
279
- }
280
-
281
- /**
282
- * Whether a packet meets a rule's conditions, as the schema describes each one.
283
- *
284
- * @param {FilterMatch} match
285
- * @param {Packet} packet
286
- * @param {Record<string, readonly string[]>} sets the device's address sets
287
- * @returns {Check}
288
- */
289
- static #packetMeets(match, packet, sets) {
290
- /** @type {Check[]} */
291
- const checks = [];
292
- const family = packet.family ?? Net.family(packet.src ?? "") ?? Net.family(packet.dst ?? "");
293
-
294
- if (match.family)
295
- checks.push(
296
- family ? Evaluate.#test(match.family === family, `it is for ${match.family} only`) : Evaluate.#unknown("its family")
297
- );
298
-
299
- // A rule with addresses of one family applies to that family only.
300
- for (const address of [match.src, match.dst]) {
301
- const own = address ? Net.family(address) : undefined;
302
- if (own && family && own !== family) checks.push(Evaluate.#test(false, `it names ${own} addresses`));
303
- }
304
-
305
- if (match.protocol)
306
- checks.push(
307
- packet.protocol
308
- ? Evaluate.#test(match.protocol === packet.protocol, `it is for ${match.protocol}`)
309
- : Evaluate.#unknown("the protocol")
310
- );
311
-
312
- /**
313
- * @param {string | undefined} rule
314
- * @param {string | undefined} given
315
- * @param {string} side
316
- */
317
- const address = (rule, given, side) => {
318
- if (!rule) return;
319
- checks.push(
320
- given ? Evaluate.#test(Net.covers(rule, given), `its ${side} is ${rule}`) : Evaluate.#unknown(`the ${side} address`)
321
- );
322
- };
323
-
324
- address(match.src, packet.src, "source");
325
- address(match.dst, packet.dst, "destination");
326
-
327
- /**
328
- * @param {string | undefined} name
329
- * @param {string | undefined} given
330
- * @param {string} side
331
- */
332
- const set = (name, given, side) => {
333
- if (!name) return;
334
- const members = sets[name] ?? [];
335
- checks.push(
336
- given
337
- ? Evaluate.#test(
338
- members.some((entry) => Net.covers(entry, given)),
339
- `its ${side} is in the set ${name}`
340
- )
341
- : Evaluate.#unknown(`the ${side} address`)
342
- );
343
- };
344
-
345
- set(match.src_set, packet.src, "source");
346
- set(match.dst_set, packet.dst, "destination");
347
-
348
- /**
349
- * @param {Ports | undefined} rule
350
- * @param {number | undefined} given
351
- * @param {string} side
352
- */
353
- const port = (rule, given, side) => {
354
- if (rule === undefined) return;
355
- if (packet.protocol && packet.protocol !== "tcp" && packet.protocol !== "udp")
356
- return checks.push(Evaluate.#test(false, "it names ports, which only TCP and UDP have"));
357
- checks.push(
358
- given !== undefined
359
- ? Evaluate.#test(Evaluate.#portIn(rule, given), `its ${side} port is ${JSON.stringify(rule)}`)
360
- : Evaluate.#unknown(`the ${side} port`)
361
- );
362
- };
363
-
364
- port(match.src_port, packet.srcPort, "source");
365
- port(match.dst_port, packet.dstPort, "destination");
366
-
367
- if (match.in_interface)
368
- checks.push(
369
- packet.inInterface
370
- ? Evaluate.#test(match.in_interface === packet.inInterface, `it is for packets arriving on ${match.in_interface}`)
371
- : Evaluate.#unknown("where the packet arrives")
372
- );
373
- if (match.out_interface)
374
- checks.push(
375
- packet.outInterface
376
- ? Evaluate.#test(
377
- match.out_interface === packet.outInterface,
378
- `it is for packets leaving through ${match.out_interface}`
379
- )
380
- : Evaluate.#unknown("where the packet leaves")
381
- );
382
- if (match.state)
383
- checks.push(
384
- packet.state
385
- ? Evaluate.#test(
386
- match.state.includes(/** @type {never} */ (packet.state)),
387
- `it is for ${match.state.join(" or ")} packets`
388
- )
389
- : Evaluate.#unknown("the connection state")
390
- );
391
-
392
- if (match.tcp_flags) {
393
- if (packet.protocol && packet.protocol !== "tcp") checks.push(Evaluate.#test(false, "it names TCP flags"));
394
- else if (!packet.flags) checks.push(Evaluate.#unknown("the TCP flags"));
395
- else {
396
- const flags = packet.flags;
397
- const set = match.tcp_flags.set ?? [];
398
- const unset = match.tcp_flags.unset ?? [];
399
- checks.push(
400
- Evaluate.#test(
401
- set.every((flag) => flags.includes(flag)) && unset.every((flag) => !flags.includes(flag)),
402
- "its TCP flags differ"
403
- )
404
- );
405
- }
406
- }
407
-
408
- return Evaluate.#all(checks);
409
- }
410
-
411
- /**
412
- * A packet through one of a device's filter chains: each rule in order until one matches, then the chain's default, accept when it has none.
413
- *
414
- * @param {Device} device
415
- * @param {"input" | "forward" | "output"} chain
416
- * @param {Packet} packet
417
- */
418
- static filterPacket(device, chain, packet) {
419
- const settings = device.firewall?.filter?.[chain];
420
- const rules = settings?.rules ?? [];
421
- const sets = device.firewall?.address_sets ?? {};
422
-
423
- /** @type {{ rule: number, description?: string, action: string, check: Check }[]} */
424
- const steps = [];
425
-
426
- for (const [index, rule] of rules.entries()) {
427
- const check = Evaluate.#packetMeets(rule.match ?? {}, packet, sets);
428
- steps.push({ rule: index + 1, description: rule.description, action: rule.action, check });
429
-
430
- if (check.answer === "yes") return { steps, verdict: rule.action, by: `rule ${index + 1}` };
431
- if (check.answer === "unknown") return { steps, verdict: undefined, by: `rule ${index + 1}` };
432
- }
433
-
434
- return {
435
- steps,
436
- verdict: settings?.default ?? "accept",
437
- by: settings?.default ? "the chain's default" : "the chain's default, accept when none is written",
438
- };
439
- }
440
-
441
- // What a route policy does to a route.
442
-
443
- /**
444
- * Whether a route meets a policy rule's conditions, as the schema describes each one. A prefix set holds a route whose prefix is one of its own or inside one.
445
- *
446
- * @param {RouteMatch} match
447
- * @param {Route} route
448
- * @param {Device} device
449
- * @returns {Check}
450
- */
451
- static #routeMeets(match, route, device) {
452
- /** @type {Check[]} */
453
- const checks = [];
454
- const range = Net.parseRange(route.prefix);
455
- const family = range?.family;
456
-
457
- if (match.family) checks.push(Evaluate.#test(match.family === family, `it is for ${match.family} only`));
458
- if (match.prefix) checks.push(Evaluate.#test(Net.samePrefix(match.prefix, route.prefix), `it is for ${match.prefix} exactly`));
459
-
460
- if (match.prefix_set) {
461
- const set = device.prefix_sets?.[match.prefix_set];
462
- if (set && "source" in set)
463
- checks.push(
464
- Evaluate.#test(
465
- route.inRegistrySets.includes(match.prefix_set),
466
- `the route is not in ${match.prefix_set}, fetched from ${set.source.registry}`
467
- )
468
- );
469
- else
470
- checks.push(
471
- Evaluate.#test(
472
- Boolean(set?.prefixes.some((prefix) => Net.covers(prefix, route.prefix))),
473
- `the route is not in the prefix set ${match.prefix_set}`
474
- )
475
- );
476
- }
477
-
478
- const length = range?.length ?? 0;
479
- if (match.prefix_length?.min !== undefined)
480
- checks.push(Evaluate.#test(length >= match.prefix_length.min, `it is for /${match.prefix_length.min} or longer`));
481
- if (match.prefix_length?.max !== undefined)
482
- checks.push(Evaluate.#test(length <= match.prefix_length.max, `it is for /${match.prefix_length.max} or shorter`));
483
-
484
- if (match.as_path_length) {
485
- const { min, max } = match.as_path_length;
486
- if (route.asPathLength === undefined) checks.push(Evaluate.#unknown("the AS path length"));
487
- else {
488
- if (min !== undefined) checks.push(Evaluate.#test(route.asPathLength >= min, `it is for AS paths of ${min} or more`));
489
- if (max !== undefined) checks.push(Evaluate.#test(route.asPathLength <= max, `it is for AS paths of ${max} or fewer`));
490
- }
491
- }
492
-
493
- if (match.rpki)
494
- checks.push(
495
- route.rpki
496
- ? Evaluate.#test(match.rpki === route.rpki, `it is for routes RPKI finds ${match.rpki}`)
497
- : Evaluate.#unknown("the RPKI state")
498
- );
499
- if (match.community)
500
- checks.push(Evaluate.#test(route.communities.includes(match.community), `the route does not carry ${match.community}`));
501
- if (match.large_community)
502
- checks.push(
503
- Evaluate.#test(route.largeCommunities.includes(match.large_community), `the route does not carry ${match.large_community}`)
504
- );
505
-
506
- return Evaluate.#all(checks);
507
- }
508
-
509
- /**
510
- * Whether a community matches a pattern in which `*` stands for any value of a field.
511
- *
512
- * @param {string} pattern
513
- * @param {string} community
514
- */
515
- static #fits(pattern, community) {
516
- const want = pattern.split(":");
517
- const have = community.split(":");
518
- return want.length === have.length && want.every((field, index) => field === "*" || field === have[index]);
519
- }
520
-
521
- /**
522
- * Apply a rule's changes to a route's attributes, and say what changed.
523
- *
524
- * @param {Sets} set
525
- * @param {Attributes} attributes
526
- */
527
- static #apply(set, attributes) {
528
- /** @type {string[]} */
529
- const changed = [];
530
-
531
- if (set.local_pref !== undefined) changed.push(`local preference ${(attributes.localPref = set.local_pref)}`);
532
- if (set.med !== undefined) changed.push(`MED ${(attributes.med = set.med)}`);
533
- if (set.prepend)
534
- changed.push(
535
- `prepend our AS ${set.prepend} ${set.prepend === 1 ? "time" : "times"} (${(attributes.prepend += set.prepend)} in all)`
536
- );
537
-
538
- /**
539
- * @param {"communities" | "largeCommunities"} key
540
- * @param {readonly string[] | undefined} replace
541
- * @param {readonly string[] | undefined} add
542
- * @param {readonly string[] | undefined} remove
543
- * @param {string} name
544
- */
545
- const communities = (key, replace, add, remove, name) => {
546
- if (replace) changed.push(`${name} set to ${(attributes[key] = [...replace]).join(", ") || "none"}`);
547
- if (remove?.length) {
548
- const before = attributes[key].length;
549
- attributes[key] = attributes[key].filter((community) => !remove.some((pattern) => Evaluate.#fits(pattern, community)));
550
- changed.push(`${before - attributes[key].length} ${name} removed by ${remove.join(", ")}`);
551
- }
552
- if (add?.length) {
553
- attributes[key] = [...new Set([...attributes[key], ...add])];
554
- changed.push(`${name} added: ${add.join(", ")}`);
555
- }
556
- };
557
-
558
- communities("communities", set.communities, set.add_communities, set.remove_communities, "communities");
559
- communities(
560
- "largeCommunities",
561
- set.large_communities,
562
- set.add_large_communities,
563
- set.remove_large_communities,
564
- "large communities"
565
- );
566
-
567
- if (set.next_hop) changed.push(`next hop ${(attributes.nextHop = set.next_hop)}`);
568
- if (set.preferred_source) changed.push(`preferred source ${(attributes.preferredSource = set.preferred_source)}`);
569
- if (set.blackhole) {
570
- attributes.blackhole = true;
571
- changed.push("blackholed: traffic to it is dropped here");
572
- }
573
-
574
- return changed;
575
- }
576
-
577
- /**
578
- * A route through one of a device's route policies: `call`, then `match`, then `set`, then `action`, rule by rule, until a decision. A route the policy itself never accepts is rejected; one a called policy leaves undecided goes on in the caller.
579
- *
580
- * @param {Device} device
581
- * @param {string} policy
582
- * @param {Route} route
583
- */
584
- static routeThrough(device, policy, route) {
585
- /** @type {PolicyStep[]} */
586
- const steps = [];
587
- /** @type {Attributes} */
588
- const attributes = { prepend: 0, communities: [...route.communities], largeCommunities: [...route.largeCommunities] };
589
-
590
- /**
591
- * @param {string} name
592
- * @param {number} depth
593
- * @returns {"accept" | "reject" | "unknown" | undefined}
594
- */
595
- const run = (name, depth) => {
596
- const rules = device.policies?.[name] ?? [];
597
- if (depth > 16) return "unknown";
598
-
599
- for (const [index, rule] of rules.entries()) {
600
- const step = { policy: name, rule: index + 1, description: rule.description };
601
-
602
- if (rule.call) {
603
- const called = run(rule.call, depth + 1);
604
- if (called) {
605
- steps.push({ ...step, call: rule.call, check: { answer: "yes", why: [] }, changed: [], decision: called });
606
- return called;
607
- }
608
- }
609
-
610
- const current = { ...route, communities: attributes.communities, largeCommunities: attributes.largeCommunities };
611
- const check = Evaluate.#routeMeets(rule.match ?? {}, current, device);
612
-
613
- if (check.answer === "unknown") {
614
- steps.push({ ...step, ...(rule.call ? { call: rule.call } : {}), check, changed: [] });
615
- return "unknown";
616
- }
617
-
618
- if (check.answer === "no") {
619
- steps.push({ ...step, ...(rule.call ? { call: rule.call } : {}), check, changed: [] });
620
- continue;
621
- }
622
-
623
- const changed = Evaluate.#apply(rule.set ?? {}, attributes);
624
- steps.push({
625
- ...step,
626
- ...(rule.call ? { call: rule.call } : {}),
627
- check,
628
- changed,
629
- ...(rule.action ? { decision: rule.action } : {}),
630
- });
631
- if (rule.action) return rule.action;
632
- }
633
-
634
- return undefined;
635
- };
636
-
637
- const decided = run(policy, 0);
638
- return { steps, decision: decided ?? "reject", byDefault: decided === undefined, attributes };
639
- }
640
-
641
- // What changed between two networks.
642
-
643
- /**
644
- * An edge's place in the graph, the same whichever way round it was written, to match it across two networks.
645
- *
646
- * @param {GraphEdge} edge
647
- */
648
- static changeKey(edge) {
649
- return `${edge.layer} ${[edge.from, edge.to].sort().join(" ")} ${edge.label}`;
650
- }
651
-
652
- /**
653
- * What differs between the network before and after: devices added, removed or rendered differently; lines of the graph added or gone; and what validate finds newly or no longer.
654
- *
655
- * @param {StudioData} before
656
- * @param {StudioData} after
657
- */
658
- static compareNetworks(before, after) {
659
- const names = (/** @type {StudioData} */ network) => new Set(network.devices.map((device) => device.name));
660
- const was = names(before);
661
- const is = names(after);
662
-
663
- const added = [...is].filter((name) => !was.has(name));
664
- const removed = [...was].filter((name) => !is.has(name));
665
- const changed = [...is].filter((name) => was.has(name) && before.rendered[name] !== after.rendered[name]);
666
-
667
- const keys = (/** @type {StudioData} */ network) => new Set(network.graph.edges.map(Evaluate.changeKey));
668
- const edgesBefore = keys(before);
669
- const edgesAfter = keys(after);
670
-
671
- const finding = (/** @type {import("../../core/define").Finding} */ one) => `${one.level} ${one.device ?? ""} ${one.message}`;
672
- const findingsBefore = new Set(before.graph.findings.map(finding));
673
- const findingsAfter = new Set(after.graph.findings.map(finding));
674
-
675
- return {
676
- devices: { added, removed, changed },
677
- edges: {
678
- added: after.graph.edges.filter((edge) => !edgesBefore.has(Evaluate.changeKey(edge))),
679
- removed: before.graph.edges.filter((edge) => !edgesAfter.has(Evaluate.changeKey(edge))),
680
- },
681
- findings: {
682
- added: after.graph.findings.filter((one) => !findingsBefore.has(finding(one))),
683
- resolved: before.graph.findings.filter((one) => !findingsAfter.has(finding(one))),
684
- },
685
- };
686
- }
687
- }