@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
@@ -2,7 +2,13 @@
2
2
 
3
3
  import { Address } from "../core/address";
4
4
  import { Utils } from "../core/utils";
5
- import type { Device } from "../schema";
5
+ import type { Device, Interface, InterfaceKind, Port } from "../model";
6
+
7
+ /** One interface of a device, with the group it is in. */
8
+ export type NamedInterface = { name: string; kind: InterfaceKind; settings: Interface };
9
+
10
+ /** The groups of `interfaces` besides `ethernet`, in the order Circuit reads them. */
11
+ const KINDS: readonly InterfaceKind[] = ["lag", "vlan_interface", "bridge", "loopback", "gre", "vxlan", "wireguard"];
6
12
 
7
13
  /** A feature a platform renders once proved on one of its devices. */
8
14
  export type ProvedFeature =
@@ -16,16 +22,35 @@ export type ProvedFeature =
16
22
  | "routing-tables";
17
23
 
18
24
  export class Derive {
25
+ /** Every interface that is not a port, group by group (`KINDS`), each in the order it is written. */
26
+ static interfaces(device: Device): NamedInterface[] {
27
+ return KINDS.flatMap((kind) =>
28
+ Object.entries<Interface | undefined>(device.interfaces?.[kind] ?? {}).flatMap(([name, settings]) =>
29
+ settings ? [{ name, kind, settings }] : []
30
+ )
31
+ );
32
+ }
33
+
34
+ /** Every port the device writes, in the order it is written. */
35
+ static ports(device: Device): [string, Port][] {
36
+ return Object.entries<Port | undefined>(device.interfaces?.ethernet ?? {}).flatMap(([name, port]) =>
37
+ port ? [[name, port] as [string, Port]] : []
38
+ );
39
+ }
40
+
41
+ /** A port or an interface by its name, whatever group it is in. */
42
+ static settings(device: Device, name: string): Port | Interface | undefined {
43
+ return device.interfaces?.ethernet?.[name] ?? Derive.interfaces(device).find((entry) => entry.name === name)?.settings;
44
+ }
45
+
19
46
  /** Each LAG's number: its own `id`, or its position among the device's LAGs. */
20
47
  static lagIds(device: Device): Map<string, number> {
21
48
  const numbers = new Map<string, number>();
22
49
  let position = 0;
23
50
 
24
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
25
- if (iface.type !== "lag") continue;
26
-
51
+ for (const [name, lag] of Object.entries(device.interfaces?.lag ?? {})) {
27
52
  position++;
28
- numbers.set(name, iface.id ?? position);
53
+ numbers.set(name, lag?.id ?? position);
29
54
  }
30
55
 
31
56
  return numbers;
@@ -33,8 +58,9 @@ export class Derive {
33
58
 
34
59
  /** The IPv4 networks of a port's or an interface's addresses. */
35
60
  static ipv4Networks(device: Device, iface: string): string[] {
36
- const settings = device.interfaces?.[iface] ?? device.ports?.[iface as keyof typeof device.ports];
37
- const addresses = (settings?.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
61
+ const addresses = (Derive.settings(device, iface)?.addresses ?? []).map((entry) =>
62
+ typeof entry === "string" ? entry : entry.address
63
+ );
38
64
 
39
65
  return addresses.filter((address) => Address.family(address) === "ipv4").map((address) => Address.prefixOf(address));
40
66
  }
@@ -50,32 +76,33 @@ export class Derive {
50
76
  const has = (feature: ProvedFeature) => proved.includes(feature);
51
77
 
52
78
  if (routing?.bfd && !has("bfd-timers")) found.push(`routing.bfd, BFD timers are ${unproved}`);
53
- if (device.vdoms && !has("management-vdom")) found.push(`vdoms, a management VDOM is ${unproved}`);
79
+ if (device.system?.vdoms && !has("management-vdom")) found.push(`system.vdoms, a management VDOM is ${unproved}`);
54
80
 
55
- for (const route of routing?.static ?? []) {
81
+ for (const route of routing?.static_routes ?? []) {
56
82
  if (route.bfd && !has("static-route-bfd"))
57
- found.push(`routing.static ${route.prefix} bfd, BFD on a static route is ${unproved}`);
58
- if (Utils.otherTable(route.table) && !has("routing-tables"))
59
- found.push(`routing.static ${route.prefix} table, a routing table beside the main one is ${unproved}`);
83
+ found.push(`routing.static_routes ${route.prefix} bfd, BFD on a static route is ${unproved}`);
84
+ if (Utils.otherTable(route.routing_table) && !has("routing-tables"))
85
+ found.push(`routing.static_routes ${route.prefix} routing_table, a routing table beside the main one is ${unproved}`);
60
86
  }
61
87
 
62
88
  if (!has("routing-tables")) {
63
89
  if (routing?.tables) found.push(`routing.tables, a routing table beside the main one is ${unproved}`);
64
- if (routing?.rules) found.push(`routing.rules, routing rules are ${unproved}`);
90
+ if (routing?.policy_routing) found.push(`routing.policy_routing, policy routing is ${unproved}`);
65
91
  }
66
92
 
67
93
  const bgp = routing?.bgp;
68
94
 
69
- for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
70
- if (Utils.otherTable(group.table) && !has("routing-tables"))
71
- found.push(`routing.bgp.groups.${name}.table, a routing table beside the main one is ${unproved}`);
95
+ for (const [name, group] of Object.entries(bgp?.neighbor_groups ?? {})) {
96
+ if (Utils.otherTable(group?.routing_table) && !has("routing-tables"))
97
+ found.push(`routing.bgp.neighbor_groups.${name}.routing_table, a routing table beside the main one is ${unproved}`);
72
98
  }
73
99
 
74
100
  for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
75
- if (Utils.otherTable(neighbor.table) && !has("routing-tables"))
76
- found.push(`routing.bgp.neighbors.${name}.table, a routing table beside the main one is ${unproved}`);
101
+ if (!neighbor) continue;
102
+ if (Utils.otherTable(neighbor.routing_table) && !has("routing-tables"))
103
+ found.push(`routing.bgp.neighbors.${name}.routing_table, a routing table beside the main one is ${unproved}`);
77
104
 
78
- const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
105
+ const group = neighbor.neighbor_group ? bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
79
106
  const bfd = neighbor.bfd ?? group?.bfd;
80
107
  const multihop = neighbor.multihop ?? group?.multihop;
81
108
 
@@ -89,12 +116,9 @@ export class Derive {
89
116
  }
90
117
  }
91
118
 
92
- const holders = [
93
- ...Object.entries(device.ports ?? {}).map(([name, settings]) => ({ where: `ports.${name}`, settings })),
94
- ...Object.entries(device.interfaces ?? {}).map(([name, settings]) => ({
95
- where: `interfaces.${name}`,
96
- settings: settings.type === "lag" ? settings : undefined,
97
- })),
119
+ const holders: { where: string; settings: { mac_limit?: number; ethertypes?: readonly string[] } | undefined }[] = [
120
+ ...Derive.ports(device).map(([name, settings]) => ({ where: `interfaces.ethernet.${name}`, settings })),
121
+ ...Object.entries(device.interfaces?.lag ?? {}).map(([name, settings]) => ({ where: `interfaces.lag.${name}`, settings })),
98
122
  ];
99
123
 
100
124
  for (const { where, settings } of holders) {
@@ -115,26 +139,29 @@ export class Derive {
115
139
  const found: string[] = [];
116
140
 
117
141
  for (const service of Derive.SWITCH_SERVICES) {
118
- const settings = device.management?.[service];
142
+ // SNMP is in `monitoring`, the other services in `management`.
143
+ const group = service === "snmp" ? "monitoring" : "management";
144
+ const settings = service === "snmp" ? device.monitoring?.snmp : device.management?.[service];
119
145
  if (settings?.allow && !ownSources.includes(service))
120
- found.push(`management.${service}.allow, every service takes management.allow`);
121
- if (settings?.port) found.push(`management.${service}.port`);
146
+ found.push(`${group}.${service}.allow, every service takes management.allow`);
147
+ if (settings?.port) found.push(`${group}.${service}.port`);
122
148
  }
123
149
 
124
- for (const [name, user] of Object.entries(device.users ?? {})) {
125
- if (user.description) found.push(`users.${name}.description`);
126
- if (user.ssh_keys?.length) found.push(`users.${name}.ssh_keys`);
150
+ for (const [name, user] of Object.entries(device.system?.users ?? {})) {
151
+ if (user?.description) found.push(`system.users.${name}.description`);
152
+ if (user?.ssh_keys?.length) found.push(`system.users.${name}.ssh_keys`);
127
153
  }
128
154
 
129
- if (device.system?.ip) found.push("system.ip");
155
+ if (device.system?.ip_stack) found.push("system.ip_stack");
130
156
 
131
- for (const [name, port] of Object.entries(device.ports ?? {})) {
132
- if (port?.hardware_offload !== undefined) found.push(`ports.${name}.hardware_offload`);
157
+ for (const [name, port] of Derive.ports(device)) {
158
+ if (port.hardware_offload !== undefined) found.push(`interfaces.ethernet.${name}.hardware_offload`);
133
159
  }
134
160
 
135
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
136
- if (iface.addresses?.some((entry) => typeof entry !== "string")) found.push(`interfaces.${name}.addresses with a peer`);
137
- if (iface.hardware_offload !== undefined) found.push(`interfaces.${name}.hardware_offload`);
161
+ for (const { name, kind, settings } of Derive.interfaces(device)) {
162
+ if (settings.addresses?.some((entry) => typeof entry !== "string"))
163
+ found.push(`interfaces.${kind}.${name}.addresses with a peer`);
164
+ if (settings.hardware_offload !== undefined) found.push(`interfaces.${kind}.${name}.hardware_offload`);
138
165
  }
139
166
 
140
167
  return found;
@@ -0,0 +1,9 @@
1
+ /** Something wrong with a config, as `circuit validate` reports it. */
2
+ export type Finding = {
3
+ /** An error stops `diff` and `apply`; a warning is reported only. */
4
+ level: "error" | "warning";
5
+ /** The device it is about, when it is about one. */
6
+ device?: string;
7
+ /** What is wrong, in a sentence. */
8
+ message: string;
9
+ };
@@ -1,8 +1,10 @@
1
1
  // Packet filtering as policies through the device and local-in policies to it, who reaches the device's own services, NAT as central source NAT and virtual IPs, and stateless ACLs on ports.
2
2
 
3
3
  import { Address } from "../../core/address";
4
- import type { AclRule, Device, Family, FilterMatch, FilterRule, NatRule, Ports, Prefix, Protocol } from "../../schema";
4
+ import type { AclRule, Device, Family, FilterMatch, FilterRule, NatRule, Ports, Prefix, Protocol, Service } from "../../model";
5
+ import { Derive } from "../derive";
5
6
  import { FortiOsNames } from "./names";
7
+
6
8
  import { FortiOsBlock, FortiOsTree } from "./tree";
7
9
 
8
10
  /** The address objects, groups and services rules name, each created once. */
@@ -48,7 +50,7 @@ class Objects {
48
50
  this.root.block("firewall ippool").entry(address).set("startip", address).set("endip", address);
49
51
  }
50
52
 
51
- service(match: FilterMatch<string, string> | undefined, family: Family): string {
53
+ service(match: FilterMatch | undefined, family: Family): string {
52
54
  const { name, block } = FortiOsFirewall.serviceOf(match, family);
53
55
 
54
56
  // Transparent mode deletes the factory's services, `ALL` with them, so every protocol is a service of Circuit's own there.
@@ -69,7 +71,7 @@ export class FortiOsFirewall {
69
71
  /** IP protocol numbers, for a protocol the platform names only by number. */
70
72
  static readonly PROTOCOL_NUMBER: Record<Protocol, number> = { tcp: 6, udp: 17, icmp: 1, gre: 47, esp: 50, ah: 51, ospf: 89, vrrp: 112 };
71
73
 
72
- /** The device's own services, by the config's name, with the protocol and port each listens on unless the config moves it. */
74
+ /** The device's own services, by the name their guards take, with the protocol and port each listens on unless the config moves it. */
73
75
  private static readonly SERVICES = {
74
76
  ssh: { protocol: "tcp", port: 22 },
75
77
  https: { protocol: "tcp", port: 443 },
@@ -79,25 +81,34 @@ export class FortiOsFirewall {
79
81
  native: { protocol: "tcp", port: 541 },
80
82
  } as const;
81
83
 
84
+ /** A service's settings in the config: SNMP is in `monitoring`, the native protocol is `management.native_protocol`. */
85
+ private static serviceSettings(device: Device, service: keyof typeof FortiOsFirewall.SERVICES): Service | undefined {
86
+ if (service === "snmp") return device.monitoring?.snmp;
87
+ if (service === "native") return device.management?.native_protocol;
88
+ return device.management?.[service];
89
+ }
90
+
82
91
  /** Every protocol, as a service of Circuit's own, for a device in transparent mode. */
83
92
  static readonly ALL_IN_TRANSPARENT = "circuit-all";
84
93
 
85
94
  /** NAT rules of both directions, which central NAT needs turned on. */
86
- static natRules(device: Device): NatRule<string, string>[] {
95
+ static natRules(device: Device): NatRule[] {
87
96
  const nat = device.firewall?.nat;
88
97
  return [...(nat?.source ?? []), ...(nat?.destination ?? [])];
89
98
  }
90
99
 
91
100
  /** Which families a rule applies to: its own, its addresses', or both. */
92
- static familiesOf(device: Device, match: FilterMatch<string, string> | undefined): Family[] {
101
+ static familiesOf(device: Device, match: FilterMatch | undefined): Family[] {
93
102
  if (match?.family) return [match.family];
94
103
 
95
- const prefixes = [match?.src, match?.dst].filter((prefix): prefix is Prefix => prefix !== undefined);
104
+ const prefixes = [FortiOsNames.single(match?.src), FortiOsNames.single(match?.dst)].filter(
105
+ (prefix): prefix is Prefix => prefix !== undefined
106
+ );
96
107
  if (prefixes.length) return [...new Set(prefixes.map(FortiOsNames.familyOfPrefix))];
97
108
 
98
109
  let families: Family[] = ["ipv4", "ipv6"];
99
110
 
100
- for (const set of [match?.src_set, match?.dst_set]) {
111
+ for (const set of [match?.src_address_set, match?.dst_address_set]) {
101
112
  if (!set) continue;
102
113
 
103
114
  const present = new Set((device.firewall?.address_sets?.[set] ?? []).map(FortiOsNames.familyOfPrefix));
@@ -116,7 +127,7 @@ export class FortiOsFirewall {
116
127
  }
117
128
 
118
129
  /** The service a rule matches, by name, with what it holds. `ALL` when the rule names no protocol and no port; ports without a protocol are TCP's and UDP's. */
119
- static serviceOf(match: FilterMatch<string, string> | undefined, family: Family): { name: string; block?: FortiOsBlock } {
130
+ static serviceOf(match: FilterMatch | undefined, family: Family): { name: string; block?: FortiOsBlock } {
120
131
  const protocol = match?.protocol;
121
132
  const destinations = FortiOsFirewall.portList(match?.dst_port);
122
133
  const sources = FortiOsFirewall.portList(match?.src_port);
@@ -145,10 +156,10 @@ export class FortiOsFirewall {
145
156
  }
146
157
 
147
158
  /** A rule the platform keeps without being told: replies to a session it let through, and packets that belong to one, pass on their own. */
148
- static isImplied(rule: FilterRule<string, string>): boolean {
149
- const state = rule.match?.state;
159
+ static isImplied(rule: FilterRule): boolean {
160
+ const state = rule.if?.connection_state;
150
161
  return (
151
- rule.action === "accept" &&
162
+ rule.then === "accept" &&
152
163
  state !== undefined &&
153
164
  state.length > 0 &&
154
165
  state.every((one) => one === "established" || one === "related")
@@ -175,17 +186,17 @@ export class FortiOsFirewall {
175
186
  for (const rule of forward?.rules ?? []) {
176
187
  if (FortiOsFirewall.isImplied(rule)) continue;
177
188
 
178
- for (const family of FortiOsFirewall.familiesOf(device, rule.match)) {
189
+ for (const family of FortiOsFirewall.familiesOf(device, rule.if)) {
179
190
  const table = root.block(family === "ipv4" ? "firewall policy" : "firewall policy6");
180
191
  FortiOsFirewall.policy(table.entry(++policies[family]), objects, device, rule, family);
181
192
  }
182
193
  }
183
194
 
184
195
  // A packet no rule matched is accepted unless the chain drops it; the platform's own last word is deny.
185
- if (forward?.default !== "drop") {
196
+ if (forward?.default_action !== "drop") {
186
197
  for (const family of ["ipv4", "ipv6"] as const) {
187
198
  const table = root.block(family === "ipv4" ? "firewall policy" : "firewall policy6");
188
- FortiOsFirewall.policy(table.entry(++policies[family]), objects, device, { action: "accept" }, family);
199
+ FortiOsFirewall.policy(table.entry(++policies[family]), objects, device, { then: "accept" }, family);
189
200
  }
190
201
  }
191
202
 
@@ -200,14 +211,14 @@ export class FortiOsFirewall {
200
211
  for (const rule of input?.rules ?? []) {
201
212
  if (FortiOsFirewall.isImplied(rule)) continue;
202
213
 
203
- for (const family of FortiOsFirewall.familiesOf(device, rule.match)) {
214
+ for (const family of FortiOsFirewall.familiesOf(device, rule.if)) {
204
215
  FortiOsFirewall.localIn(localInEntry(family), objects, device, rule, family);
205
216
  }
206
217
  }
207
218
 
208
- if (input?.default === "drop") {
219
+ if (input?.default_action === "drop") {
209
220
  for (const family of ["ipv4", "ipv6"] as const) {
210
- FortiOsFirewall.localIn(localInEntry(family), objects, device, { action: "drop" }, family);
221
+ FortiOsFirewall.localIn(localInEntry(family), objects, device, { then: "drop" }, family);
211
222
  }
212
223
  }
213
224
 
@@ -227,13 +238,12 @@ export class FortiOsFirewall {
227
238
  private static management(entry: (family: Family) => FortiOsBlock, objects: Objects, device: Device): void {
228
239
  type Services = typeof FortiOsFirewall.SERVICES;
229
240
 
230
- const config = device.management ?? {};
231
241
  const guards = new Map<string, { sources: readonly string[]; services: string[] }>();
232
242
 
233
243
  for (const [service, listener] of Object.entries(FortiOsFirewall.SERVICES) as [keyof Services, Services[keyof Services]][]) {
234
- const settings = config[service];
235
- const sources = settings?.allow ?? config.allow;
236
- if (!settings || !sources?.length) continue;
244
+ const settings = FortiOsFirewall.serviceSettings(device, service);
245
+ const sources = settings?.allow ?? FortiOsNames.allowed(device);
246
+ if (!settings || !sources.length) continue;
237
247
 
238
248
  const name = settings.allow ? `circuit-${service}` : "circuit-management";
239
249
  const guard = guards.get(name) ?? { sources, services: [] };
@@ -274,52 +284,52 @@ export class FortiOsFirewall {
274
284
  }
275
285
 
276
286
  /** A policy for traffic through the device. A rejection answers the sender; the platform's deny is silent otherwise. */
277
- private static policy(entry: FortiOsBlock, objects: Objects, device: Device, rule: FilterRule<string, string>, family: Family): void {
278
- const match = rule.match;
279
-
280
- entry.set("srcintf", FortiOsFirewall.interfaceOf(device, match?.in_interface));
281
- entry.set("dstintf", FortiOsFirewall.interfaceOf(device, match?.out_interface));
282
- entry.set("srcaddr", FortiOsTree.quote(objects.address(match?.src, match?.src_set, family)));
283
- entry.set("dstaddr", FortiOsTree.quote(objects.address(match?.dst, match?.dst_set, family)));
284
- entry.set("action", rule.action === "accept" ? "accept" : "deny");
287
+ private static policy(entry: FortiOsBlock, objects: Objects, device: Device, rule: FilterRule, family: Family): void {
288
+ const match = rule.if;
289
+
290
+ entry.set("srcintf", FortiOsFirewall.interfaceOf(device, FortiOsNames.single(match?.in_interface)));
291
+ entry.set("dstintf", FortiOsFirewall.interfaceOf(device, FortiOsNames.single(match?.out_interface)));
292
+ entry.set("srcaddr", FortiOsTree.quote(objects.address(FortiOsNames.single(match?.src), match?.src_address_set, family)));
293
+ entry.set("dstaddr", FortiOsTree.quote(objects.address(FortiOsNames.single(match?.dst), match?.dst_address_set, family)));
294
+ entry.set("action", rule.then === "accept" ? "accept" : "deny");
285
295
  entry.set("schedule", FortiOsTree.quote("always"));
286
296
  entry.set("service", FortiOsTree.quote(objects.service(match, family)));
287
297
 
288
298
  // The device gives a deny policy `logtraffic disable` of its own accord.
289
- if (rule.action !== "accept") entry.set("logtraffic", "disable");
290
- if (rule.action === "accept" && !device.hardware_offload && !rule.offload) entry.set("auto-asic-offload", "disable");
299
+ if (rule.then !== "accept") entry.set("logtraffic", "disable");
300
+ if (rule.then === "accept" && !device.system?.hardware_offload && !rule.fast_path) entry.set("auto-asic-offload", "disable");
291
301
  if (rule.description) entry.set("comments", FortiOsTree.quote(rule.description));
292
- if (rule.action === "reject") entry.set("send-deny-packet", "enable");
302
+ if (rule.then === "reject") entry.set("send-deny-packet", "enable");
293
303
  }
294
304
 
295
305
  /** A local-in policy, for traffic to the device itself. */
296
- private static localIn(entry: FortiOsBlock, objects: Objects, device: Device, rule: FilterRule<string, string>, family: Family): void {
297
- const match = rule.match;
306
+ private static localIn(entry: FortiOsBlock, objects: Objects, device: Device, rule: FilterRule, family: Family): void {
307
+ const match = rule.if;
298
308
 
299
- entry.set("intf", FortiOsFirewall.interfaceOf(device, match?.in_interface));
300
- entry.set("srcaddr", FortiOsTree.quote(objects.address(match?.src, match?.src_set, family)));
301
- entry.set("dstaddr", FortiOsTree.quote(objects.address(match?.dst, match?.dst_set, family)));
302
- entry.set("action", rule.action === "accept" ? "accept" : "deny");
309
+ entry.set("intf", FortiOsFirewall.interfaceOf(device, FortiOsNames.single(match?.in_interface)));
310
+ entry.set("srcaddr", FortiOsTree.quote(objects.address(FortiOsNames.single(match?.src), match?.src_address_set, family)));
311
+ entry.set("dstaddr", FortiOsTree.quote(objects.address(FortiOsNames.single(match?.dst), match?.dst_address_set, family)));
312
+ entry.set("action", rule.then === "accept" ? "accept" : "deny");
303
313
  entry.set("service", FortiOsTree.quote(objects.service(match, family)));
304
314
  entry.set("schedule", FortiOsTree.quote("always"));
305
315
  if (rule.description) entry.set("comments", FortiOsTree.quote(rule.description));
306
316
  }
307
317
 
308
318
  /** Source NAT: to the outgoing interface's address, or to a pool of one address. */
309
- private static sourceNat(entry: FortiOsBlock, objects: Objects, device: Device, rule: NatRule<string, string>): void {
310
- const match = rule.match;
319
+ private static sourceNat(entry: FortiOsBlock, objects: Objects, device: Device, rule: NatRule): void {
320
+ const match = rule.if;
311
321
 
312
- entry.set("orig-addr", FortiOsTree.quote(objects.address(match?.src, match?.src_set, "ipv4")));
313
- entry.set("srcintf", FortiOsFirewall.interfaceOf(device, match?.in_interface));
314
- entry.set("dst-addr", FortiOsTree.quote(objects.address(match?.dst, match?.dst_set, "ipv4")));
315
- entry.set("dstintf", FortiOsFirewall.interfaceOf(device, match?.out_interface));
322
+ entry.set("orig-addr", FortiOsTree.quote(objects.address(FortiOsNames.single(match?.src), match?.src_address_set, "ipv4")));
323
+ entry.set("srcintf", FortiOsFirewall.interfaceOf(device, FortiOsNames.single(match?.in_interface)));
324
+ entry.set("dst-addr", FortiOsTree.quote(objects.address(FortiOsNames.single(match?.dst), match?.dst_address_set, "ipv4")));
325
+ entry.set("dstintf", FortiOsFirewall.interfaceOf(device, FortiOsNames.single(match?.out_interface)));
316
326
  if (match?.protocol) entry.set("protocol", FortiOsFirewall.PROTOCOL_NUMBER[match.protocol]);
317
327
 
318
328
  const ports = FortiOsFirewall.portList(match?.src_port);
319
329
 
320
- if (rule.action === "snat") {
321
- objects.pool(rule.to);
322
- entry.set("nat-ippool", FortiOsTree.quote(rule.to));
330
+ if (rule.then === "snat") {
331
+ objects.pool(rule.to_address);
332
+ entry.set("nat-ippool", FortiOsTree.quote(rule.to_address));
323
333
  // A pool keeps the source port only when told which.
324
334
  if (ports.length) entry.set("orig-port", ports[0]!).set("nat-port", rule.to_port ?? ports[0]!);
325
335
  } else if (ports.length) {
@@ -330,20 +340,22 @@ export class FortiOsFirewall {
330
340
  }
331
341
 
332
342
  /** Destination NAT, as a virtual IP: the address it answers on, and the port it forwards when the rule names one. */
333
- private static destinationNat(entry: FortiOsBlock, device: Device, rule: NatRule<string, string>): void {
334
- if (rule.action !== "dnat") return;
343
+ private static destinationNat(entry: FortiOsBlock, device: Device, rule: NatRule): void {
344
+ if (rule.then !== "dnat") return;
335
345
 
336
- const match = rule.match;
346
+ const match = rule.if;
347
+ const dst = FortiOsNames.single(match?.dst);
348
+ const src = FortiOsNames.single(match?.src);
337
349
  if (rule.description) entry.set("comment", FortiOsTree.quote(rule.description));
338
- if (match?.dst) entry.set("extip", Address.host(FortiOsNames.withLength(match.dst)));
339
- entry.set("extintf", FortiOsFirewall.interfaceOf(device, match?.in_interface));
350
+ if (dst) entry.set("extip", Address.host(FortiOsNames.withLength(dst)));
351
+ entry.set("extintf", FortiOsFirewall.interfaceOf(device, FortiOsNames.single(match?.in_interface)));
340
352
 
341
- const sources = match?.src ? [match.src] : match?.src_set ? (device.firewall?.address_sets?.[match.src_set] ?? []) : [];
353
+ const sources = src ? [src] : match?.src_address_set ? (device.firewall?.address_sets?.[match.src_address_set] ?? []) : [];
342
354
  if (sources.length) entry.set("src-filter", FortiOsTree.quoteAll(sources.map(FortiOsNames.network)));
343
355
 
344
356
  const ports = FortiOsFirewall.portList(match?.dst_port);
345
357
  if (ports.length) entry.set("portforward", "enable");
346
- entry.set("mappedip", FortiOsTree.quote(rule.to));
358
+ entry.set("mappedip", FortiOsTree.quote(rule.to_address));
347
359
 
348
360
  if (ports.length) {
349
361
  if (match?.protocol === "udp") entry.set("protocol", "udp");
@@ -358,20 +370,20 @@ export class FortiOsFirewall {
358
370
  private static acls(root: FortiOsBlock, objects: Objects, device: Device): void {
359
371
  const counters = { ipv4: 0, ipv6: 0 };
360
372
 
361
- for (const [port, settings] of Object.entries(device.ports ?? {})) {
362
- if (!settings?.acl) continue;
373
+ for (const [port, settings] of Derive.ports(device)) {
374
+ if (!settings.ingress_acl) continue;
363
375
 
364
- for (const rule of device.acls?.[settings.acl] ?? []) {
365
- if (rule.action !== "drop") continue;
376
+ for (const rule of device.switching?.acls?.[settings.ingress_acl] ?? []) {
377
+ if (rule.then !== "drop") continue;
366
378
 
367
- const match = rule.match as FilterMatch<string, string> | undefined;
379
+ const { vlan: _, ...match } = rule.if ?? {};
368
380
 
369
381
  for (const family of FortiOsFirewall.familiesOf(device, match)) {
370
382
  const entry = root.block(family === "ipv4" ? "firewall acl" : "firewall acl6").entry(++counters[family]);
371
383
  if (rule.description) entry.set("comments", FortiOsTree.quote(rule.description));
372
384
  entry.set("interface", FortiOsTree.quote(FortiOsNames.nameOf(device, port)));
373
- entry.set("srcaddr", FortiOsTree.quote(objects.address(match?.src, undefined, family)));
374
- entry.set("dstaddr", FortiOsTree.quote(objects.address(match?.dst, undefined, family)));
385
+ entry.set("srcaddr", FortiOsTree.quote(objects.address(FortiOsNames.single(match.src), undefined, family)));
386
+ entry.set("dstaddr", FortiOsTree.quote(objects.address(FortiOsNames.single(match.dst), undefined, family)));
375
387
  entry.set("service", FortiOsTree.quote(objects.service(match, family)));
376
388
  }
377
389
  }
@@ -379,8 +391,8 @@ export class FortiOsFirewall {
379
391
  }
380
392
 
381
393
  /** Whether a port's ACL drops before it accepts: an accept before a drop is an exception the platform's ACLs cannot hold. */
382
- static acceptsBeforeDrop(rules: readonly AclRule<string>[]): boolean {
383
- const lastDrop = rules.map((rule) => rule.action).lastIndexOf("drop");
384
- return rules.slice(0, Math.max(0, lastDrop)).some((rule) => rule.action === "accept");
394
+ static acceptsBeforeDrop(rules: readonly AclRule[]): boolean {
395
+ const lastDrop = rules.map((rule) => rule.then).lastIndexOf("drop");
396
+ return rules.slice(0, Math.max(0, lastDrop)).some((rule) => rule.then === "accept");
385
397
  }
386
398
  }