@takodotid/circuit 0.24.0 → 0.26.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 (42) hide show
  1. package/README.md +3 -3
  2. package/dist/chunks/{index-n4zpd25n.js → index-22sbskdx.js} +22 -6
  3. package/dist/chunks/{server-ckdy25w2.js → server-1ftkqkx5.js} +1 -1
  4. package/dist/cli/index.js +4621 -324
  5. package/dist/presets/index.js +5 -0
  6. package/dist/types/adapters/devices/catalog.d.ts +28 -0
  7. package/dist/types/adapters/devices/fortios/firewall.d.ts +21 -0
  8. package/dist/types/adapters/devices/fortios/index.d.ts +55 -0
  9. package/dist/types/adapters/devices/fortios/models.d.ts +14 -0
  10. package/dist/types/adapters/devices/fortios/names.d.ts +32 -0
  11. package/dist/types/adapters/devices/fortios/plan.d.ts +26 -0
  12. package/dist/types/adapters/devices/fortios/render.d.ts +36 -0
  13. package/dist/types/adapters/devices/fortios/routing.d.ts +32 -0
  14. package/dist/types/adapters/devices/fortios/tree.d.ts +33 -0
  15. package/dist/types/adapters/devices/junos/bgp.d.ts +11 -0
  16. package/dist/types/adapters/devices/junos/index.d.ts +39 -0
  17. package/dist/types/adapters/devices/junos/models.d.ts +14 -0
  18. package/dist/types/adapters/devices/junos/render.d.ts +52 -0
  19. package/dist/types/core/graph.d.ts +2 -0
  20. package/dist/types/presets/index.d.ts +1 -1
  21. package/package.json +7 -2
  22. package/src/adapters/devices/catalog.ts +5 -2
  23. package/src/adapters/devices/fortios/TRAPS.md +40 -0
  24. package/src/adapters/devices/fortios/firewall.ts +352 -0
  25. package/src/adapters/devices/fortios/index.ts +800 -0
  26. package/src/adapters/devices/fortios/models.ts +7 -0
  27. package/src/adapters/devices/fortios/names.ts +78 -0
  28. package/src/adapters/devices/fortios/plan.ts +840 -0
  29. package/src/adapters/devices/fortios/render.ts +734 -0
  30. package/src/adapters/devices/fortios/routing.ts +444 -0
  31. package/src/adapters/devices/fortios/tree.ts +121 -0
  32. package/src/adapters/devices/index.ts +3 -1
  33. package/src/adapters/devices/junos/TRAPS.md +31 -0
  34. package/src/adapters/devices/junos/bgp.ts +263 -0
  35. package/src/adapters/devices/junos/index.ts +784 -0
  36. package/src/adapters/devices/junos/models.ts +7 -0
  37. package/src/adapters/devices/junos/render.ts +868 -0
  38. package/src/core/across.ts +6 -3
  39. package/src/core/graph.ts +3 -1
  40. package/src/core/validate.ts +4 -3
  41. package/src/presets/index.ts +7 -1
  42. package/templates/colocation/checks.ts +8 -1
@@ -169,7 +169,7 @@ function checkLinks(devices: readonly Device[]): Finding[] {
169
169
  return findings;
170
170
  }
171
171
 
