@takodotid/circuit 0.43.1 → 1.0.0-rc.2

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 (256) 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 +71 -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 +350 -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/render.ts +69 -62
  63. package/src/platforms/fortios/routing.ts +89 -58
  64. package/src/platforms/fortios/vdoms.ts +47 -16
  65. package/src/platforms/junos/TRAPS.md +2 -2
  66. package/src/platforms/junos/bgp.ts +63 -42
  67. package/src/platforms/junos/index.ts +122 -89
  68. package/src/platforms/junos/render.ts +139 -104
  69. package/src/platforms/platform.ts +10 -8
  70. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  71. package/src/platforms/raisecom-ros/index.ts +89 -70
  72. package/src/platforms/raisecom-ros/render.ts +49 -45
  73. package/src/platforms/routeros/TRAPS.md +3 -3
  74. package/src/platforms/routeros/index.ts +121 -60
  75. package/src/platforms/routeros/render.ts +214 -195
  76. package/src/platforms/vrp/TRAPS.md +1 -1
  77. package/src/platforms/vrp/index.ts +74 -50
  78. package/src/platforms/vrp/render.ts +67 -56
  79. package/src/project/project.ts +725 -0
  80. package/src/studio/app/env.d.ts +9 -0
  81. package/src/studio/app/index.html +18 -0
  82. package/src/studio/app/src/App.vue +95 -0
  83. package/src/studio/app/src/components/AddressesView.vue +38 -0
  84. package/src/studio/app/src/components/CablesView.vue +29 -0
  85. package/src/studio/app/src/components/CompareView.vue +160 -0
  86. package/src/studio/app/src/components/DataTable.vue +49 -0
  87. package/src/studio/app/src/components/DeviceView.vue +237 -0
  88. package/src/studio/app/src/components/DevicesView.vue +32 -0
  89. package/src/studio/app/src/components/Faceplate.vue +75 -0
  90. package/src/studio/app/src/components/GraphView.vue +282 -0
  91. package/src/studio/app/src/components/LineDiff.vue +24 -0
  92. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  93. package/src/studio/app/src/components/SearchView.vue +72 -0
  94. package/src/studio/app/src/components/SecretsView.vue +112 -0
  95. package/src/studio/app/src/components/SessionsView.vue +35 -0
  96. package/src/studio/app/src/components/Settings.vue +95 -0
  97. package/src/studio/app/src/components/ToolsView.vue +118 -0
  98. package/src/studio/app/src/components/VlansView.vue +50 -0
  99. package/src/studio/app/src/lib/csv.ts +19 -0
  100. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  101. package/src/studio/app/src/lib/format.ts +38 -0
  102. package/src/studio/app/src/lib/net.ts +68 -0
  103. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  104. package/src/studio/app/src/lib/store.ts +71 -0
  105. package/src/studio/app/src/lib/tools.ts +217 -0
  106. package/src/studio/app/src/main.ts +9 -0
  107. package/src/studio/{public → app/src}/style.css +14 -4
  108. package/src/studio/app/tsconfig.json +17 -0
  109. package/src/studio/app/vite.config.ts +16 -0
  110. package/src/studio/assets.generated.ts +3 -0
  111. package/src/studio/data.ts +96 -188
  112. package/src/studio/git.ts +27 -43
  113. package/src/studio/server.ts +196 -107
  114. package/src/studio/types.ts +141 -0
  115. package/src/transport/console.ts +3 -3
  116. package/src/transport/ssh.ts +1 -1
  117. package/dist/chunks/index-3b343xva.js +0 -177
  118. package/dist/chunks/index-6wb41q67.js +0 -62
  119. package/dist/chunks/index-n7w3tg5y.js +0 -149
  120. package/dist/chunks/index-yzw86rc8.js +0 -921
  121. package/dist/chunks/new-w3zys5y4.js +0 -172
  122. package/dist/chunks/server-cxr9ynxc.js +0 -259
  123. package/dist/cli/index.js +0 -12922
  124. package/dist/helpers/index.js +0 -881
  125. package/dist/index.js +0 -28
  126. package/dist/types/core/address-space.d.ts +0 -62
  127. package/dist/types/core/address.d.ts +0 -44
  128. package/dist/types/core/cabling.d.ts +0 -41
  129. package/dist/types/core/cross-checks.d.ts +0 -28
  130. package/dist/types/core/define.d.ts +0 -79
  131. package/dist/types/core/project-files.d.ts +0 -12
  132. package/dist/types/core/schema-docs.d.ts +0 -25
  133. package/dist/types/core/secrets.d.ts +0 -62
  134. package/dist/types/core/topology.d.ts +0 -96
  135. package/dist/types/core/utils.d.ts +0 -10
  136. package/dist/types/core/validator.d.ts +0 -23
  137. package/dist/types/helpers/bgp.d.ts +0 -195
  138. package/dist/types/helpers/cables.d.ts +0 -69
  139. package/dist/types/helpers/checks.d.ts +0 -36
  140. package/dist/types/helpers/exchange.d.ts +0 -55
  141. package/dist/types/helpers/filters.d.ts +0 -51
  142. package/dist/types/helpers/index.d.ts +0 -14
  143. package/dist/types/helpers/route-server.d.ts +0 -52
  144. package/dist/types/helpers/upstreams.d.ts +0 -192
  145. package/dist/types/index.d.ts +0 -10
  146. package/dist/types/integrations/librenms.d.ts +0 -60
  147. package/dist/types/integrations/netbox.d.ts +0 -89
  148. package/dist/types/integrations/peeringdb.d.ts +0 -24
  149. package/dist/types/integrations/prometheus.d.ts +0 -24
  150. package/dist/types/integrations/registries.d.ts +0 -8
  151. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  152. package/dist/types/platforms/catalog.d.ts +0 -109
  153. package/dist/types/platforms/derive.d.ts +0 -17
  154. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  155. package/dist/types/platforms/fortios/index.d.ts +0 -140
  156. package/dist/types/platforms/fortios/models.d.ts +0 -14
  157. package/dist/types/platforms/fortios/names.d.ts +0 -52
  158. package/dist/types/platforms/fortios/plan.d.ts +0 -114
  159. package/dist/types/platforms/fortios/render.d.ts +0 -77
  160. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  161. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  162. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  163. package/dist/types/platforms/index.d.ts +0 -13
  164. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  165. package/dist/types/platforms/junos/index.d.ts +0 -114
  166. package/dist/types/platforms/junos/models.d.ts +0 -14
  167. package/dist/types/platforms/junos/render.d.ts +0 -154
  168. package/dist/types/platforms/line-diff.d.ts +0 -67
  169. package/dist/types/platforms/platform.d.ts +0 -135
  170. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  171. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  172. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  173. package/dist/types/platforms/routeros/export.d.ts +0 -59
  174. package/dist/types/platforms/routeros/index.d.ts +0 -116
  175. package/dist/types/platforms/routeros/models.d.ts +0 -18
  176. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  177. package/dist/types/platforms/routeros/render.d.ts +0 -250
  178. package/dist/types/platforms/vrp/index.d.ts +0 -83
  179. package/dist/types/platforms/vrp/models.d.ts +0 -12
  180. package/dist/types/platforms/vrp/render.d.ts +0 -96
  181. package/dist/types/schema/common.d.ts +0 -30
  182. package/dist/types/schema/device.d.ts +0 -126
  183. package/dist/types/schema/firewall.d.ts +0 -131
  184. package/dist/types/schema/index.d.ts +0 -8
  185. package/dist/types/schema/interfaces.d.ts +0 -205
  186. package/dist/types/schema/policy.d.ts +0 -107
  187. package/dist/types/schema/routing.d.ts +0 -161
  188. package/dist/types/schema/system.d.ts +0 -198
  189. package/dist/types/schema/time-zones.d.ts +0 -2
  190. package/dist/types/transport/console.d.ts +0 -115
  191. package/dist/types/transport/path-check.d.ts +0 -32
  192. package/dist/types/transport/ssh.d.ts +0 -27
  193. package/src/cli/graph-format.ts +0 -91
  194. package/src/cli/new.ts +0 -234
  195. package/src/core/address-space.ts +0 -320
  196. package/src/core/cross-checks.ts +0 -369
  197. package/src/core/define.ts +0 -124
  198. package/src/core/project-files.ts +0 -35
  199. package/src/core/schema-docs.ts +0 -174
  200. package/src/core/validator.ts +0 -508
  201. package/src/helpers/bgp.ts +0 -473
  202. package/src/helpers/cables.ts +0 -74
  203. package/src/helpers/checks.ts +0 -201
  204. package/src/helpers/exchange.ts +0 -123
  205. package/src/helpers/filters.ts +0 -107
  206. package/src/helpers/index.ts +0 -25
  207. package/src/helpers/route-server.ts +0 -212
  208. package/src/helpers/upstreams.ts +0 -369
  209. package/src/index.ts +0 -13
  210. package/src/integrations/librenms.ts +0 -291
  211. package/src/integrations/netbox.ts +0 -620
  212. package/src/integrations/peeringdb.ts +0 -173
  213. package/src/integrations/prometheus.ts +0 -90
  214. package/src/schema/common.ts +0 -41
  215. package/src/schema/device.ts +0 -145
  216. package/src/schema/firewall.ts +0 -140
  217. package/src/schema/index.ts +0 -8
  218. package/src/schema/policy.ts +0 -115
  219. package/src/schema/routing.ts +0 -173
  220. package/src/schema/system.ts +0 -203
  221. package/src/studio/public/app.js +0 -186
  222. package/src/studio/public/compare.js +0 -250
  223. package/src/studio/public/dom.js +0 -138
  224. package/src/studio/public/evaluate.js +0 -687
  225. package/src/studio/public/export.js +0 -130
  226. package/src/studio/public/graph.js +0 -1541
  227. package/src/studio/public/index.html +0 -83
  228. package/src/studio/public/net.js +0 -134
  229. package/src/studio/public/tables.js +0 -1289
  230. package/src/studio/public/tools.js +0 -610
  231. package/templates/colocation/checks.ts +0 -53
  232. package/templates/colocation/circuit.config.ts +0 -9
  233. package/templates/colocation/edge-01.ts +0 -133
  234. package/templates/colocation/routing.ts +0 -13
  235. package/templates/colocation/tenancy.ts +0 -76
  236. package/templates/colocation/tenants/acme.ts +0 -19
  237. package/templates/colocation/tenants/index.ts +0 -6
  238. package/templates/colocation/tor-01.ts +0 -33
  239. package/templates/edge-router/checks.ts +0 -20
  240. package/templates/edge-router/circuit.config.ts +0 -8
  241. package/templates/edge-router/edge-01.ts +0 -110
  242. package/templates/edge-router/routing.ts +0 -13
  243. package/templates/project/AGENTS.md +0 -51
  244. package/templates/project/CLAUDE.md +0 -1
  245. package/templates/project/README.md +0 -16
  246. package/templates/project/gitignore +0 -5
  247. package/templates/project/tsconfig.json +0 -13
  248. package/templates/single-site/checks.ts +0 -26
  249. package/templates/single-site/circuit.config.ts +0 -8
  250. package/templates/single-site/router.ts +0 -86
  251. package/templates/single-site/site.ts +0 -18
  252. package/templates/single-site/switch.ts +0 -34
  253. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  254. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  256. /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
- }