@takodotid/circuit 0.34.3 → 0.35.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 +13 -14
  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 +17 -11
  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
@@ -1,350 +0,0 @@
1
- // Rules that span devices: what one device declares about another has to agree with what the other declares.
2
-
3
- import type { Device, Gre, Port, Routed, Switched, Vxlan } from "../schema";
4
- import { contains, familyOf, host, isCidr, network as prefixOf } from "./addr";
5
- import type { Finding } from "./define";
6
- import { type Holder, holdersOf, keyOf } from "./graph";
7
-
8
- /** Every rule across devices. */
9
- export function checkAcross(devices: readonly Device[]): Finding[] {
10
- const holders = holdersOf(devices);
11
-
12
- // Who holds each address. The first holder wins; a second is reported by checkAddresses.
13
- const owners = new Map<string, Holder>();
14
- for (const holder of holders) {
15
- const key = keyOf(host(holder.address))!;
16
- if (!owners.has(key)) owners.set(key, holder);
17
- }
18
-
19
- return [
20
- ...checkLinks(devices),
21
- ...checkAddresses(holders),
22
- ...checkSessions(devices, owners),
23
- ...checkVrrp(holders),
24
- ...checkTunnels(devices, owners),
25
- ];
26
- }
27
-
28
- /** The VLAN ids a port or LAG carries, by how it carries them, so two ends can be compared across devices. */
29
- function carried(device: Device, settings: Switched<string>) {
30
- const id = (vlan: string | undefined) => (vlan ? device.vlans?.[vlan]?.id : undefined);
31
- return {
32
- access: id(settings.access_vlan),
33
- native: id(settings.native_vlan),
34
- trunk: (settings.trunk_vlans ?? []).map(id).sort((a, b) => (a ?? 0) - (b ?? 0)),
35
- };
36
- }
37
-
38
- /** How a port carries its VLANs, in words: "VLANs 10, 20 tagged and VLAN 30 untagged as native". */
39
- function describeCarried({ access, native, trunk }: ReturnType<typeof carried>): string {
40
- const parts: string[] = [];
41
- const tagged = trunk.filter((id) => id !== undefined);
42
-
43
- if (access !== undefined) parts.push(`VLAN ${access} untagged`);
44
- if (tagged.length) parts.push(`${tagged.length === 1 ? "VLAN" : "VLANs"} ${tagged.join(", ")} tagged`);
45
- if (native !== undefined) parts.push(`VLAN ${native} untagged as native`);
46
-
47
- return parts.length ? parts.join(" and ") : "no VLANs";
48
- }
49
-
50
- /** VLAN names by id, for the VLANs a port or LAG carries. */
51
- function vlanNames(device: Device, settings: Switched<string>): Map<number, string> {
52
- const names = [settings.access_vlan, settings.native_vlan, ...(settings.trunk_vlans ?? [])].filter((name) => name !== undefined);
53
-
54
- return new Map(names.flatMap((name) => (device.vlans?.[name] ? [[device.vlans[name].id, name] as const] : [])));
55
- }
56
-
57
- /** What a port's settings are kept on: the LAG when the port is a member of one, otherwise the port. */
58
- function carrierOf(device: Device, portName: string, port: Port<string, string, string, string>) {
59
- const lag = port.lag ? device.interfaces?.[port.lag] : undefined;
60
- if (port.lag && lag?.type === "lag") return { name: port.lag, settings: lag };
61
-
62
- return { name: portName, settings: port };
63
- }
64
-
65
- /** The plain addresses of one family on an interface, leaving out point-to-point ones. */
66
- const plainAddresses = (settings: Routed<string>, family: string) =>
67
- (settings.addresses ?? []).filter((entry): entry is string => typeof entry === "string" && isCidr(entry) && familyOf(entry) === family);
68
-
69
- /** Why the two ends of a cable cannot reach each other by address, or undefined when they can. */
70
- function addressDisagreement(here: Routed<string>, there: Routed<string>, far: Device, farName: string): string | undefined {
71
- for (const family of ["ipv4", "ipv6"]) {
72
- const ours = plainAddresses(here, family);
73
- const theirs = plainAddresses(there, family);
74
- if (!ours.length || !theirs.length) continue;
75
-
76
- // A prefix includes its length, so 10.0.0.0/24 and 10.0.0.0/25 are not one.
77
- const theirPrefixes = new Set(theirs.map(prefixOf));
78
- if (!ours.some((address) => theirPrefixes.has(prefixOf(address)))) {
79
- return `has ${ours.join(", ")}, and ${far.name} ${farName} has ${theirs.join(", ")}: not one prefix of one length`;
80
- }
81
- }
82
-
83
- // A point-to-point address names the far end's own address as its peer.
84
- const farHosts = new Set((there.addresses ?? []).map((entry) => keyOf(host(typeof entry === "string" ? entry : entry.address))));
85
-
86
- for (const entry of here.addresses ?? []) {
87
- if (typeof entry === "string" || !keyOf(entry.peer) || farHosts.has(keyOf(entry.peer))) continue;
88
- return `${entry.address} names peer ${entry.peer}, which ${far.name} ${farName} does not hold`;
89
- }
90
-
91
- return undefined;
92
- }
93
-
94
- /** A link names a device and port that exist, the far end links back, and both ends carry the same VLANs the same way, with the same MTU, speed and addresses. */
95
- function checkLinks(devices: readonly Device[]): Finding[] {
96
- const findings: Finding[] = [];
97
- const byName = new Map(devices.map((device) => [device.name, device]));
98
-
99
- for (const device of devices) {
100
- for (const [portName, port] of Object.entries(device.ports ?? {})) {
101
- const link = port?.link;
102
- if (!port || !link) continue;
103
-
104
- const report = (message: string, level: Finding["level"] = "error") =>
105
- findings.push({
106
- level,
107
- device: device.name,
108
- message: `port ${portName} link: ${message}`,
109
- });
110
-
111
- const far = byName.get(link.device);
112
- if (!far) {
113
- report(`device ${link.device} is not in the network`);
114
- continue;
115
- }
116
-
117
- const farPort = far.ports?.[link.port];
118
- if (!farPort) {
119
- report(`${link.device} does not declare port ${link.port}`);
120
- continue;
121
- }
122
-
123
- const linksBack = farPort.link?.device === device.name && farPort.link.port === portName;
124
- if (!linksBack) {
125
- report(`${link.device} ${link.port} does not link back to it`);
126
- continue;
127
- }
128
-
129
- const near = carrierOf(device, portName, port);
130
- const away = carrierOf(far, link.port, farPort);
131
-
132
- // A LAG's VLANs are on the LAG, so its member ports are compared by what their LAGs carry.
133
- const inLag = near.name !== portName;
134
- const farInLag = away.name !== link.port;
135
- if (inLag !== farInLag) {
136
- report(
137
- inLag
138
- ? `${portName} is in ${near.name}, and ${link.device} ${link.port} is in no LAG`
139
- : `${portName} is in no LAG, and ${link.device} ${link.port} is in ${away.name}`
140
- );
141
- }
142
-
143
- const here = carried(device, near.settings);
144
- const there = carried(far, away.settings);
145
- if (JSON.stringify(here) !== JSON.stringify(there)) {
146
- report(`${near.name} carries ${describeCarried(here)}, and ${link.device} ${away.name} carries ${describeCarried(there)}`);
147
- }
148
-
149
- if (port.speed && farPort.speed && port.speed !== farPort.speed) {
150
- report(`speed ${port.speed}, and ${link.device} ${link.port} is ${farPort.speed}`);
151
- }
152
-
153
- const mtu = near.settings.mtu ?? 1500;
154
- const farMtu = away.settings.mtu ?? 1500;
155
- if (mtu !== farMtu) report(`MTU ${mtu} on ${near.name}, and ${link.device} ${away.name} has ${farMtu}`);
156
-
157
- const disagreement = addressDisagreement(near.settings, away.settings, far, away.name);
158
- if (disagreement) report(`${near.name} ${disagreement}`);
159
-
160
- // One VLAN under two names works, but reads as two VLANs.
161
- const farNames = vlanNames(far, away.settings);
162
- for (const [id, name] of vlanNames(device, near.settings)) {
163
- const farName = farNames.get(id);
164
- if (farName && farName !== name) report(`VLAN ${id} is ${name} here and ${farName} on ${link.device}`, "warning");
165
- }
166
- }
167
- }
168
-
169
- return findings;
170
- }
171
-
172
- /** No address is held by two devices in one VRF. Two loopbacks may hold one on purpose, for anycast, so that is a warning. */
173
- function checkAddresses(holders: Holder[]): Finding[] {
174
- const findings: Finding[] = [];
175
- const first = new Map<string, Holder>();
176
-
177
- const isLoopback = (holder: Holder) => holder.device.interfaces?.[holder.name]?.type === "loopback";
178
-
179
- for (const holder of holders) {
180
- const key = `${holder.vrf ?? ""} ${keyOf(host(holder.address))!}`;
181
- const earlier = first.get(key);
182
-
183
- if (!earlier) {
184
- first.set(key, holder);
185
- continue;
186
- }
187
-
188
- // Two on one device are reported by the device's own checks.
189
- if (earlier.device === holder.device) continue;
190
-
191
- const anycast = isLoopback(earlier) && isLoopback(holder);
192
-
193
- findings.push({
194
- level: anycast ? "warning" : "error",
195
- device: holder.device.name,
196
- message: `${host(holder.address)} on ${holder.name} is also on ${earlier.device.name} ${earlier.name}${anycast ? ", which is right only for anycast" : ""}`,
197
- });
198
- }
199
-
200
- return findings;
201
- }
202
-
203
- /** A BGP session to an address another of our devices holds needs that device's session back, in the AS it expects, from the address it names. */
204
- function checkSessions(devices: readonly Device[], owners: Map<string, Holder>): Finding[] {
205
- const findings: Finding[] = [];
206
-
207
- const sessionsOf = (device: Device) => {
208
- const bgp = device.routing?.bgp;
209
-
210
- return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
211
- const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
212
-
213
- return {
214
- name,
215
- address: neighbor.address,
216
- remoteAs: neighbor.remote_as ?? group?.remote_as,
217
- localAddress: neighbor.local_address ?? group?.local_address,
218
- };
219
- });
220
- };
221
-
222
- for (const device of devices) {
223
- for (const session of sessionsOf(device)) {
224
- const far = owners.get(keyOf(session.address) ?? "")?.device;
225
- if (!far || far === device) continue;
226
-
227
- const report = (message: string) =>
228
- findings.push({ level: "error", device: device.name, message: `neighbor ${session.name}: ${message}` });
229
-
230
- const farBgp = far.routing?.bgp;
231
- if (!farBgp) {
232
- report(`${session.address} is ${far.name}'s, which runs no BGP`);
233
- continue;
234
- }
235
-
236
- if (session.remoteAs !== undefined && session.remoteAs !== farBgp.asn) {
237
- report(`expects AS${session.remoteAs}, and ${far.name} is AS${farBgp.asn}`);
238
- }
239
-
240
- // Two routers often hold one session per family, and a session of the other family does not answer this one.
241
- const back = sessionsOf(far).find(
242
- (other) =>
243
- owners.get(keyOf(other.address) ?? "")?.device === device && familyOf(other.address) === familyOf(session.address)
244
- );
245
- if (!back) {
246
- report(`${far.name} has no session back to ${device.name}`);
247
- continue;
248
- }
249
-
250
- // The far end only accepts the session from the address it names.
251
- if (session.localAddress && keyOf(session.localAddress) !== keyOf(back.address)) {
252
- report(`starts from ${session.localAddress}, and ${far.name}'s session ${back.name} expects ${back.address}`);
253
- }
254
- }
255
- }
256
-
257
- return findings;
258
- }
259
-
260
- /** On one segment, a VRRP id has one address and an address has one id, on every device. */
261
- function checkVrrp(holders: Holder[]): Finding[] {
262
- const findings: Finding[] = [];
263
- const bySegment = new Map<string, { holder: Holder; id: number; address: string }[]>();
264
-
265
- for (const holder of holders) {
266
- const segment = prefixOf(holder.address);
267
-
268
- for (const group of holder.vrrp) {
269
- if (!keyOf(group.address) || familyOf(group.address) !== familyOf(holder.address)) continue;
270
- if (!contains(segment, group.address)) continue;
271
-
272
- bySegment.set(segment, [...(bySegment.get(segment) ?? []), { holder, id: group.id, address: group.address }]);
273
- }
274
- }
275
-
276
- for (const members of bySegment.values()) {
277
- for (const member of members) {
278
- const where = `${member.holder.device.name} ${member.holder.name}`;
279
- const others = members.filter((other) => other.holder.device !== member.holder.device);
280
-
281
- const sameId = others.find((other) => other.id === member.id && keyOf(other.address) !== keyOf(member.address));
282
- if (sameId) {
283
- findings.push({
284
- level: "error",
285
- device: member.holder.device.name,
286
- message: `VRRP ${member.id} on ${member.holder.name} is ${member.address}, and on ${sameId.holder.device.name} ${sameId.holder.name} it is ${sameId.address}`,
287
- });
288
- }
289
-
290
- const sameAddress = others.find((other) => keyOf(other.address) === keyOf(member.address) && other.id !== member.id);
291
- if (sameAddress) {
292
- findings.push({
293
- level: "error",
294
- device: member.holder.device.name,
295
- message: `VRRP address ${member.address} is id ${member.id} on ${where}, and id ${sameAddress.id} on ${sameAddress.holder.device.name} ${sameAddress.holder.name}`,
296
- });
297
- }
298
- }
299
- }
300
-
301
- return findings;
302
- }
303
-
304
- /** A GRE or VXLAN tunnel to another of our devices has a tunnel back there, with the endpoints mirrored and, for VXLAN, the same VNI and port. */
305
- function checkTunnels(devices: readonly Device[], owners: Map<string, Holder>): Finding[] {
306
- const findings: Finding[] = [];
307
-
308
- const ends = devices.flatMap((device) =>
309
- Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) =>
310
- iface.type === "gre" || iface.type === "vxlan" ? [{ device, name, iface: iface as Gre<string> | Vxlan<string> }] : []
311
- )
312
- );
313
-
314
- const same = (a: string, b: string) => keyOf(a) !== undefined && keyOf(a) === keyOf(b);
315
-
316
- for (const end of ends) {
317
- const far = owners.get(keyOf(end.iface.remote) ?? "")?.device;
318
- if (!far || far === end.device) continue;
319
-
320
- const report = (message: string) =>
321
- findings.push({ level: "error", device: end.device.name, message: `interface ${end.name}: ${message}` });
322
- const kind = end.iface.type === "gre" ? "GRE" : "VXLAN";
323
-
324
- // Two routers may run several VXLAN tunnels between them, one for each VNI, so a tunnel pairs with the one of its own VNI first.
325
- const back = ends.filter(
326
- (other) =>
327
- other.device === far &&
328
- other.iface.type === end.iface.type &&
329
- same(other.iface.local, end.iface.remote) &&
330
- same(other.iface.remote, end.iface.local)
331
- );
332
- const vniOf = (iface: Gre<string> | Vxlan<string>) => (iface.type === "vxlan" ? iface.vni : undefined);
333
- const mirror = back.find((other) => vniOf(other.iface) === vniOf(end.iface)) ?? back[0];
334
-
335
- if (!mirror) {
336
- report(`${end.iface.remote} is ${far.name}'s, which has no ${kind} tunnel from there back to ${end.iface.local}`);
337
- continue;
338
- }
339
-
340
- if (end.iface.type !== "vxlan" || mirror.iface.type !== "vxlan") continue;
341
-
342
- if (end.iface.vni !== mirror.iface.vni) report(`VNI ${end.iface.vni}, and ${far.name} ${mirror.name} has ${mirror.iface.vni}`);
343
-
344
- const port = end.iface.port ?? 4789;
345
- const farPort = mirror.iface.port ?? 4789;
346
- if (port !== farPort) report(`UDP port ${port}, and ${far.name} ${mirror.name} has ${farPort}`);
347
- }
348
-
349
- return findings;
350
- }
package/src/core/addr.ts DELETED
@@ -1,174 +0,0 @@
1
- // Address arithmetic for both families. Addresses are parsed to bigint so v4 and v6 share one code path.
2
-
3
- import type { Family } from "../schema";
4
-
5
- export const familyOf = (address: string): Family => (address.includes(":") ? "ipv6" : "ipv4");
6
-
7
- const bitsOf = (family: Family) => (family === "ipv4" ? 32 : 128);
8
-
9
- /** An address as a number, or null when it is not one. */
10
- export function parse(address: string): bigint | null {
11
- if (!address.includes(":")) {
12
- const octets = address.split(".");
13
- const valid = octets.length === 4 && octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255);
14
- if (!valid) return null;
15
-
16
- return octets.reduce((value, octet) => (value << 8n) | BigInt(octet), 0n);
17
- }
18
-
19
- const [before, after, extra] = address.split("::");
20
- if (extra !== undefined) return null;
21
-
22
- const groupsOf = (text: string | undefined) => (text ? text.split(":") : []);
23
- const leading = groupsOf(before);
24
- const trailing = groupsOf(after);
25
-
26
- // `::` stands for as many zero groups as make eight.
27
- const zeros = after === undefined ? 0 : 8 - leading.length - trailing.length;
28
- const groups = [...leading, ...Array(zeros).fill("0"), ...trailing];
29
-
30
- const valid = groups.length === 8 && groups.every((group) => /^[0-9a-fA-F]{1,4}$/.test(group));
31
- if (!valid) return null;
32
-
33
- return groups.reduce((value, group) => (value << 16n) | BigInt(parseInt(group, 16)), 0n);
34
- }
35
-
36
- /** `10.0.0.1/24` to its parts. Throws when it is not an address with a length. */
37
- export function split(cidr: string): { addr: string; len: number; family: Family } {
38
- const [address, lengthText] = cidr.split("/");
39
- const family = familyOf(address!);
40
- const length = Number(lengthText);
41
-
42
- const valid =
43
- parse(address!) !== null && lengthText !== undefined && Number.isInteger(length) && length >= 0 && length <= bitsOf(family);
44
- if (!valid) throw new Error(`'${cidr}' is not an address with a length`);
45
-
46
- return { addr: address!, len: length, family };
47
- }
48
-
49
- export const isAddress = (text: string) => parse(text) !== null;
50
-
51
- export function isCidr(text: string): boolean {
52
- try {
53
- split(text);
54
- return true;
55
- } catch {
56
- return false;
57
- }
58
- }
59
-
60
- const mask = (length: number, family: Family) => {
61
- const all = (1n << BigInt(bitsOf(family))) - 1n;
62
- const hostBits = (1n << BigInt(bitsOf(family) - length)) - 1n;
63
- return all ^ hostBits;
64
- };
65
-
66
- function format(value: bigint, family: Family): string {
67
- if (family === "ipv4") {
68
- return [24n, 16n, 8n, 0n].map((shift) => String((value >> shift) & 255n)).join(".");
69
- }
70
-
71
- const groups = Array.from({ length: 8 }, (_, index) => ((value >> BigInt(112 - index * 16)) & 0xffffn).toString(16));
72
-
73
- // Compress the longest run of zero groups, as RFC 5952 asks.
74
- let longestStart = -1;
75
- let longestLength = 0;
76
-
77
- for (let start = 0; start < 8;) {
78
- let end = start;
79
- while (end < 8 && groups[end] === "0") end++;
80
-
81
- const runLength = end - start;
82
- if (runLength > longestLength && runLength > 1) {
83
- longestStart = start;
84
- longestLength = runLength;
85
- }
86
-
87
- start = end === start ? start + 1 : end;
88
- }
89
-
90
- if (longestStart < 0) return groups.join(":");
91
-
92
- const head = groups.slice(0, longestStart).join(":");
93
- const tail = groups.slice(longestStart + longestLength).join(":");
94
- return `${head}::${tail}`;
95
- }
96
-
97
- /** `10.0.0.1/24` to `10.0.0.0/24`. */
98
- export function network(cidr: string): string {
99
- const { addr, len, family } = split(cidr);
100
- return `${format(parse(addr)! & mask(len, family), family)}/${len}`;
101
- }
102
-
103
- /** Is the address or prefix `inner` inside `outer`? */
104
- export function contains(outer: string, inner: string): boolean {
105
- const outerParts = split(outer);
106
- const [innerAddress, innerLengthText] = inner.split("/");
107
- if (familyOf(innerAddress!) !== outerParts.family) return false;
108
-
109
- const innerLength = innerLengthText === undefined ? bitsOf(outerParts.family) : Number(innerLengthText);
110
- if (innerLength < outerParts.len) return false;
111
-
112
- const outerMask = mask(outerParts.len, outerParts.family);
113
- return (parse(innerAddress!)! & outerMask) === (parse(outerParts.addr)! & outerMask);
114
- }
115
-
116
- /** A number back to the address it stands for. */
117
- export const formatAddress = (value: bigint, family: Family) => format(value, family);
118
-
119
- /** The first and last address of a prefix, as numbers. */
120
- export function rangeOf(cidr: string): { first: bigint; last: bigint; family: Family } {
121
- const { addr, len, family } = split(cidr);
122
- const hostBits = mask(len, family) ^ ((1n << BigInt(bitsOf(family))) - 1n);
123
- const first = parse(addr)! & mask(len, family);
124
-
125
- return { first, last: first | hostBits, family };
126
- }
127
-
128
- /** The fewest prefixes that cover every address from `first` to `last` and nothing else. */
129
- export function prefixesBetween(first: bigint, last: bigint, family: Family): string[] {
130
- const bits = bitsOf(family);
131
- const prefixes: string[] = [];
132
-
133
- for (let start = first; start <= last;) {
134
- // The largest block that starts here on its own boundary and does not pass `last`.
135
- let size = 0;
136
- while (size < bits && start % (1n << BigInt(size + 1)) === 0n && start + (1n << BigInt(size + 1)) - 1n <= last) size++;
137
-
138
- prefixes.push(`${format(start, family)}/${bits - size}`);
139
- start += 1n << BigInt(size);
140
- }
141
-
142
- return prefixes;
143
- }
144
-
145
- /** `/25` to `255.255.255.128`. */
146
- export const netmask = (length: number) => format(mask(length, "ipv4"), "ipv4");
147
-
148
- /** `/24` to `0.0.0.255`, the inverted mask some ACLs take. */
149
- export const wildcard = (length: number) => format(mask(length, "ipv4") ^ 0xffffffffn, "ipv4");
150
-
151
- /** The address with its length dropped. */
152
- export const host = (cidr: string) => cidr.split("/")[0]!;
153
-
154
- /** IPv4 space another network cannot reach: this host, private, shared, loopback, link-local and documentation, after RFC 6890. */
155
- const NOT_GLOBAL_V4 = [
156
- "0.0.0.0/8",
157
- "10.0.0.0/8",
158
- "100.64.0.0/10",
159
- "127.0.0.0/8",
160
- "169.254.0.0/16",
161
- "172.16.0.0/12",
162
- "192.0.2.0/24",
163
- "192.168.0.0/16",
164
- "198.18.0.0/15",
165
- "198.51.100.0/24",
166
- "203.0.113.0/24",
167
- ];
168
-
169
- /** Whether another network can reach the address. IPv6 is global only inside 2000::/3, less documentation space. */
170
- export function isGlobal(address: string): boolean {
171
- if (familyOf(address) === "ipv6") return contains("2000::/3", address) && !contains("2001:db8::/32", address);
172
-
173
- return !NOT_GLOBAL_V4.some((prefix) => contains(prefix, address));
174
- }