172
- /** No address is held by two devices. Two loopbacks may hold one on purpose, for anycast, so that is a warning. */
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
173
  function checkAddresses(holders: Holder[]): Finding[] {
174
174
  const findings: Finding[] = [];
175
175
  const first = new Map<string, Holder>();
@@ -177,7 +177,7 @@ function checkAddresses(holders: Holder[]): Finding[] {
177
177
  const isLoopback = (holder: Holder) => holder.device.interfaces?.[holder.name]?.type === "loopback";
178
178
 
179
179
  for (const holder of holders) {
180
- const key = keyOf(host(holder.address))!;
180
+ const key = `${holder.vrf ?? ""} ${keyOf(host(holder.address))!}`;
181
181
  const earlier = first.get(key);
182
182
 
183
183
  if (!earlier) {
@@ -321,13 +321,16 @@ function checkTunnels(devices: readonly Device[], owners: Map<string, Holder>):
321
321
  findings.push({ level: "error", device: end.device.name, message: `interface ${end.name}: ${message}` });
322
322
  const kind = end.iface.type === "gre" ? "GRE" : "VXLAN";
323
323
 
324
- const mirror = ends.find(
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(
325
326
  (other) =>
326
327
  other.device === far &&
327
328
  other.iface.type === end.iface.type &&
328
329
  same(other.iface.local, end.iface.remote) &&
329
330
  same(other.iface.remote, end.iface.local)
330
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];
331
334
 
332
335
  if (!mirror) {
333
336
  report(`${end.iface.remote} is ${far.name}'s, which has no ${kind} tunnel from there back to ${end.iface.local}`);
package/src/core/graph.ts CHANGED
@@ -72,6 +72,8 @@ export type Holder = {
72
72
  /** The far end of a point-to-point address. */
73
73
  peer?: string;
74
74
  vrrp: NonNullable<Routed<string>["vrrp"]>;
75
+ /** The VRF the address is in. The default table when absent. */
76
+ vrf?: string;
75
77
  };
76
78
 
77
79
  export const deviceId = (name: string) => `device:${name}`;
@@ -124,7 +126,7 @@ export function holdersOf(devices: readonly Device[]): Holder[] {
124
126
  if (!isCidr(address)) return [];
125
127
 
126
128
  const peer = typeof entry === "string" ? undefined : entry.peer;
127
- return [{ device, name, address, peer, vrrp: settings.vrrp ?? [] }];
129
+ return [{ device, name, address, peer, vrrp: settings.vrrp ?? [], vrf: settings.vrf }];
128
130
  })
129
131
  )
130
132
  );
@@ -101,7 +101,7 @@ function checkVlans(device: Device, report: Report): void {
101
101
  }
102
102
  }
103
103
 
104
- /** Every address is valid, and no address sits on two interfaces. */
104
+ /** Every address is valid, and no address sits on two interfaces of one routing table. Two VRFs may hold the same address. */
105
105
  function checkAddresses(device: Device, report: Report, cidr: Checker, ip: Checker): void {
106
106
  const holderOf = new Map<string, string>();
107
107
  const holders = [
@@ -116,8 +116,9 @@ function checkAddresses(device: Device, report: Report, cidr: Checker, ip: Check
116
116
  if (typeof entry !== "string") ip(where, entry.peer);
117
117
 
118
118
  const bare = address.split("/")[0]!;
119
- if (holderOf.has(bare)) report(`${bare} is on both ${holderOf.get(bare)} and ${where}`);
120
- holderOf.set(bare, where);
119
+ const key = `${settings?.vrf ?? ""} ${bare}`;
120
+ if (holderOf.has(key)) report(`${bare} is on both ${holderOf.get(key)} and ${where}`);
121
+ holderOf.set(key, where);
121
122
  }
122
123
 
123
124
  for (const group of settings?.vrrp ?? []) {
@@ -496,7 +496,7 @@ export type InternetExchange<V extends string> = {
496
496
  * Member ports and a check for an internet exchange's fabric.
497
497
  *
498
498
  * - `ports(device)`: every member's port on one switch. A member port is in the peering VLAN and nothing else, takes no part in spanning tree or LLDP, and lets only a trickle of broadcast, multicast and unknown unicast through.
499
- * - `check`: members never share a name, an AS or an address; every address is inside the peering LAN and not reserved; every member port carries the peering VLAN alone.
499
+ * - `check`: members never share a name, an AS, an address or a port; every address is inside the peering LAN and not reserved; every member port carries the peering VLAN alone.
500
500
  */
501
501
  export function internetExchange<const V extends string>(exchange: InternetExchange<V>, members: readonly ExchangeMember[]) {
502
502
  const percent = exchange.storm_percent ?? 1;
@@ -524,6 +524,8 @@ export function internetExchange<const V extends string>(exchange: InternetExcha
524
524
  const findings: Finding[] = [];
525
525
  const error = (message: string, device?: string) => findings.push({ level: "error", ...(device ? { device } : {}), message });
526
526
  const taken = new Map<string, string>((exchange.reserved ?? []).map((address) => [address, "reserved"]));
527
+ // `ports()` writes one port per name, so a second member on a port would silently replace the first.
528
+ const portHolders = new Map<string, string>();
527
529
 
528
530
  members.forEach((member, index) => {
529
531
  for (const other of members.slice(index + 1)) {
@@ -544,6 +546,10 @@ export function internetExchange<const V extends string>(exchange: InternetExcha
544
546
  }
545
547
 
546
548
  for (const [deviceName, portName] of Object.entries(member.ports)) {
549
+ const holder = portHolders.get(`${deviceName} ${portName}`);
550
+ if (holder) error(`port ${portName} is given to both ${holder} and ${member.name}`, deviceName);
551
+ portHolders.set(`${deviceName} ${portName}`, member.name);
552
+
547
553
  const device = devices.find((candidate) => candidate.name === deviceName);
548
554
  const port = device?.ports?.[portName as keyof typeof device.ports];
549
555
  const carried = port ? vlansOf(port) : [];
@@ -12,7 +12,7 @@ export default [
12
12
  // Traffic from the transit and the exchange reaches the tenants only through the router, which filters it.
13
13
  trustBoundary({ untrusted: ["transit", "ix"], routers: ["edge-01"] }),
14
14
 
15
- // Tenants never share a name, a number or an address.
15
+ // Tenants never share a name, a number, an address or a port. A port given twice would go to the last tenant alone.
16
16
  () => {
17
17
  const findings: Finding[] = [];
18
18
  const error = (message: string) => findings.push({ level: "error", message });
@@ -28,6 +28,13 @@ export default [
28
28
  }
29
29
  }
30
30
 
31
+ for (const [device, ports] of Object.entries(t.ports)) {
32
+ for (const port of Object.keys(ports)) {
33
+ const sharer = TENANTS.slice(index + 1).find((other) => other.ports[device]?.[port]);
34
+ if (sharer) error(`port ${port} on ${device} is given to both ${t.name} and ${sharer.name}`);
35
+ }
36
+ }
37
+
31
38
  for (const { prefix, gateway } of t.blocks) {
32
39
  if (!contains(prefix, gateway)) error(`the gateway ${gateway} of ${t.name} is outside ${prefix}`);
33
40
  }