@takodotid/circuit 0.34.3 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +14 -15
  2. package/dist/chunks/index-20kcjxjg.js +174 -0
  3. package/dist/chunks/index-2r4bbhac.js +977 -0
  4. package/dist/chunks/index-n7w3tg5y.js +149 -0
  5. package/dist/chunks/new-w3zys5y4.js +172 -0
  6. package/dist/chunks/server-gzsf5me5.js +259 -0
  7. package/dist/cli/index.js +11345 -9024
  8. package/dist/helpers/index.js +594 -0
  9. package/dist/index.js +9 -28
  10. package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
  11. package/dist/types/core/address.d.ts +44 -0
  12. package/dist/types/core/cabling.d.ts +41 -0
  13. package/dist/types/core/cross-checks.d.ts +28 -0
  14. package/dist/types/core/define.d.ts +25 -3
  15. package/dist/types/core/project-files.d.ts +12 -0
  16. package/dist/types/core/schema-docs.d.ts +13 -4
  17. package/dist/types/core/secrets.d.ts +62 -21
  18. package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
  19. package/dist/types/core/utils.d.ts +8 -0
  20. package/dist/types/core/validator.d.ts +19 -0
  21. package/dist/types/helpers/bgp.d.ts +97 -0
  22. package/dist/types/helpers/checks.d.ts +32 -0
  23. package/dist/types/helpers/exchange.d.ts +49 -0
  24. package/dist/types/helpers/filters.d.ts +51 -0
  25. package/dist/types/helpers/index.d.ts +10 -0
  26. package/dist/types/helpers/route-server.d.ts +52 -0
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/integrations/librenms.d.ts +60 -0
  29. package/dist/types/integrations/netbox.d.ts +89 -0
  30. package/dist/types/integrations/peeringdb.d.ts +24 -0
  31. package/dist/types/integrations/prometheus.d.ts +24 -0
  32. package/dist/types/integrations/registries.d.ts +8 -0
  33. package/dist/types/platforms/bgp-wait.d.ts +18 -0
  34. package/dist/types/platforms/catalog.d.ts +109 -0
  35. package/dist/types/platforms/derive.d.ts +17 -0
  36. package/dist/types/platforms/fortios/firewall.d.ts +45 -0
  37. package/dist/types/platforms/fortios/index.d.ts +134 -0
  38. package/dist/types/platforms/fortios/names.d.ts +52 -0
  39. package/dist/types/platforms/fortios/plan.d.ts +104 -0
  40. package/dist/types/platforms/fortios/render.d.ts +75 -0
  41. package/dist/types/platforms/fortios/routing.d.ts +56 -0
  42. package/dist/types/platforms/fortios/tree.d.ts +37 -0
  43. package/dist/types/platforms/index.d.ts +13 -0
  44. package/dist/types/platforms/junos/bgp.d.ts +44 -0
  45. package/dist/types/platforms/junos/index.d.ts +114 -0
  46. package/dist/types/platforms/junos/render.d.ts +154 -0
  47. package/dist/types/platforms/line-diff.d.ts +67 -0
  48. package/dist/types/platforms/platform.d.ts +120 -0
  49. package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
  50. package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
  51. package/dist/types/platforms/routeros/export.d.ts +59 -0
  52. package/dist/types/platforms/routeros/index.d.ts +96 -0
  53. package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
  54. package/dist/types/platforms/routeros/plan.d.ts +54 -0
  55. package/dist/types/platforms/routeros/render.d.ts +240 -0
  56. package/dist/types/platforms/vrp/index.d.ts +83 -0
  57. package/dist/types/platforms/vrp/render.d.ts +96 -0
  58. package/dist/types/schema/common.d.ts +12 -6
  59. package/dist/types/schema/index.d.ts +1 -0
  60. package/dist/types/schema/interfaces.d.ts +14 -6
  61. package/dist/types/schema/policy.d.ts +18 -10
  62. package/dist/types/schema/routing.d.ts +14 -1
  63. package/dist/types/schema/system.d.ts +16 -6
  64. package/dist/types/schema/time-zones.d.ts +2 -0
  65. package/dist/types/transport/console.d.ts +79 -23
  66. package/dist/types/transport/path-check.d.ts +32 -0
  67. package/dist/types/transport/ssh.d.ts +17 -15
  68. package/package.json +18 -12
  69. package/src/cli/graph-format.ts +91 -0
  70. package/src/cli/index.ts +1074 -484
  71. package/src/cli/new.ts +208 -175
  72. package/src/cli/rename.ts +97 -91
  73. package/src/core/address-space.ts +320 -0
  74. package/src/core/address.ts +202 -0
  75. package/src/core/cabling.ts +154 -0
  76. package/src/core/cross-checks.ts +364 -0
  77. package/src/core/define.ts +26 -3
  78. package/src/core/project-files.ts +35 -0
  79. package/src/core/schema-docs.ts +138 -130
  80. package/src/core/secrets.ts +170 -76
  81. package/src/core/topology.ts +656 -0
  82. package/src/core/utils.ts +37 -0
  83. package/src/core/validator.ts +407 -0
  84. package/src/helpers/bgp.ts +301 -0
  85. package/src/helpers/checks.ts +178 -0
  86. package/src/helpers/exchange.ts +117 -0
  87. package/src/helpers/filters.ts +107 -0
  88. package/src/helpers/index.ts +12 -0
  89. package/src/helpers/route-server.ts +212 -0
  90. package/src/index.ts +6 -3
  91. package/src/integrations/librenms.ts +291 -0
  92. package/src/integrations/netbox.ts +620 -0
  93. package/src/integrations/peeringdb.ts +173 -0
  94. package/src/integrations/prometheus.ts +90 -0
  95. package/src/integrations/registries.ts +25 -0
  96. package/src/platforms/bgp-wait.ts +41 -0
  97. package/src/platforms/catalog.ts +69 -0
  98. package/src/platforms/derive.ts +117 -0
  99. package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
  100. package/src/platforms/fortios/firewall.ts +377 -0
  101. package/src/platforms/fortios/index.ts +1060 -0
  102. package/src/platforms/fortios/names.ts +130 -0
  103. package/src/platforms/fortios/plan.ts +929 -0
  104. package/src/platforms/fortios/render.ts +779 -0
  105. package/src/platforms/fortios/routing.ts +476 -0
  106. package/src/platforms/fortios/tree.ts +129 -0
  107. package/src/platforms/index.ts +36 -0
  108. package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
  109. package/src/platforms/junos/bgp.ts +293 -0
  110. package/src/platforms/junos/index.ts +1006 -0
  111. package/src/platforms/junos/render.ts +967 -0
  112. package/src/platforms/line-diff.ts +244 -0
  113. package/src/platforms/platform.ts +122 -0
  114. package/src/platforms/raisecom-ros/TRAPS.md +40 -0
  115. package/src/platforms/raisecom-ros/index.ts +731 -0
  116. package/src/platforms/raisecom-ros/render.ts +328 -0
  117. package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
  118. package/src/platforms/routeros/export.ts +342 -0
  119. package/src/platforms/routeros/index.ts +636 -0
  120. package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
  121. package/src/platforms/routeros/plan.ts +444 -0
  122. package/src/platforms/routeros/render.ts +1483 -0
  123. package/src/platforms/vrp/TRAPS.md +45 -0
  124. package/src/platforms/vrp/index.ts +652 -0
  125. package/src/platforms/vrp/render.ts +615 -0
  126. package/src/schema/common.ts +15 -6
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/interfaces.ts +15 -5
  129. package/src/schema/policy.ts +22 -10
  130. package/src/schema/routing.ts +16 -1
  131. package/src/schema/system.ts +16 -6
  132. package/src/schema/time-zones.ts +482 -0
  133. package/src/studio/data.ts +172 -168
  134. package/src/studio/git.ts +95 -90
  135. package/src/studio/public/app.js +161 -150
  136. package/src/studio/public/compare.js +219 -212
  137. package/src/studio/public/diff.js +77 -75
  138. package/src/studio/public/dom.js +118 -108
  139. package/src/studio/public/evaluate.js +584 -545
  140. package/src/studio/public/export.js +120 -118
  141. package/src/studio/public/graph.js +1343 -1266
  142. package/src/studio/public/net.js +115 -109
  143. package/src/studio/public/tables.js +1135 -1083
  144. package/src/studio/public/tools.js +561 -529
  145. package/src/studio/server.ts +202 -196
  146. package/src/transport/console.ts +363 -271
  147. package/src/transport/path-check.ts +91 -0
  148. package/src/transport/ssh.ts +98 -94
  149. package/templates/colocation/checks.ts +11 -6
  150. package/templates/colocation/edge-01.ts +9 -9
  151. package/templates/colocation/routing.ts +2 -2
  152. package/templates/colocation/tenancy.ts +3 -3
  153. package/templates/colocation/tor-01.ts +3 -3
  154. package/templates/edge-router/checks.ts +4 -4
  155. package/templates/edge-router/edge-01.ts +5 -5
  156. package/templates/edge-router/routing.ts +2 -2
  157. package/templates/project/AGENTS.md +5 -1
  158. package/templates/single-site/checks.ts +2 -2
  159. package/dist/chunks/index-47debt16.js +0 -956
  160. package/dist/chunks/index-v8zgr7wm.js +0 -128
  161. package/dist/chunks/new-ny89s7y7.js +0 -166
  162. package/dist/chunks/server-tjbb3rqc.js +0 -251
  163. package/dist/presets/index.js +0 -394
  164. package/dist/types/adapters/devices/catalog.d.ts +0 -86
  165. package/dist/types/adapters/devices/derive.d.ts +0 -7
  166. package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
  167. package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
  168. package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
  169. package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
  170. package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
  171. package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
  172. package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
  173. package/dist/types/adapters/devices/index.d.ts +0 -3
  174. package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
  175. package/dist/types/adapters/devices/junos/index.d.ts +0 -42
  176. package/dist/types/adapters/devices/junos/render.d.ts +0 -52
  177. package/dist/types/adapters/devices/lines.d.ts +0 -56
  178. package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
  179. package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
  180. package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
  181. package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
  182. package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
  183. package/dist/types/adapters/devices/types.d.ts +0 -40
  184. package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
  185. package/dist/types/adapters/registries/index.d.ts +0 -3
  186. package/dist/types/adapters/registries/librenms.d.ts +0 -15
  187. package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
  188. package/dist/types/adapters/registries/prometheus.d.ts +0 -16
  189. package/dist/types/core/across.d.ts +0 -4
  190. package/dist/types/core/addr.d.ts +0 -34
  191. package/dist/types/core/files.d.ts +0 -6
  192. package/dist/types/core/merge.d.ts +0 -4
  193. package/dist/types/core/time.d.ts +0 -2
  194. package/dist/types/core/validate.d.ts +0 -2
  195. package/dist/types/presets/index.d.ts +0 -183
  196. package/src/adapters/devices/catalog.ts +0 -41
  197. package/src/adapters/devices/derive.ts +0 -59
  198. package/src/adapters/devices/fortios/firewall.ts +0 -362
  199. package/src/adapters/devices/fortios/index.ts +0 -923
  200. package/src/adapters/devices/fortios/names.ts +0 -115
  201. package/src/adapters/devices/fortios/plan.ts +0 -888
  202. package/src/adapters/devices/fortios/render.ts +0 -768
  203. package/src/adapters/devices/fortios/routing.ts +0 -444
  204. package/src/adapters/devices/fortios/tree.ts +0 -121
  205. package/src/adapters/devices/index.ts +0 -9
  206. package/src/adapters/devices/junos/bgp.ts +0 -269
  207. package/src/adapters/devices/junos/index.ts +0 -862
  208. package/src/adapters/devices/junos/render.ts +0 -872
  209. package/src/adapters/devices/lines.ts +0 -212
  210. package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -700
  212. package/src/adapters/devices/routeros/export.ts +0 -321
  213. package/src/adapters/devices/routeros/index.ts +0 -346
  214. package/src/adapters/devices/routeros/plan.ts +0 -398
  215. package/src/adapters/devices/routeros/render.ts +0 -1490
  216. package/src/adapters/devices/types.ts +0 -38
  217. package/src/adapters/devices/vrp/TRAPS.md +0 -26
  218. package/src/adapters/devices/vrp/index.ts +0 -865
  219. package/src/adapters/registries/index.ts +0 -19
  220. package/src/adapters/registries/librenms.ts +0 -92
  221. package/src/adapters/registries/peeringdb.ts +0 -169
  222. package/src/adapters/registries/prometheus.ts +0 -71
  223. package/src/cli/graph.ts +0 -89
  224. package/src/core/across.ts +0 -350
  225. package/src/core/addr.ts +0 -174
  226. package/src/core/addresses.ts +0 -316
  227. package/src/core/files.ts +0 -32
  228. package/src/core/graph.ts +0 -640
  229. package/src/core/merge.ts +0 -19
  230. package/src/core/time.ts +0 -15
  231. package/src/core/validate.ts +0 -348
  232. package/src/presets/index.ts +0 -579
  233. /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
  234. /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
  235. /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
  236. /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
  237. /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
  238. /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
  239. /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
  240. /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
