@takodotid/circuit 0.23.0 → 0.25.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.
- package/README.md +12 -3
- package/dist/chunks/{index-n4zpd25n.js → index-wm9ypkjq.js} +14 -6
- package/dist/chunks/{server-ckdy25w2.js → server-n2q4tzy4.js} +1 -1
- package/dist/cli/index.js +1766 -172
- package/dist/presets/index.js +5 -0
- package/dist/types/adapters/devices/catalog.d.ts +14 -0
- package/dist/types/adapters/devices/junos/bgp.d.ts +11 -0
- package/dist/types/adapters/devices/junos/index.d.ts +39 -0
- package/dist/types/adapters/devices/junos/models.d.ts +14 -0
- package/dist/types/adapters/devices/junos/render.d.ts +52 -0
- package/dist/types/core/graph.d.ts +2 -0
- package/dist/types/presets/index.d.ts +1 -1
- package/package.json +4 -2
- package/src/adapters/devices/catalog.ts +4 -2
- package/src/adapters/devices/index.ts +2 -1
- package/src/adapters/devices/junos/TRAPS.md +31 -0
- package/src/adapters/devices/junos/bgp.ts +263 -0
- package/src/adapters/devices/junos/index.ts +784 -0
- package/src/adapters/devices/junos/models.ts +7 -0
- package/src/adapters/devices/junos/render.ts +868 -0
- package/src/adapters/devices/vrp/index.ts +5 -0
- package/src/core/across.ts +42 -14
- package/src/core/graph.ts +3 -1
- package/src/core/validate.ts +4 -3
- package/src/presets/index.ts +7 -1
- package/templates/colocation/checks.ts +8 -1
- package/templates/single-site/router.ts +4 -7
|
@@ -609,6 +609,7 @@ export function unsupported(device: Device): string[] {
|
|
|
609
609
|
if (iface.type !== "vlan" && iface.type !== "lag") found.push(`interfaces.${name}, a ${iface.type}`);
|
|
610
610
|
if (iface.type === "lag" && iface.addresses?.length) found.push(`interfaces.${name}.addresses, LAGs are switched`);
|
|
611
611
|
if (iface.mtu !== undefined) found.push(`interfaces.${name}.mtu`);
|
|
612
|
+
if (iface.ipv6_ra) found.push(`interfaces.${name}.ipv6_ra`);
|
|
612
613
|
for (const group of iface.vrrp ?? []) if (familyOf(group.address) === "ipv6") found.push(`interfaces.${name}.vrrp for IPv6`);
|
|
613
614
|
}
|
|
614
615
|
|
|
@@ -621,6 +622,10 @@ export function unsupported(device: Device): string[] {
|
|
|
621
622
|
if (routing?.rpki) found.push("routing.rpki");
|
|
622
623
|
if (routing?.ospf?.families?.includes("ipv6")) found.push("routing.ospf for IPv6");
|
|
623
624
|
|
|
625
|
+
for (const [index, route] of (routing?.static ?? []).entries()) {
|
|
626
|
+
if (route.description && familyOf(split(route.prefix).addr) === "ipv6") found.push(`routing.static[${index}].description for IPv6`);
|
|
627
|
+
}
|
|
628
|
+
|
|
624
629
|
for (const field of ["firewall", "dhcp", "acls", "policies", "prefix_sets", "hardware_offload", "certificates"] as const) {
|
|
625
630
|
if (device[field] !== undefined) found.push(field);
|
|
626
631
|
}
|
package/src/core/across.ts
CHANGED
|
@@ -25,16 +25,28 @@ export function checkAcross(devices: readonly Device[]): Finding[] {
|
|
|
25
25
|
];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/** The VLAN ids a port carries, by how it carries them, so two ends can be compared across devices. */
|
|
29
|
-
function carried(device: Device,
|
|
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
30
|
const id = (vlan: string | undefined) => (vlan ? device.vlans?.[vlan]?.id : undefined);
|
|
31
31
|
return {
|
|
32
|
-
access: id(
|
|
33
|
-
native: id(
|
|
34
|
-
trunk: (
|
|
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
35
|
};
|
|
36
36
|
}
|
|
37
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
|
+
|
|
38
50
|
/** VLAN names by id, for the VLANs a port or LAG carries. */
|
|
39
51
|
function vlanNames(device: Device, settings: Switched<string>): Map<number, string> {
|
|
40
52
|
const names = [settings.access_vlan, settings.native_vlan, ...(settings.trunk_vlans ?? [])].filter((name) => name !== undefined);
|
|
@@ -114,17 +126,30 @@ function checkLinks(devices: readonly Device[]): Finding[] {
|
|
|
114
126
|
continue;
|
|
115
127
|
}
|
|
116
128
|
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
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
|
+
}
|
|
120
148
|
|
|
121
149
|
if (port.speed && farPort.speed && port.speed !== farPort.speed) {
|
|
122
150
|
report(`speed ${port.speed}, and ${link.device} ${link.port} is ${farPort.speed}`);
|
|
123
151
|
}
|
|
124
152
|
|
|
125
|
-
const near = carrierOf(device, portName, port);
|
|
126
|
-
const away = carrierOf(far, link.port, farPort);
|
|
127
|
-
|
|
128
153
|
const mtu = near.settings.mtu ?? 1500;
|
|
129
154
|
const farMtu = away.settings.mtu ?? 1500;
|
|
130
155
|
if (mtu !== farMtu) report(`MTU ${mtu} on ${near.name}, and ${link.device} ${away.name} has ${farMtu}`);
|
|
@@ -144,7 +169,7 @@ function checkLinks(devices: readonly Device[]): Finding[] {
|
|
|
144
169
|
return findings;
|
|
145
170
|
}
|
|
146
171
|
|
|
147
|
-
/** 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. */
|
|
148
173
|
function checkAddresses(holders: Holder[]): Finding[] {
|
|
149
174
|
const findings: Finding[] = [];
|
|
150
175
|
const first = new Map<string, Holder>();
|
|
@@ -152,7 +177,7 @@ function checkAddresses(holders: Holder[]): Finding[] {
|
|
|
152
177
|
const isLoopback = (holder: Holder) => holder.device.interfaces?.[holder.name]?.type === "loopback";
|
|
153
178
|
|
|
154
179
|
for (const holder of holders) {
|
|
155
|
-
const key = keyOf(host(holder.address))
|
|
180
|
+
const key = `${holder.vrf ?? ""} ${keyOf(host(holder.address))!}`;
|
|
156
181
|
const earlier = first.get(key);
|
|
157
182
|
|
|
158
183
|
if (!earlier) {
|
|
@@ -296,13 +321,16 @@ function checkTunnels(devices: readonly Device[], owners: Map<string, Holder>):
|
|
|
296
321
|
findings.push({ level: "error", device: end.device.name, message: `interface ${end.name}: ${message}` });
|
|
297
322
|
const kind = end.iface.type === "gre" ? "GRE" : "VXLAN";
|
|
298
323
|
|
|
299
|
-
|
|
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(
|
|
300
326
|
(other) =>
|
|
301
327
|
other.device === far &&
|
|
302
328
|
other.iface.type === end.iface.type &&
|
|
303
329
|
same(other.iface.local, end.iface.remote) &&
|
|
304
330
|
same(other.iface.remote, end.iface.local)
|
|
305
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];
|
|
306
334
|
|
|
307
335
|
if (!mirror) {
|
|
308
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
|
);
|
package/src/core/validate.ts
CHANGED
|
@@ -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
|
-
|
|
120
|
-
holderOf.
|
|
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 ?? []) {
|
package/src/presets/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|
|
@@ -69,16 +69,13 @@ export default defineDevice({
|
|
|
69
69
|
{ description: "DNS from guests", match: { in_interface: "guests", protocol: "udp", dst_port: 53 }, action: "accept" },
|
|
70
70
|
],
|
|
71
71
|
},
|
|
72
|
-
// Traffic passing through the router.
|
|
72
|
+
// Traffic passing through the router: home and guests reach the internet, and nothing else gets through.
|
|
73
73
|
forward: {
|
|
74
|
+
default: "drop",
|
|
74
75
|
rules: [
|
|
75
76
|
{ description: "Replies", match: { state: ["established", "related"] }, action: "accept", offload: true },
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
match: { in_interface: "guests", out_interface: "home" },
|
|
79
|
-
action: "drop",
|
|
80
|
-
},
|
|
81
|
-
{ description: "Guests stay off management", match: { in_interface: "guests", out_interface: "mgmt" }, action: "drop" },
|
|
77
|
+
{ description: "Home to the internet", match: { in_interface: "home", out_interface: "1g-1" }, action: "accept" },
|
|
78
|
+
{ description: "Guests to the internet", match: { in_interface: "guests", out_interface: "1g-1" }, action: "accept" },
|
|
82
79
|
],
|
|
83
80
|
},
|
|
84
81
|
},
|