@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,610 +0,0 @@
1
- // @ts-check
2
- /// <reference lib="dom" />
3
- // Studio's tools, for finding out why something does or does not reach where it should: trace an address, find the VLAN path between two devices, test a packet against a firewall, and test a route against a route policy.
4
-
5
- import { Dom } from "./dom.js";
6
- import { Evaluate } from "./evaluate.js";
7
- import { Graph } from "./graph.js";
8
- import { Tables } from "./tables.js";
9
-
10
- /** @typedef {import("../data").StudioData} StudioData */
11
- /** @typedef {import("../../schema").Device} Device */
12
- /** @typedef {import("./dom.js").Child} Child */
13
- /** @typedef {import("./evaluate.js").Check} Check */
14
-
15
- export class Tools {
16
- /** What Studio shows, set each time a tool is drawn. @type {StudioData} */
17
- static #data;
18
-
19
- /** What was typed into each tool, kept while the page is open, so a tool drawn again shows its last answer. */
20
- static #typed = {
21
- vlan: { vlan: "", from: "", to: "" },
22
- firewall: {
23
- device: "",
24
- chain: "input",
25
- protocol: "tcp",
26
- src: "",
27
- dst: "",
28
- srcPort: "",
29
- dstPort: "",
30
- inInterface: "",
31
- outInterface: "",
32
- state: "new",
33
- flags: /** @type {string[]} */ ([]),
34
- },
35
- policy: {
36
- device: "",
37
- policy: "",
38
- prefix: "",
39
- asPathLength: "",
40
- rpki: "",
41
- communities: "",
42
- largeCommunities: "",
43
- inRegistrySets: /** @type {string[]} */ ([]),
44
- },
45
- };
46
-
47
- static #TOOLS = [
48
- ["trace", "Trace an address"],
49
- ["vlan", "VLAN path"],
50
- ["firewall", "Test a firewall"],
51
- ["policy", "Test a route policy"],
52
- ];
53
-
54
- /**
55
- * A labelled field.
56
- *
57
- * @param {string} label
58
- * @param {HTMLElement} control
59
- * @param {string} [hint]
60
- */
61
- static #field(label, control, hint) {
62
- return Dom.h(
63
- "label",
64
- { class: "field" },
65
- Dom.h("span", {}, label),
66
- control,
67
- hint ? Dom.h("small", { class: "muted" }, hint) : undefined
68
- );
69
- }
70
-
71
- /**
72
- * A text box that keeps what is typed in `store[key]`.
73
- *
74
- * @param {Record<string, unknown>} store
75
- * @param {string} key
76
- * @param {string} [placeholder]
77
- */
78
- static #textBox(store, key, placeholder = "") {
79
- const input = Dom.h("input", { type: "text", value: String(store[key] ?? ""), placeholder, spellcheck: "false" });
80
- input.addEventListener("input", () => (store[key] = input.value));
81
- return input;
82
- }
83
-
84
- /**
85
- * A choice that keeps what is chosen in `store[key]`.
86
- *
87
- * @param {Record<string, unknown>} store
88
- * @param {string} key
89
- * @param {[string, string][]} options value and text
90
- * @param {() => void} [changed]
91
- */
92
- static #choice(store, key, options, changed) {
93
- const select = Dom.h(
94
- "select",
95
- {},
96
- ...options.map(([value, text]) => Dom.h("option", { value, ...(value === store[key] ? { selected: "" } : {}) }, text))
97
- );
98
- if (!options.some(([value]) => value === store[key])) store[key] = options[0]?.[0] ?? "";
99
- select.value = String(store[key]);
100
- select.addEventListener("change", () => {
101
- store[key] = select.value;
102
- changed?.();
103
- });
104
- return select;
105
- }
106
-
107
- /**
108
- * What a check found, in words.
109
- *
110
- * @param {Check} check
111
- */
112
- static #verdictOf(check) {
113
- return check.answer === "yes"
114
- ? "matches"
115
- : check.answer === "no"
116
- ? `does not match: ${check.why.join("; ")}`
117
- : `cannot tell without ${check.why.join(", ")}`;
118
- }
119
-
120
- /**
121
- * @param {string} text
122
- * @param {() => void} action
123
- */
124
- static #button(text, action) {
125
- const element = Dom.h("button", { type: "submit", class: "primary" }, text);
126
- element.addEventListener("click", (event) => {
127
- event.preventDefault();
128
- action();
129
- });
130
- return element;
131
- }
132
-
133
- // Trace an address.
134
-
135
- /**
136
- * @param {string | undefined} asked
137
- * @param {HTMLElement} container
138
- */
139
- static #trace(asked, container) {
140
- const store = { address: asked ?? "" };
141
- const input = Tools.#textBox(store, "address", "10.0.10.5");
142
-
143
- container.append(
144
- Tables.prose(
145
- "Where an address is in the network, how each device would reach it from what the files say, and every firewall rule and ACL that names it."
146
- ),
147
- Dom.h(
148
- "form",
149
- { class: "tool-form" },
150
- Tools.#field("Address", input),
151
- Tools.#button("Trace", () => store.address.trim() && Dom.go(`tools/trace/${encodeURIComponent(store.address.trim())}`))
152
- )
153
- );
154
-
155
- if (!asked) return;
156
-
157
- const found = Evaluate.trace(Tools.#data, asked);
158
- if (!found) {
159
- container.append(Tables.empty(`${asked} is not an address, such as 10.0.10.5 or 2001:db8::1.`));
160
- return;
161
- }
162
-
163
- const holder = found.held.length
164
- ? Dom.h(
165
- "span",
166
- {},
167
- "It is held by ",
168
- ...found.held.flatMap((held, index) => [index ? ", " : "", Tables.deviceLink(held.device), ` ${held.where}`]),
169
- "."
170
- )
171
- : "No device holds it.";
172
- const where = found.segment
173
- ? `It is in ${found.segment.prefix}${found.space ? `, inside ${found.space.prefix}, ${found.space.owner}` : ""}.`
174
- : found.block
175
- ? `It is in ${found.block.prefix}: ${found.block.what}.`
176
- : found.space
177
- ? `It is in ${found.space.prefix}, ${found.space.owner}, where nothing uses it yet.`
178
- : "No segment or block of the network holds it.";
179
-
180
- container.append(Tables.heading(asked, "h2"), Dom.h("p", {}, holder, " ", where));
181
-
182
- if (found.uses.length) {
183
- container.append(
184
- Tables.heading("Used by"),
185
- Tables.of({
186
- head: [{ label: "Address" }, { label: "Used by" }],
187
- rows: found.uses.map((use) => ({ cells: [use.address, use.what] })),
188
- })
189
- );
190
- }
191
-
192
- const kinds = {
193
- local: "it is its own address",
194
- connected: "an address of its own is in the same prefix",
195
- static: "a static route",
196
- none: "no route in the files",
197
- };
198
-
199
- container.append(
200
- Tables.heading("How each device reaches it"),
201
- Tables.prose(
202
- "Routes a device learns from BGP or OSPF are not in the files, so a device with no route here may still reach it."
203
- ),
204
- Tables.of({
205
- csv: `trace-${asked}`,
206
- head: ["Device", "Table", "By", "Prefix", "Next hop or interface", "Note"].map((label) => ({ label })),
207
- rows: found.routes.map((route) => ({
208
- muted: route.kind === "none",
209
- cells: [
210
- Tables.deviceLink(route.device),
211
- route.table,
212
- kinds[route.kind],
213
- route.prefix ?? "",
214
- [route.via, route.through].filter(Boolean).join(", "),
215
- route.note ?? "",
216
- ],
217
- })),
218
- })
219
- );
220
-
221
- container.append(Tables.heading("Rules that name it"));
222
- container.append(
223
- found.rules.length
224
- ? Tables.of({
225
- head: ["Device", "Where", "Rule", "Description", "Action", "Names it as"].map((label) => ({ label })),
226
- rows: found.rules.map((rule) => ({
227
- cells: [
228
- Tables.deviceLink(rule.device),
229
- rule.where,
230
- String(rule.rule),
231
- rule.description ?? "",
232
- rule.action,
233
- rule.side,
234
- ],
235
- })),
236
- })
237
- : Tables.empty("No firewall rule or ACL names it, by address, prefix or address set.")
238
- );
239
- }
240
-
241
- // The VLAN path between two devices.
242
-
243
- /** @param {HTMLElement} container */
244
- static #vlan(container) {
245
- const store = Tools.#typed.vlan;
246
- const vlans = new Map();
247
- for (const edge of Tools.#data.graph.edges) for (const vlan of edge.vlans ?? []) vlans.set(vlan.id, vlan.name);
248
-
249
- const devices = Tools.#data.devices.map((device) => /** @type {[string, string]} */ ([device.name, device.name]));
250
- const vlanChoices = [...vlans]
251
- .sort(([a], [b]) => a - b)
252
- .map(([id, name]) => /** @type {[string, string]} */ ([String(id), `${name} (${id})`]));
253
-
254
- const result = Dom.h("div");
255
- const find = () => {
256
- result.replaceChildren();
257
-
258
- const path = Evaluate.vlanPath(Tools.#data, Number(store.vlan), store.from, store.to);
259
- if (store.from === store.to) return result.append(Tables.empty("Pick two different devices."));
260
- if (!path) return result.append(Tables.empty(`No cables carry VLAN ${store.vlan} from ${store.from} to ${store.to}.`));
261
-
262
- const cableOf = (/** @type {import("../../core/topology").GraphEdge} */ edge) =>
263
- Tools.#data.graph.edges.find(
264
- (other) =>
265
- other.layer === "l1" &&
266
- ((other.from === edge.from && other.to === edge.to) || (other.from === edge.to && other.to === edge.from))
267
- )?.label ?? "";
268
- const nameOf = (/** @type {string} */ id) => id.replace(/^device:/, "");
269
-
270
- result.append(
271
- Dom.h(
272
- "p",
273
- {},
274
- `${path.length} ${path.length === 1 ? "cable" : "cables"} carry it from ${store.from} to ${store.to}. `,
275
- Dom.link("Show in the graph", () => {
276
- Graph.lightPath(path);
277
- Dom.go("graph/l2");
278
- })
279
- ),
280
- Tables.of({
281
- head: ["#", "From", "To", "Cable"].map((label) => ({ label })),
282
- rows: path.map((edge, index) => ({
283
- cells: [String(index + 1), Tables.deviceLink(nameOf(edge.from)), Tables.deviceLink(nameOf(edge.to)), cableOf(edge)],
284
- })),
285
- })
286
- );
287
- };
288
-
289
- if (!vlanChoices.length) return container.append(Tables.empty("No cable carries a VLAN."));
290
-
291
- container.append(
292
- Tables.prose("The fewest cables that carry a VLAN from one device to another, from the ports' VLANs and links."),
293
- Dom.h(
294
- "form",
295
- { class: "tool-form" },
296
- Tools.#field("VLAN", Tools.#choice(store, "vlan", vlanChoices)),
297
- Tools.#field("From", Tools.#choice(store, "from", devices)),
298
- Tools.#field("To", Tools.#choice(store, "to", devices)),
299
- Tools.#button("Find the path", find)
300
- ),
301
- result
302
- );
303
- }
304
-
305
- // Test a firewall.
306
-
307
- /** @param {HTMLElement} container */
308
- static #firewall(container) {
309
- const store = Tools.#typed.firewall;
310
- const devices = Tools.#data.devices.filter((device) => Object.values(device.firewall?.filter ?? {}).some(Boolean));
311
- if (!devices.length) return container.append(Tables.empty("No device has a firewall filter."));
312
-
313
- const result = Dom.h("div");
314
- const form = Dom.h("form", { class: "tool-form" });
315
-
316
- const draw = () => {
317
- form.replaceChildren();
318
- const device = devices.find((candidate) => candidate.name === store.device) ?? /** @type {Device} */ (devices[0]);
319
- const chains = /** @type {[string, string][]} */ (Object.keys(device.firewall?.filter ?? {}).map((chain) => [chain, chain]));
320
- const names = /** @type {[string, string][]} */ ([
321
- ["", "not given"],
322
- ...Object.keys({ ...device.ports, ...device.interfaces }).map((name) => [name, name]),
323
- ]);
324
- const protocols = /** @type {[string, string][]} */ (
325
- ["tcp", "udp", "icmp", "gre", "esp", "ah", "ospf", "vrrp"].map((name) => [name, name])
326
- );
327
- const states = /** @type {[string, string][]} */ (
328
- ["new", "established", "related", "untracked", "invalid"].map((name) => [name, name])
329
- );
330
-
331
- const flags = Dom.h("span", { class: "flags" });
332
- for (const flag of ["syn", "ack", "fin", "rst", "psh", "urg"]) {
333
- const box = Dom.h("input", { type: "checkbox", ...(store.flags.includes(flag) ? { checked: "" } : {}) });
334
- box.addEventListener(
335
- "change",
336
- () => (store.flags = box.checked ? [...store.flags, flag] : store.flags.filter((other) => other !== flag))
337
- );
338
- flags.append(Dom.h("label", { class: "flag" }, box, flag));
339
- }
340
-
341
- form.append(
342
- Tools.#field(
343
- "Device",
344
- Tools.#choice(
345
- store,
346
- "device",
347
- devices.map((candidate) => [candidate.name, candidate.name]),
348
- draw
349
- )
350
- ),
351
- Tools.#field("Chain", Tools.#choice(store, "chain", chains)),
352
- Tools.#field("Protocol", Tools.#choice(store, "protocol", protocols)),
353
- Tools.#field("Source", Tools.#textBox(store, "src", "203.0.113.9")),
354
- Tools.#field("Destination", Tools.#textBox(store, "dst", "192.0.2.1")),
355
- Tools.#field("Source port", Tools.#textBox(store, "srcPort")),
356
- Tools.#field("Destination port", Tools.#textBox(store, "dstPort", "22")),
357
- Tools.#field("Arrives on", Tools.#choice(store, "inInterface", names)),
358
- Tools.#field("Leaves through", Tools.#choice(store, "outInterface", names)),
359
- Tools.#field("State", Tools.#choice(store, "state", states)),
360
- Tools.#field("TCP flags", flags, "Leave all unticked when not known."),
361
- Tools.#button("Test", test)
362
- );
363
- };
364
-
365
- const test = () => {
366
- result.replaceChildren();
367
- const device = devices.find((candidate) => candidate.name === store.device) ?? /** @type {Device} */ (devices[0]);
368
- const port = (/** @type {string} */ text) => (text.trim() === "" ? undefined : Number(text));
369
-
370
- const outcome = Evaluate.filterPacket(device, /** @type {"input" | "forward" | "output"} */ (store.chain), {
371
- protocol: store.protocol,
372
- src: store.src.trim() || undefined,
373
- dst: store.dst.trim() || undefined,
374
- srcPort: port(store.srcPort),
375
- dstPort: port(store.dstPort),
376
- inInterface: store.inInterface || undefined,
377
- outInterface: store.outInterface || undefined,
378
- state: store.state,
379
- flags: store.flags.length ? store.flags : undefined,
380
- });
381
-
382
- const decided = outcome.verdict
383
- ? Dom.h("p", { class: `verdict ${outcome.verdict}` }, `${outcome.verdict}, by ${outcome.by}.`)
384
- : Dom.h("p", { class: "verdict unknown" }, `Cannot tell: ${outcome.by} needs more about the packet.`);
385
- const reached = outcome.steps.length;
386
- const rules = device.firewall?.filter?.[/** @type {"input" | "forward" | "output"} */ (store.chain)]?.rules ?? [];
387
-
388
- result.append(
389
- decided,
390
- Tables.of({
391
- head: ["Rule", "Description", "Action", "This packet"].map((label) => ({ label })),
392
- rows: rules.map((rule, index) => {
393
- const step = outcome.steps[index];
394
- return {
395
- muted: index >= reached,
396
- cells: [
397
- String(index + 1),
398
- rule.description ?? "",
399
- rule.action,
400
- step ? Tools.#verdictOf(step.check) : "not reached",
401
- ],
402
- };
403
- }),
404
- })
405
- );
406
- };
407
-
408
- draw();
409
- container.append(
410
- Tables.prose(
411
- "Which rule of a device's filter chain a packet meets, first match first, as the schema describes each condition."
412
- ),
413
- form,
414
- result
415
- );
416
- }
417
-
418
- // Test a route policy.
419
-
420
- /** @param {HTMLElement} container */
421
- static #policy(container) {
422
- const store = Tools.#typed.policy;
423
- const devices = Tools.#data.devices.filter((device) => Object.keys(device.policies ?? {}).length);
424
- if (!devices.length) return container.append(Tables.empty("No device has a route policy."));
425
-
426
- const result = Dom.h("div");
427
- const form = Dom.h("form", { class: "tool-form" });
428
-
429
- const draw = () => {
430
- form.replaceChildren();
431
- const device = devices.find((candidate) => candidate.name === store.device) ?? /** @type {Device} */ (devices[0]);
432
- const neighbors = Object.entries(device.routing?.bgp?.neighbors ?? {});
433
- const groups = device.routing?.bgp?.groups ?? {};
434
-
435
- // A policy is listed with the sessions that use it, so it can be picked by neighbor.
436
- const usedBy = (/** @type {string} */ policy) =>
437
- neighbors.flatMap(([name, neighbor]) => {
438
- const group = neighbor.group ? groups[neighbor.group] : undefined;
439
- return [
440
- (neighbor.import ?? group?.import) === policy ? `import from ${name}` : "",
441
- (neighbor.export ?? group?.export) === policy ? `export to ${name}` : "",
442
- ].filter(Boolean);
443
- });
444
-
445
- const policies = /** @type {[string, string][]} */ (
446
- Object.keys(device.policies ?? {}).map((name) => {
447
- const uses = usedBy(name);
448
- return [name, uses.length ? `${name}: ${uses.join(", ")}` : name];
449
- })
450
- );
451
-
452
- const registrySets = Object.entries(device.prefix_sets ?? {}).filter(([, set]) => "source" in set);
453
- const sets = Dom.h("span", { class: "flags" });
454
- for (const [name] of registrySets) {
455
- const box = Dom.h("input", { type: "checkbox", ...(store.inRegistrySets.includes(name) ? { checked: "" } : {}) });
456
- box.addEventListener(
457
- "change",
458
- () =>
459
- (store.inRegistrySets = box.checked
460
- ? [...store.inRegistrySets, name]
461
- : store.inRegistrySets.filter((other) => other !== name))
462
- );
463
- sets.append(Dom.h("label", { class: "flag" }, box, name));
464
- }
465
-
466
- form.append(
467
- Tools.#field(
468
- "Device",
469
- Tools.#choice(
470
- store,
471
- "device",
472
- devices.map((candidate) => [candidate.name, candidate.name]),
473
- draw
474
- )
475
- ),
476
- Tools.#field("Policy", Tools.#choice(store, "policy", policies)),
477
- Tools.#field("Prefix", Tools.#textBox(store, "prefix", "198.51.100.0/24")),
478
- Tools.#field("AS path length", Tools.#textBox(store, "asPathLength"), "As the route arrives."),
479
- Tools.#field(
480
- "RPKI",
481
- Tools.#choice(store, "rpki", [
482
- ["", "not known"],
483
- ["valid", "valid"],
484
- ["invalid", "invalid"],
485
- ["not-found", "not found"],
486
- ])
487
- ),
488
- Tools.#field("Communities", Tools.#textBox(store, "communities", "64500:100"), "Separated by spaces or commas."),
489
- Tools.#field("Large communities", Tools.#textBox(store, "largeCommunities", "64500:1:1")),
490
- ...(registrySets.length
491
- ? [
492
- Tools.#field(
493
- "In prefix sets from a registry",
494
- sets,
495
- "Their prefixes are fetched by circuit refresh and are not in the files."
496
- ),
497
- ]
498
- : []),
499
- Tools.#button("Test", test)
500
- );
501
- };
502
-
503
- const test = () => {
504
- result.replaceChildren();
505
- const device = devices.find((candidate) => candidate.name === store.device) ?? /** @type {Device} */ (devices[0]);
506
- const list = (/** @type {string} */ text) => text.split(/[\s,]+/).filter(Boolean);
507
-
508
- if (!store.prefix.trim()) return result.append(Tables.empty("Give the route's prefix, such as 198.51.100.0/24."));
509
-
510
- const outcome = Evaluate.routeThrough(device, store.policy, {
511
- prefix: store.prefix.trim(),
512
- asPathLength: store.asPathLength.trim() === "" ? undefined : Number(store.asPathLength),
513
- rpki: store.rpki || undefined,
514
- communities: list(store.communities),
515
- largeCommunities: list(store.largeCommunities),
516
- inRegistrySets: store.inRegistrySets,
517
- });
518
-
519
- const decided =
520
- outcome.decision === "unknown"
521
- ? Dom.h(
522
- "p",
523
- { class: "verdict unknown" },
524
- `Cannot tell: rule ${outcome.steps.at(-1)?.rule} of ${outcome.steps.at(-1)?.policy} needs more about the route.`
525
- )
526
- : Dom.h(
527
- "p",
528
- { class: `verdict ${outcome.decision}` },
529
- `${outcome.decision}ed${outcome.byDefault ? ", since no rule accepted it" : ""}.`
530
- );
531
-
532
- const { attributes } = outcome;
533
- const final = [
534
- attributes.localPref !== undefined ? `local preference ${attributes.localPref}` : "",
535
- attributes.med !== undefined ? `MED ${attributes.med}` : "",
536
- attributes.prepend ? `our AS prepended ${attributes.prepend} ${attributes.prepend === 1 ? "time" : "times"}` : "",
537
- `communities ${attributes.communities.join(", ") || "none"}`,
538
- `large communities ${attributes.largeCommunities.join(", ") || "none"}`,
539
- attributes.nextHop ? `next hop ${attributes.nextHop}` : "",
540
- attributes.preferredSource ? `preferred source ${attributes.preferredSource}` : "",
541
- attributes.blackhole ? "blackholed" : "",
542
- ].filter(Boolean);
543
-
544
- result.append(
545
- decided,
546
- Tables.of({
547
- head: ["Policy", "Rule", "Description", "This route", "Changes", "Decision"].map((label) => ({ label })),
548
- rows: outcome.steps.map((step) => ({
549
- cells: [
550
- step.policy,
551
- String(step.rule),
552
- step.description ?? (step.call ? `calls ${step.call}` : ""),
553
- Tools.#verdictOf(step.check),
554
- step.changed.join("; "),
555
- step.decision ?? "",
556
- ],
557
- })),
558
- }),
559
- ...(outcome.decision === "accept" ? [Dom.h("p", {}, "The route leaves with ", final.join(", "), ".")] : [])
560
- );
561
- };
562
-
563
- draw();
564
- container.append(
565
- Tables.prose(
566
- "What a device's route policy does to a route: rule by rule, what matches, what changes, and the decision. A prefix set holds a route whose prefix is one of its own or inside one."
567
- ),
568
- form,
569
- result
570
- );
571
- }
572
-
573
- /**
574
- * Draw a tool, such as `["tools", "trace", "10.0.10.5"]`, into a container.
575
- *
576
- * @param {string[]} route
577
- * @param {HTMLElement} container
578
- * @param {StudioData} next
579
- */
580
- static show(route, container, next) {
581
- Tools.#data = next;
582
- container.replaceChildren();
583
-
584
- const [, asked = "trace", argument] = route;
585
- const tool = Tools.#TOOLS.find(([id]) => id === asked) ?? Tools.#TOOLS[0];
586
- if (!tool) return;
587
-
588
- const tabs = Dom.h("nav", { class: "tabs", "aria-label": "Tools" });
589
- for (const [id, title] of Tools.#TOOLS) {
590
- const tab = Dom.h("button", { "aria-pressed": String(id === tool[0]) }, title ?? id ?? "");
591
- tab.addEventListener("click", () => Dom.go(`tools/${id}`));
592
- tabs.append(tab);
593
- }
594
-
595
- const body = Dom.h("div", { class: "section" });
596
- container.append(
597
- Tables.heading("Tools", "h2"),
598
- Tables.prose(
599
- "They read the files as the schema describes each field. A device can still behave otherwise; these say what the config means."
600
- ),
601
- tabs,
602
- body
603
- );
604
-
605
- if (tool[0] === "trace") Tools.#trace(argument, body);
606
- else if (tool[0] === "vlan") Tools.#vlan(body);
607
- else if (tool[0] === "firewall") Tools.#firewall(body);
608
- else Tools.#policy(body);
609
- }
610
- }
@@ -1,53 +0,0 @@
1
- // Rules of your own. `circuit validate` runs them beside Circuit's own, and an error stops `diff` and `apply`.
2
-
3
- import type { Check, Finding } from "@takodotid/circuit";
4
- import { Address } from "@takodotid/circuit";
5
- import { Checks } from "@takodotid/circuit/helpers";
6
- import { blocksOf, type Tenant } from "./tenancy";
7
- import { TENANTS } from "./tenants";
8
-
9
- export default [
10
- // Nothing leaves by accident: every export ends by rejecting what it did not accept.
11
- Checks.exportsEndInReject,
12
-
13
- // Traffic from the transit and the exchange reaches the tenants only through the router, which filters it.
14
- Checks.trustBoundary({ untrusted: ["transit", "ix"], routers: ["edge-01"] }),
15
-
16
- // A native VLAN stays on one trunk and nowhere else, or it joins every trunk it is native on.
17
- Checks.nativeVlansAreDeadEnds,
18
-
19
- // Tenants never share a name, a VLAN, an address or a port. A port given twice would go to the last tenant alone.
20
- () => {
21
- const findings: Finding[] = [];
22
- const error = (message: string) => findings.push({ level: "error", message });
23
- const vlansOf = (t: Tenant) => [t.internet.vlan, ...(t.private ? [t.private.vlan] : [])];
24
-
25
- TENANTS.forEach((t, index) => {
26
- for (const other of TENANTS.slice(index + 1)) {
27
- if (other.name === t.name) error(`tenant ${t.name} is declared twice`);
28
-
29
- for (const vlan of vlansOf(t)) {
30
- for (const theirs of vlansOf(other)) {
31
- if (theirs.id === vlan.id)
32
- error(`VLAN ${vlan.id} is both ${vlan.name} of ${t.name} and ${theirs.name} of ${other.name}`);
33
- if (theirs.name === vlan.name) error(`VLAN ${vlan.name} is declared by both ${t.name} and ${other.name}`);
34
- }
35
- }
36
-
37
- for (const block of blocksOf(t)) {
38
- const clash = blocksOf(other).find((prefix) => Address.contains(prefix, block) || Address.contains(block, prefix));
39
- if (clash) error(`${block} of ${t.name} overlaps ${clash} of ${other.name}`);
40
- }
41
- }
42
-
43
- for (const [device, ports] of Object.entries(t.ports)) {
44
- for (const port of Object.keys(ports)) {
45
- const sharer = TENANTS.slice(index + 1).find((other) => other.ports[device]?.[port]);
46
- if (sharer) error(`port ${port} on ${device} is given to both ${t.name} and ${sharer.name}`);
47
- }
48
- }
49
- });
50
-
51
- return findings;
52
- },
53
- ] satisfies Check[];
@@ -1,9 +0,0 @@
1
- // The network: every device Circuit manages, your own checks, and what is published about the network.
2
-
3
- import { defineNetwork } from "@takodotid/circuit";
4
- import checks from "./checks";
5
- import edge from "./edge-01";
6
- import { ASN, communities } from "./routing";
7
- import tor from "./tor-01";
8
-
9
- export default defineNetwork({ devices: [edge, tor], checks, asn: ASN, communities: communities.catalogue });