@@ -0,0 +1,407 @@
1
+ // Rules every network has to satisfy, whatever its design. Types catch most of this at compile time; these run again at load, because config is data and may not come from TypeScript.
2
+
3
+ import { X509Certificate } from "node:crypto";
4
+ import { Platforms } from "../platforms";
5
+ import { Catalog } from "../platforms/catalog";
6
+ import type { Device, Switched } from "../schema";
7
+ import { Address } from "./address";
8
+ import { CrossChecks } from "./cross-checks";
9
+ import type { Finding, Network } from "./define";
10
+
11
+ type Report = (message: string) => void;
12
+
13
+ /** Names a device declares, and helpers to report a reference to one it does not. */
14
+ class Names {
15
+ readonly ports: string[];
16
+
17
+ constructor(
18
+ readonly device: Device,
19
+ private readonly report: Report
20
+ ) {
21
+ let ports: string[] = [];
22
+
23
+ try {
24
+ ports = Catalog.ports(device.platform, device.model);
25
+ } catch (error) {
26
+ report((error as Error).message);
27
+ }
28
+
29
+ this.ports = ports;
30
+ }
31
+
32
+ isInterface(name: string): boolean {
33
+ return name in (this.device.interfaces ?? {}) || this.ports.includes(name);
34
+ }
35
+
36
+ /** Report `name` when it is set and not declared. */
37
+ expect(what: string, name: string | undefined, declared: boolean): void {
38
+ if (name !== undefined && !declared) this.report(`${what} '${name}' is not declared`);
39
+ }
40
+
41
+ expectInterface(what: string, name: string | undefined): void {
42
+ this.expect(what, name, name !== undefined && this.isInterface(name));
43
+ }
44
+ }
45
+
46
+ type Checker = (where: string, value: string) => void;
47
+
48
+ export class Validator {
49
+ static run(network: Network): Finding[] {
50
+ const findings: Finding[] = [];
51
+
52
+ const seen = new Set<string>();
53
+
54
+ for (const device of network.devices) {
55
+ if (seen.has(device.name)) findings.push({ level: "error", device: device.name, message: "declared twice" });
56
+ seen.add(device.name);
57
+ }
58
+
59
+ for (const device of network.devices) {
60
+ const report: Report = (message) => findings.push({ level: "error", device: device.name, message });
61
+ Validator.checkDevice(device, report);
62
+ findings.push(...Validator.checkLimits(device));
63
+
64
+ const adapter = Platforms.find(device.platform);
65
+ if (!adapter) report(`no adapter for platform ${device.platform}`);
66
+ else for (const field of adapter.unsupported(device)) report(`${device.platform} cannot express ${field}`);
67
+ }
68
+
69
+ findings.push(...CrossChecks.run(network.devices));
70
+ for (const check of network.checks ?? []) findings.push(...check(network.devices));
71
+ return findings;
72
+ }
73
+
74
+ private static checkDevice(device: Device, report: Report): void {
75
+ const names = new Names(device, report);
76
+ const cidr: Checker = (where, value) => {
77
+ if (!Address.isCidr(value)) report(`${where}: '${value}' is not an address with a length`);
78
+ };
79
+ const ip: Checker = (where, value) => {
80
+ if (!Address.isValid(value)) report(`${where}: '${value}' is not an address`);
81
+ };
82
+
83
+ if (!device.users?.[device.connection.user]) report(`connection user '${device.connection.user}' is not one of its users`);
84
+ if (!device.connection.host && !device.connection.console)
85
+ report("connection has neither a host nor a console to reach the device through");
86
+
87
+ Validator.checkVlans(device, report);
88
+ Validator.checkAddresses(device, report, cidr, ip);
89
+ Validator.checkPorts(device, names, report);
90
+ Validator.checkInterfaces(device, names, report, ip);
91
+ Validator.checkRouting(device, names, report, cidr, ip);
92
+ Validator.checkPolicies(device, names, report, cidr);
93
+ Validator.checkFirewall(device, names, cidr);
94
+ Validator.checkServices(device, names, report, cidr);
95
+ Validator.checkCertificates(device, names, report);
96
+ }
97
+
98
+ /** What the model's hardware holds, from the catalog. A LAG with too many members is an error; too many switch rules is a warning, since the limit is only roughly known. */
99
+ private static checkLimits(device: Device): Finding[] {
100
+ const limits = Catalog.limits(device.platform, device.model);
101
+ const findings: Finding[] = [];
102
+ const report = (level: Finding["level"], message: string) => findings.push({ level, device: device.name, message });
103
+
104
+ if (limits.lag_members !== undefined) {
105
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
106
+ if (iface.type !== "lag") continue;
107
+
108
+ const members = Object.values(device.ports ?? {}).filter((port) => port?.lag === name).length;
109
+ if (members > limits.lag_members) {
110
+ report("error", `LAG ${name} has ${members} members; ${device.model} holds at most ${limits.lag_members}`);
111
+ }
112
+ }
113
+ }
114
+
115
+ if (limits.switch_rules === undefined && limits.switch_port_ranges === undefined) return findings;
116
+
117
+ const rules = Platforms.find(device.platform)?.switchRules?.(device);
118
+ if (!rules) return findings;
119
+
120
+ const after = "A rule it cannot hold is invalid, and so is every rule after it";
121
+
122
+ if (limits.switch_rules !== undefined && rules.count > limits.switch_rules) {
123
+ report("warning", `ACLs make ${rules.count} switch rules; a ${device.model} held about ${limits.switch_rules}. ${after}`);
124
+ }
125
+
126
+ if (limits.switch_port_ranges !== undefined && rules.ranges > limits.switch_port_ranges) {
127
+ report(
128
+ "warning",
129
+ `ACLs use ${rules.ranges} distinct port ranges; a ${device.model} held ${limits.switch_port_ranges}. ${after}`
130
+ );
131
+ }
132
+
133
+ return findings;
134
+ }
135
+
136
+ private static checkVlans(device: Device, report: Report): void {
137
+ const ids = new Map<number, string>();
138
+
139
+ for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
140
+ if (!Number.isInteger(vlan.id) || vlan.id < 1 || vlan.id > 4094) report(`VLAN ${name} id ${vlan.id} is outside 1-4094`);
141
+ if (ids.has(vlan.id)) report(`VLANs ${ids.get(vlan.id)} and ${name} share id ${vlan.id}`);
142
+ ids.set(vlan.id, name);
143
+ }
144
+ }
145
+
146
+ /** Every address is valid, and no address sits on two interfaces of one routing table. Two VRFs may hold the same address. */
147
+ private static checkAddresses(device: Device, report: Report, cidr: Checker, ip: Checker): void {
148
+ const holderOf = new Map<string, string>();
149
+ const holders = [
150
+ ...Object.entries(device.ports ?? {}).map(([name, settings]) => ({ where: `port ${name}`, settings })),
151
+ ...Object.entries(device.interfaces ?? {}).map(([name, settings]) => ({ where: `interface ${name}`, settings })),
152
+ ];
153
+
154
+ for (const { where, settings } of holders) {
155
+ for (const entry of settings?.addresses ?? []) {
156
+ const address = typeof entry === "string" ? entry : entry.address;
157
+ cidr(where, address);
158
+ if (typeof entry !== "string") ip(where, entry.peer);
159
+
160
+ const bare = address.split("/")[0]!;
161
+ const key = `${settings?.vrf ?? ""} ${bare}`;
162
+ if (holderOf.has(key)) report(`${bare} is on both ${holderOf.get(key)} and ${where}`);
163
+ holderOf.set(key, where);
164
+ }
165
+
166
+ for (const group of settings?.vrrp ?? []) {
167
+ ip(`${where} VRRP ${group.id}`, group.address);
168
+ if (group.id < 1 || group.id > 255) report(`${where} VRRP id ${group.id} is outside 1-255`);
169
+ }
170
+ }
171
+ }
172
+
173
+ private static checkPorts(device: Device, names: Names, report: Report): void {
174
+ const vlans = device.vlans ?? {};
175
+
176
+ for (const [name, port] of Object.entries(device.ports ?? {})) {
177
+ if (!port) continue;
178
+
179
+ const switched = Boolean(port.access_vlan || port.trunk_vlans);
180
+
181
+ if (!names.ports.includes(name)) report(`port ${name} does not exist on ${device.model}`);
182
+ if (port.access_vlan && port.trunk_vlans) report(`port ${name} is both access and trunk`);
183
+ if (port.native_vlan && !port.trunk_vlans) report(`port ${name} has a native VLAN but is not a trunk`);
184
+ if (switched && port.addresses?.length) report(`port ${name} is switched and routed at once`);
185
+
186
+ if (port.lag && (switched || port.addresses)) {
187
+ report(`port ${name} is in a LAG and states its own switching or addresses; the LAG holds them`);
188
+ }
189
+
190
+ for (const vlan of [port.access_vlan, port.native_vlan, ...(port.trunk_vlans ?? [])]) {
191
+ names.expect(`port ${name} VLAN`, vlan, vlan === undefined || vlan in vlans);
192
+ }
193
+
194
+ names.expect(`port ${name} ACL`, port.acl, port.acl !== undefined && port.acl in (device.acls ?? {}));
195
+ names.expect(`port ${name} LAG`, port.lag, port.lag !== undefined && device.interfaces?.[port.lag]?.type === "lag");
196
+ names.expect(`port ${name} VRF`, port.vrf, port.vrf !== undefined && port.vrf in (device.vrfs ?? {}));
197
+ Validator.checkFrames(`port ${name}`, port, report);
198
+ }
199
+ }
200
+
201
+ /** A MAC limit is a whole number above 0, and an ethertype filter lets at least one kind in, each once. */
202
+ private static checkFrames(where: string, settings: Switched<string>, report: Report): void {
203
+ const limit = settings.mac_limit;
204
+ if (limit !== undefined && !(Number.isInteger(limit) && limit >= 1))
205
+ report(`${where} mac_limit ${limit} is not a whole number above 0`);
206
+
207
+ const kinds = settings.ethertypes;
208
+ if (kinds && !kinds.length) report(`${where} ethertypes is empty, so it would drop every frame`);
209
+ if (kinds && new Set(kinds).size !== kinds.length) report(`${where} ethertypes names a kind twice`);
210
+ }
211
+
212
+ private static checkInterfaces(device: Device, names: Names, report: Report, ip: Checker): void {
213
+ const vlans = device.vlans ?? {};
214
+
215
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
216
+ if (names.ports.includes(name)) report(`interface ${name} has the name of a port`);
217
+ names.expect(`interface ${name} VRF`, iface.vrf, iface.vrf !== undefined && iface.vrf in (device.vrfs ?? {}));
218
+
219
+ if (iface.type === "vlan") names.expect(`interface ${name} VLAN`, iface.vlan, iface.vlan in vlans);
220
+ if (iface.type === "gre" || iface.type === "vxlan") {
221
+ ip(`interface ${name}`, iface.local);
222
+ ip(`interface ${name}`, iface.remote);
223
+ }
224
+
225
+ if (iface.type === "lag") {
226
+ Validator.checkFrames(`LAG ${name}`, iface, report);
227
+
228
+ const members = Object.values(device.ports ?? {}).filter((port) => port?.lag === name);
229
+ if (!members.length) report(`LAG ${name} has no member ports`);
230
+ }
231
+
232
+ if (iface.type === "bridge") {
233
+ if (iface.members.length < 2) report(`bridge ${name} joins fewer than two interfaces`);
234
+
235
+ for (const member of iface.members) {
236
+ const joined = device.interfaces?.[member];
237
+ if (joined?.type !== "vlan") report(`bridge ${name}: member ${member} is not a VLAN interface`);
238
+ else if (joined.addresses?.length) report(`bridge ${name}: member ${member} has an address of its own`);
239
+
240
+ const others = Object.entries(device.interfaces ?? {}).filter(
241
+ ([other, settings]) => other !== name && settings.type === "bridge" && settings.members.includes(member)
242
+ );
243
+ if (others.length) report(`bridge ${name}: member ${member} is also in bridge ${others[0]![0]}`);
244
+ }
245
+ }
246
+
247
+ if (iface.type === "wireguard") {
248
+ for (const peer of iface.peers ?? []) {
249
+ for (const address of peer.allowed_addresses) {
250
+ if (!Address.isCidr(address))
251
+ report(`interface ${name} peer ${peer.name}: '${address}' is not an address with a length`);
252
+ }
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ private static checkRouting(device: Device, names: Names, report: Report, cidr: Checker, ip: Checker): void {
259
+ const routing = device.routing;
260
+ const policies = device.policies ?? {};
261
+
262
+ for (const route of routing?.static ?? []) {
263
+ cidr("static route", route.prefix);
264
+ if (!route.via && !route.interface && !route.blackhole) report(`static route ${route.prefix} has no next hop`);
265
+ if (route.via) ip(`static route ${route.prefix}`, route.via);
266
+ if (route.bfd && !route.via) report(`static route ${route.prefix} has bfd without via, the address BFD watches`);
267
+ names.expectInterface(`static route ${route.prefix} interface`, route.interface);
268
+ names.expect(`static route ${route.prefix} VRF`, route.vrf, route.vrf !== undefined && route.vrf in (device.vrfs ?? {}));
269
+ }
270
+
271
+ const bgp = routing?.bgp;
272
+ const isPolicy = (policy: string | undefined) => policy !== undefined && policy in policies;
273
+
274
+ for (const prefix of bgp?.networks ?? []) cidr("BGP network", prefix);
275
+
276
+ for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
277
+ for (const policy of [group.import, group.export]) {
278
+ names.expect(`BGP group ${name} policy`, policy, isPolicy(policy));
279
+ }
280
+ }
281
+
282
+ for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
283
+ ip(`neighbor ${name}`, neighbor.address);
284
+
285
+ const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
286
+ names.expect(`neighbor ${name} group`, neighbor.group, group !== undefined);
287
+
288
+ if (!(neighbor.remote_as ?? group?.remote_as)) report(`neighbor ${name} has no remote_as`);
289
+
290
+ for (const policy of [neighbor.import, neighbor.export]) {
291
+ names.expect(`neighbor ${name} policy`, policy, isPolicy(policy));
292
+ }
293
+
294
+ const local = neighbor.local_address ?? group?.local_address;
295
+ if (local && Address.family(local) !== Address.family(neighbor.address))
296
+ report(`neighbor ${name} local_address and address differ in family`);
297
+ }
298
+
299
+ for (const [areaId, area] of Object.entries(routing?.ospf?.areas ?? {})) {
300
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(areaId)) report(`OSPF area '${areaId}' is not written as a dotted area ID`);
301
+ for (const iface of Object.keys(area.interfaces)) names.expectInterface(`OSPF area ${areaId} interface`, iface);
302
+ }
303
+ }
304
+
305
+ private static checkPolicies(device: Device, names: Names, report: Report, cidr: Checker): void {
306
+ const policies = device.policies ?? {};
307
+ const sets = device.prefix_sets ?? {};
308
+
309
+ for (const [name, rules] of Object.entries(policies)) {
310
+ for (const rule of rules) {
311
+ names.expect(`policy ${name} call`, rule.call, rule.call !== undefined && rule.call in policies);
312
+ const set = rule.match?.prefix_set;
313
+ names.expect(`policy ${name} prefix set`, set, set !== undefined && set in sets);
314
+ if (rule.match?.prefix) cidr(`policy ${name}`, rule.match.prefix);
315
+ }
316
+ }
317
+
318
+ // A policy that calls itself, directly or not, never finishes.
319
+ const visit = (policy: string, path: string[]): void => {
320
+ if (path.includes(policy)) return report(`policy ${[...path, policy].join(" -> ")} calls itself`);
321
+
322
+ for (const rule of policies[policy] ?? []) if (rule.call) visit(rule.call, [...path, policy]);
323
+ };
324
+ for (const policy of Object.keys(policies)) visit(policy, []);
325
+ }
326
+
327
+ private static checkFirewall(device: Device, names: Names, cidr: Checker): void {
328
+ const firewall = device.firewall;
329
+ const sets = firewall?.address_sets ?? {};
330
+
331
+ for (const [name, members] of Object.entries(sets)) {
332
+ for (const member of members) {
333
+ if (!Address.isValid(member)) cidr(`address set ${name}`, member);
334
+ }
335
+ }
336
+
337
+ const filterChains = [firewall?.filter?.input, firewall?.filter?.forward, firewall?.filter?.output];
338
+ const filterRules = filterChains.flatMap((chain) => chain?.rules ?? []);
339
+ const natRules = [...(firewall?.nat?.source ?? []), ...(firewall?.nat?.destination ?? [])];
340
+
341
+ for (const rule of [...filterRules, ...natRules]) {
342
+ const match = rule.match;
343
+ if (!match) continue;
344
+
345
+ for (const set of [match.src_set, match.dst_set]) {
346
+ names.expect("address set", set, set !== undefined && set in sets);
347
+ }
348
+
349
+ for (const iface of [match.in_interface, match.out_interface]) names.expectInterface("interface", iface);
350
+
351
+ for (const address of [match.src, match.dst]) {
352
+ if (address) cidr("firewall rule", address);
353
+ }
354
+ }
355
+ }
356
+
357
+ private static checkServices(device: Device, names: Names, report: Report, cidr: Checker): void {
358
+ for (const [name, server] of Object.entries(device.dhcp ?? {})) {
359
+ names.expectInterface(`DHCP ${name} interface`, server.interface);
360
+ cidr(`DHCP ${name}`, server.network);
361
+ if (!Address.isCidr(server.network)) continue;
362
+
363
+ const reserved = (server.reservations ?? []).map((reservation) => reservation.address);
364
+ const addresses = [server.gateway, ...server.pool, ...reserved];
365
+
366
+ for (const address of addresses) {
367
+ if (!Address.contains(server.network, address)) report(`DHCP ${name}: ${address} is outside ${server.network}`);
368
+ }
369
+ }
370
+
371
+ for (const [name, relay] of Object.entries(device.dhcp_relay ?? {})) {
372
+ names.expectInterface(`DHCP relay ${name} interface`, relay.interface);
373
+ }
374
+
375
+ for (const iface of device.flow_export?.interfaces ?? []) names.expectInterface("flow export interface", iface);
376
+
377
+ const lldp = device.lldp;
378
+ if (lldp && lldp !== true) for (const iface of lldp.interfaces) names.expectInterface("LLDP interface", iface);
379
+
380
+ const management = device.management;
381
+ for (const iface of management?.native?.interfaces ?? []) names.expectInterface("native management interface", iface);
382
+ for (const prefix of management?.allow ?? []) cidr("management allow", prefix);
383
+ if (management?.snmp && !management.snmp.community && !management.snmp.users)
384
+ report("SNMP is on with neither a community nor a user");
385
+ }
386
+
387
+ private static checkCertificates(device: Device, names: Names, report: Report): void {
388
+ const certificates = device.certificates ?? {};
389
+
390
+ for (const [name, certificate] of Object.entries(certificates)) {
391
+ try {
392
+ new X509Certificate(certificate.certificate);
393
+ } catch {
394
+ report(`certificate ${name} is not a PEM certificate`);
395
+ }
396
+ }
397
+
398
+ for (const service of ["https", "api_tls"] as const) {
399
+ const name = device.management?.[service]?.certificate;
400
+ names.expect(`${service} certificate`, name, name !== undefined && name in certificates);
401
+
402
+ if (name && certificates[name] && !certificates[name].private_key) {
403
+ report(`${service} certificate ${name} has no private key to serve with`);
404
+ }
405
+ }
406
+ }
407
+ }