@takodotid/circuit 0.43.1 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -37
- package/package.json +21 -58
- package/src/checks/across.ts +402 -0
- package/src/checks/built-in.ts +256 -0
- package/src/checks/device.ts +436 -0
- package/src/checks/index.ts +53 -0
- package/src/cli/advisories.ts +108 -0
- package/src/cli/app.ts +71 -0
- package/src/cli/editor-types.generated.ts +30 -0
- package/src/cli/editor-types.ts +69 -0
- package/src/cli/format.ts +124 -0
- package/src/cli/git-hooks.ts +79 -0
- package/src/cli/index.ts +350 -1170
- package/src/cli/migrate.ts +554 -0
- package/src/cli/project-commands.ts +656 -0
- package/src/cli/releases.ts +374 -0
- package/src/cli/remember.ts +339 -0
- package/src/cli/rename.ts +27 -33
- package/src/cli/secrets-password.ts +60 -0
- package/src/cli/secrets.ts +258 -0
- package/src/cli/setup.ts +300 -0
- package/src/cli/signing.ts +23 -0
- package/src/cli/text-editor.ts +268 -0
- package/src/cli/ui.ts +298 -0
- package/src/cli/version.ts +2 -0
- package/src/cli/versions.ts +158 -0
- package/src/config/address.ts +102 -0
- package/src/config/circuit.d.ts +30 -0
- package/src/config/common.ts +65 -0
- package/src/config/communities.ts +479 -0
- package/src/config/define.ts +684 -0
- package/src/config/device.ts +164 -0
- package/src/config/firewall.ts +150 -0
- package/src/config/index.ts +22 -0
- package/src/{schema → config}/interfaces.ts +121 -85
- package/src/config/keys.ts +434 -0
- package/src/config/management.ts +61 -0
- package/src/config/merge.ts +335 -0
- package/src/config/models.ts +70 -0
- package/src/config/monitoring.ts +64 -0
- package/src/config/network.ts +180 -0
- package/src/config/presets.ts +149 -0
- package/src/config/routing.ts +401 -0
- package/src/config/services.ts +51 -0
- package/src/config/switching.ts +84 -0
- package/src/config/system.ts +103 -0
- package/src/{schema → config}/time-zones.ts +1 -1
- package/src/core/address.ts +1 -1
- package/src/core/cabling.ts +8 -43
- package/src/core/graph.ts +40 -0
- package/src/{integrations → core}/registries.ts +5 -3
- package/src/core/secrets-file.ts +539 -0
- package/src/core/secrets.ts +83 -64
- package/src/core/topology.ts +103 -146
- package/src/core/utils.ts +12 -1
- package/src/model.ts +83 -0
- package/src/platforms/derive.ts +64 -37
- package/src/platforms/finding.ts +9 -0
- package/src/platforms/fortios/firewall.ts +76 -64
- package/src/platforms/fortios/index.ts +189 -125
- package/src/platforms/fortios/names.ts +51 -18
- package/src/platforms/fortios/render.ts +69 -62
- package/src/platforms/fortios/routing.ts +89 -58
- package/src/platforms/fortios/vdoms.ts +47 -16
- package/src/platforms/junos/TRAPS.md +2 -2
- package/src/platforms/junos/bgp.ts +63 -42
- package/src/platforms/junos/index.ts +122 -89
- package/src/platforms/junos/render.ts +139 -104
- package/src/platforms/platform.ts +10 -8
- package/src/platforms/raisecom-ros/TRAPS.md +1 -1
- package/src/platforms/raisecom-ros/index.ts +89 -70
- package/src/platforms/raisecom-ros/render.ts +49 -45
- package/src/platforms/routeros/TRAPS.md +3 -3
- package/src/platforms/routeros/index.ts +121 -60
- package/src/platforms/routeros/render.ts +214 -195
- package/src/platforms/vrp/TRAPS.md +1 -1
- package/src/platforms/vrp/index.ts +74 -50
- package/src/platforms/vrp/render.ts +67 -56
- package/src/project/project.ts +725 -0
- package/src/studio/app/env.d.ts +9 -0
- package/src/studio/app/index.html +18 -0
- package/src/studio/app/src/App.vue +95 -0
- package/src/studio/app/src/components/AddressesView.vue +38 -0
- package/src/studio/app/src/components/CablesView.vue +29 -0
- package/src/studio/app/src/components/CompareView.vue +160 -0
- package/src/studio/app/src/components/DataTable.vue +49 -0
- package/src/studio/app/src/components/DeviceView.vue +237 -0
- package/src/studio/app/src/components/DevicesView.vue +32 -0
- package/src/studio/app/src/components/Faceplate.vue +75 -0
- package/src/studio/app/src/components/GraphView.vue +282 -0
- package/src/studio/app/src/components/LineDiff.vue +24 -0
- package/src/studio/app/src/components/ProblemsView.vue +42 -0
- package/src/studio/app/src/components/SearchView.vue +72 -0
- package/src/studio/app/src/components/SecretsView.vue +112 -0
- package/src/studio/app/src/components/SessionsView.vue +35 -0
- package/src/studio/app/src/components/Settings.vue +95 -0
- package/src/studio/app/src/components/ToolsView.vue +118 -0
- package/src/studio/app/src/components/VlansView.vue +50 -0
- package/src/studio/app/src/lib/csv.ts +19 -0
- package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
- package/src/studio/app/src/lib/format.ts +38 -0
- package/src/studio/app/src/lib/net.ts +68 -0
- package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
- package/src/studio/app/src/lib/store.ts +71 -0
- package/src/studio/app/src/lib/tools.ts +217 -0
- package/src/studio/app/src/main.ts +9 -0
- package/src/studio/{public → app/src}/style.css +14 -4
- package/src/studio/app/tsconfig.json +17 -0
- package/src/studio/app/vite.config.ts +16 -0
- package/src/studio/assets.generated.ts +3 -0
- package/src/studio/data.ts +96 -188
- package/src/studio/git.ts +27 -43
- package/src/studio/server.ts +196 -107
- package/src/studio/types.ts +141 -0
- package/src/transport/console.ts +3 -3
- package/src/transport/ssh.ts +1 -1
- package/dist/chunks/index-3b343xva.js +0 -177
- package/dist/chunks/index-6wb41q67.js +0 -62
- package/dist/chunks/index-n7w3tg5y.js +0 -149
- package/dist/chunks/index-yzw86rc8.js +0 -921
- package/dist/chunks/new-w3zys5y4.js +0 -172
- package/dist/chunks/server-cxr9ynxc.js +0 -259
- package/dist/cli/index.js +0 -12922
- package/dist/helpers/index.js +0 -881
- package/dist/index.js +0 -28
- package/dist/types/core/address-space.d.ts +0 -62
- package/dist/types/core/address.d.ts +0 -44
- package/dist/types/core/cabling.d.ts +0 -41
- package/dist/types/core/cross-checks.d.ts +0 -28
- package/dist/types/core/define.d.ts +0 -79
- package/dist/types/core/project-files.d.ts +0 -12
- package/dist/types/core/schema-docs.d.ts +0 -25
- package/dist/types/core/secrets.d.ts +0 -62
- package/dist/types/core/topology.d.ts +0 -96
- package/dist/types/core/utils.d.ts +0 -10
- package/dist/types/core/validator.d.ts +0 -23
- package/dist/types/helpers/bgp.d.ts +0 -195
- package/dist/types/helpers/cables.d.ts +0 -69
- package/dist/types/helpers/checks.d.ts +0 -36
- package/dist/types/helpers/exchange.d.ts +0 -55
- package/dist/types/helpers/filters.d.ts +0 -51
- package/dist/types/helpers/index.d.ts +0 -14
- package/dist/types/helpers/route-server.d.ts +0 -52
- package/dist/types/helpers/upstreams.d.ts +0 -192
- package/dist/types/index.d.ts +0 -10
- package/dist/types/integrations/librenms.d.ts +0 -60
- package/dist/types/integrations/netbox.d.ts +0 -89
- package/dist/types/integrations/peeringdb.d.ts +0 -24
- package/dist/types/integrations/prometheus.d.ts +0 -24
- package/dist/types/integrations/registries.d.ts +0 -8
- package/dist/types/platforms/bgp-wait.d.ts +0 -24
- package/dist/types/platforms/catalog.d.ts +0 -109
- package/dist/types/platforms/derive.d.ts +0 -17
- package/dist/types/platforms/fortios/firewall.d.ts +0 -47
- package/dist/types/platforms/fortios/index.d.ts +0 -140
- package/dist/types/platforms/fortios/models.d.ts +0 -14
- package/dist/types/platforms/fortios/names.d.ts +0 -52
- package/dist/types/platforms/fortios/plan.d.ts +0 -114
- package/dist/types/platforms/fortios/render.d.ts +0 -77
- package/dist/types/platforms/fortios/routing.d.ts +0 -56
- package/dist/types/platforms/fortios/tree.d.ts +0 -37
- package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
- package/dist/types/platforms/index.d.ts +0 -13
- package/dist/types/platforms/junos/bgp.d.ts +0 -44
- package/dist/types/platforms/junos/index.d.ts +0 -114
- package/dist/types/platforms/junos/models.d.ts +0 -14
- package/dist/types/platforms/junos/render.d.ts +0 -154
- package/dist/types/platforms/line-diff.d.ts +0 -67
- package/dist/types/platforms/platform.d.ts +0 -135
- package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
- package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
- package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
- package/dist/types/platforms/routeros/export.d.ts +0 -59
- package/dist/types/platforms/routeros/index.d.ts +0 -116
- package/dist/types/platforms/routeros/models.d.ts +0 -18
- package/dist/types/platforms/routeros/plan.d.ts +0 -58
- package/dist/types/platforms/routeros/render.d.ts +0 -250
- package/dist/types/platforms/vrp/index.d.ts +0 -83
- package/dist/types/platforms/vrp/models.d.ts +0 -12
- package/dist/types/platforms/vrp/render.d.ts +0 -96
- package/dist/types/schema/common.d.ts +0 -30
- package/dist/types/schema/device.d.ts +0 -126
- package/dist/types/schema/firewall.d.ts +0 -131
- package/dist/types/schema/index.d.ts +0 -8
- package/dist/types/schema/interfaces.d.ts +0 -205
- package/dist/types/schema/policy.d.ts +0 -107
- package/dist/types/schema/routing.d.ts +0 -161
- package/dist/types/schema/system.d.ts +0 -198
- package/dist/types/schema/time-zones.d.ts +0 -2
- package/dist/types/transport/console.d.ts +0 -115
- package/dist/types/transport/path-check.d.ts +0 -32
- package/dist/types/transport/ssh.d.ts +0 -27
- package/src/cli/graph-format.ts +0 -91
- package/src/cli/new.ts +0 -234
- package/src/core/address-space.ts +0 -320
- package/src/core/cross-checks.ts +0 -369
- package/src/core/define.ts +0 -124
- package/src/core/project-files.ts +0 -35
- package/src/core/schema-docs.ts +0 -174
- package/src/core/validator.ts +0 -508
- package/src/helpers/bgp.ts +0 -473
- package/src/helpers/cables.ts +0 -74
- package/src/helpers/checks.ts +0 -201
- package/src/helpers/exchange.ts +0 -123
- package/src/helpers/filters.ts +0 -107
- package/src/helpers/index.ts +0 -25
- package/src/helpers/route-server.ts +0 -212
- package/src/helpers/upstreams.ts +0 -369
- package/src/index.ts +0 -13
- package/src/integrations/librenms.ts +0 -291
- package/src/integrations/netbox.ts +0 -620
- package/src/integrations/peeringdb.ts +0 -173
- package/src/integrations/prometheus.ts +0 -90
- package/src/schema/common.ts +0 -41
- package/src/schema/device.ts +0 -145
- package/src/schema/firewall.ts +0 -140
- package/src/schema/index.ts +0 -8
- package/src/schema/policy.ts +0 -115
- package/src/schema/routing.ts +0 -173
- package/src/schema/system.ts +0 -203
- package/src/studio/public/app.js +0 -186
- package/src/studio/public/compare.js +0 -250
- package/src/studio/public/dom.js +0 -138
- package/src/studio/public/evaluate.js +0 -687
- package/src/studio/public/export.js +0 -130
- package/src/studio/public/graph.js +0 -1541
- package/src/studio/public/index.html +0 -83
- package/src/studio/public/net.js +0 -134
- package/src/studio/public/tables.js +0 -1289
- package/src/studio/public/tools.js +0 -610
- package/templates/colocation/checks.ts +0 -53
- package/templates/colocation/circuit.config.ts +0 -9
- package/templates/colocation/edge-01.ts +0 -133
- package/templates/colocation/routing.ts +0 -13
- package/templates/colocation/tenancy.ts +0 -76
- package/templates/colocation/tenants/acme.ts +0 -19
- package/templates/colocation/tenants/index.ts +0 -6
- package/templates/colocation/tor-01.ts +0 -33
- package/templates/edge-router/checks.ts +0 -20
- package/templates/edge-router/circuit.config.ts +0 -8
- package/templates/edge-router/edge-01.ts +0 -110
- package/templates/edge-router/routing.ts +0 -13
- package/templates/project/AGENTS.md +0 -51
- package/templates/project/CLAUDE.md +0 -1
- package/templates/project/README.md +0 -16
- package/templates/project/gitignore +0 -5
- package/templates/project/tsconfig.json +0 -13
- package/templates/single-site/checks.ts +0 -26
- package/templates/single-site/circuit.config.ts +0 -8
- package/templates/single-site/router.ts +0 -86
- package/templates/single-site/site.ts +0 -18
- package/templates/single-site/switch.ts +0 -34
- /package/src/studio/{public → app/public}/favicon.svg +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// Rules that span devices: what one device declares about another has to agree with what the other declares. That a link's far end exists and links back
|
|
2
|
+
// is stopped on when the project is read, so a link that doesn't is skipped here.
|
|
3
|
+
|
|
4
|
+
import { Address } from "../core/address";
|
|
5
|
+
import type { Device, Gre, Prefix, Routed, Switched, Vrrp, Vxlan } from "../model";
|
|
6
|
+
import { Derive } from "../platforms/derive";
|
|
7
|
+
import type { Finding } from "../platforms/finding";
|
|
8
|
+
import { DeviceChecks } from "./device";
|
|
9
|
+
|
|
10
|
+
/** An address one of the devices holds on a port or interface. */
|
|
11
|
+
export type AddressHolder = {
|
|
12
|
+
device: Device;
|
|
13
|
+
/** The port or interface, as a path: `interfaces.ethernet.10g-1`, `interfaces.loopback.lo`. */
|
|
14
|
+
where: string;
|
|
15
|
+
/** Whether it is a loopback, which may hold an address another loopback holds, for anycast. */
|
|
16
|
+
loopback: boolean;
|
|
17
|
+
/** With its length. */
|
|
18
|
+
address: string;
|
|
19
|
+
/** The far end of a point-to-point address. */
|
|
20
|
+
peer?: string;
|
|
21
|
+
vrrp: readonly Vrrp[];
|
|
22
|
+
/** The VRF the address is in. The default table when absent. */
|
|
23
|
+
vrf?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** What a port's settings are kept on, as a path: the LAG when the port is in one, otherwise the port. */
|
|
27
|
+
type Carrier = { where: string; settings: Routed & Switched & { mtu?: number } };
|
|
28
|
+
|
|
29
|
+
/** One end of a GRE or VXLAN tunnel. */
|
|
30
|
+
type TunnelEnd = { device: Device; where: string; kind: "gre" | "vxlan"; tunnel: Gre | Vxlan };
|
|
31
|
+
|
|
32
|
+
export class CrossChecks {
|
|
33
|
+
/** Every rule across devices. */
|
|
34
|
+
static run(devices: readonly Device[]): Finding[] {
|
|
35
|
+
const holders = CrossChecks.holders(devices);
|
|
36
|
+
|
|
37
|
+
// Who holds each address. The first holder wins; a second is reported by `addresses`.
|
|
38
|
+
const owners = new Map<string, AddressHolder>();
|
|
39
|
+
for (const holder of holders) {
|
|
40
|
+
const key = CrossChecks.keyOf(Address.host(holder.address))!;
|
|
41
|
+
if (!owners.has(key)) owners.set(key, holder);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
...CrossChecks.links(devices),
|
|
46
|
+
...CrossChecks.addresses(holders),
|
|
47
|
+
...CrossChecks.sessions(devices, owners),
|
|
48
|
+
...CrossChecks.vrrp(holders),
|
|
49
|
+
...CrossChecks.tunnels(devices, owners),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** An address as a key, so two spellings of one address compare equal. Undefined when it is not an address. */
|
|
54
|
+
static keyOf(address: string | undefined): string | undefined {
|
|
55
|
+
if (!address || !Address.isValid(address)) return undefined;
|
|
56
|
+
return `${Address.family(address)}:${Address.toNumber(address)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Every valid address the devices hold. An invalid one is left for the device's own checks to report. */
|
|
60
|
+
static holders(devices: readonly Device[]): AddressHolder[] {
|
|
61
|
+
return devices.flatMap((device) =>
|
|
62
|
+
DeviceChecks.holders(device).flatMap(({ where, kind, settings }) =>
|
|
63
|
+
(settings.addresses ?? []).flatMap((entry) => {
|
|
64
|
+
const address = DeviceChecks.addressOf(entry);
|
|
65
|
+
if (!Address.isCidr(address)) return [];
|
|
66
|
+
|
|
67
|
+
const peer = typeof entry === "string" ? undefined : entry.peer_address;
|
|
68
|
+
return [{ device, where, loopback: kind === "loopback", address, peer, vrrp: settings.vrrp ?? [], vrf: settings.vrf }];
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The VLAN ids a port or LAG carries, by how it carries them, so two ends can be compared across devices. */
|
|
75
|
+
private static carried(device: Device, settings: Switched) {
|
|
76
|
+
const id = (vlan: string | undefined) => (vlan ? device.switching?.vlans?.[vlan]?.id : undefined);
|
|
77
|
+
return {
|
|
78
|
+
access: id(settings.access_vlan),
|
|
79
|
+
native: id(settings.native_vlan),
|
|
80
|
+
trunk: (settings.trunk_vlans ?? []).map(id).sort((a, b) => (a ?? 0) - (b ?? 0)),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** How a port carries its VLANs, in words: "VLANs 10, 20 tagged and VLAN 30 untagged as native". */
|
|
85
|
+
private static describeCarried({ access, native, trunk }: ReturnType<typeof CrossChecks.carried>): string {
|
|
86
|
+
const parts: string[] = [];
|
|
87
|
+
const tagged = trunk.filter((id) => id !== undefined);
|
|
88
|
+
|
|
89
|
+
if (access !== undefined) parts.push(`VLAN ${access} untagged`);
|
|
90
|
+
if (tagged.length) parts.push(`${tagged.length === 1 ? "VLAN" : "VLANs"} ${tagged.join(", ")} tagged`);
|
|
91
|
+
if (native !== undefined) parts.push(`VLAN ${native} untagged as native`);
|
|
92
|
+
|
|
93
|
+
return parts.length ? parts.join(" and ") : "no VLANs";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** VLAN names by id, for the VLANs a port or LAG carries. */
|
|
97
|
+
private static vlanNames(device: Device, settings: Switched): Map<number, string> {
|
|
98
|
+
const names = [settings.access_vlan, settings.native_vlan, ...(settings.trunk_vlans ?? [])].filter((name) => name !== undefined);
|
|
99
|
+
const vlans = device.switching?.vlans ?? {};
|
|
100
|
+
|
|
101
|
+
return new Map(names.flatMap((name) => (vlans[name] ? [[vlans[name].id, name] as const] : [])));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** What a port's settings are kept on: the LAG when the port is in one, otherwise the port. */
|
|
105
|
+
private static carrierOf(device: Device, portName: string): Carrier {
|
|
106
|
+
const port = device.interfaces!.ethernet![portName]!;
|
|
107
|
+
const lag = port.in_lag ? device.interfaces?.lag?.[port.in_lag] : undefined;
|
|
108
|
+
if (port.in_lag && lag) return { where: `interfaces.lag.${port.in_lag}`, settings: lag };
|
|
109
|
+
|
|
110
|
+
return { where: `interfaces.ethernet.${portName}`, settings: port };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The plain addresses of one family on an interface, leaving out point-to-point ones. */
|
|
114
|
+
private static plainAddresses(settings: Routed, family: string): string[] {
|
|
115
|
+
return (settings.addresses ?? []).filter(
|
|
116
|
+
(entry): entry is Prefix => typeof entry === "string" && Address.isCidr(entry) && Address.family(entry) === family
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Why the two ends of a cable cannot reach each other by address, or undefined when they can. */
|
|
121
|
+
private static addressDisagreement(here: Routed, there: Routed, far: Device, farWhere: string): string | undefined {
|
|
122
|
+
for (const family of ["ipv4", "ipv6"]) {
|
|
123
|
+
const ours = CrossChecks.plainAddresses(here, family);
|
|
124
|
+
const theirs = CrossChecks.plainAddresses(there, family);
|
|
125
|
+
if (!ours.length || !theirs.length) continue;
|
|
126
|
+
|
|
127
|
+
// A prefix includes its length, so 10.0.0.0/24 and 10.0.0.0/25 are not one.
|
|
128
|
+
const theirPrefixes = new Set(theirs.map(Address.prefixOf));
|
|
129
|
+
if (!ours.some((address) => theirPrefixes.has(Address.prefixOf(address)))) {
|
|
130
|
+
return `has ${ours.join(", ")}, and ${far.name} ${farWhere} has ${theirs.join(", ")}: not one prefix of one length`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// A point-to-point address names the far end's own address as its peer.
|
|
135
|
+
const farHosts = new Set((there.addresses ?? []).map((entry) => CrossChecks.keyOf(Address.host(DeviceChecks.addressOf(entry)))));
|
|
136
|
+
|
|
137
|
+
for (const entry of here.addresses ?? []) {
|
|
138
|
+
if (typeof entry === "string" || !CrossChecks.keyOf(entry.peer_address) || farHosts.has(CrossChecks.keyOf(entry.peer_address)))
|
|
139
|
+
continue;
|
|
140
|
+
return `${entry.address} names peer ${entry.peer_address}, which ${far.name} ${farWhere} does not hold`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Both ends of a link carry the same VLANs the same way, through a LAG on both ends or neither, with the same MTU, speed and addresses. */
|
|
147
|
+
private static links(devices: readonly Device[]): Finding[] {
|
|
148
|
+
const findings: Finding[] = [];
|
|
149
|
+
const byName = new Map(devices.map((device) => [device.name, device]));
|
|
150
|
+
|
|
151
|
+
for (const device of devices) {
|
|
152
|
+
for (const [portName, port] of Derive.ports(device)) {
|
|
153
|
+
const link = port.link;
|
|
154
|
+
if (!link) continue;
|
|
155
|
+
|
|
156
|
+
// Reading the project stops on a far end that is missing or doesn't link back.
|
|
157
|
+
const far = byName.get(link.device);
|
|
158
|
+
const farPort = far?.interfaces?.ethernet?.[link.port];
|
|
159
|
+
if (!far || !farPort || farPort.link?.device !== device.name || farPort.link.port !== portName) continue;
|
|
160
|
+
|
|
161
|
+
const report = (message: string, level: Finding["level"] = "error") =>
|
|
162
|
+
findings.push({ level, device: device.name, message: `interfaces.ethernet.${portName}.link: ${message}` });
|
|
163
|
+
|
|
164
|
+
const near = CrossChecks.carrierOf(device, portName);
|
|
165
|
+
const away = CrossChecks.carrierOf(far, link.port);
|
|
166
|
+
const farPortWhere = `${link.device} interfaces.ethernet.${link.port}`;
|
|
167
|
+
|
|
168
|
+
// A LAG's VLANs are on the LAG, so its member ports are compared by what their LAGs carry.
|
|
169
|
+
const inLag = near.where !== `interfaces.ethernet.${portName}`;
|
|
170
|
+
const farInLag = away.where !== `interfaces.ethernet.${link.port}`;
|
|
171
|
+
if (inLag !== farInLag) {
|
|
172
|
+
report(
|
|
173
|
+
inLag
|
|
174
|
+
? `is in LAG ${port.in_lag}, and ${farPortWhere} is in no LAG`
|
|
175
|
+
: `is in no LAG, and ${farPortWhere} is in LAG ${farPort.in_lag}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const here = CrossChecks.carried(device, near.settings);
|
|
180
|
+
const there = CrossChecks.carried(far, away.settings);
|
|
181
|
+
if (JSON.stringify(here) !== JSON.stringify(there)) {
|
|
182
|
+
report(
|
|
183
|
+
`${near.where} carries ${CrossChecks.describeCarried(here)}, and ${link.device} ${away.where} carries ${CrossChecks.describeCarried(there)}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (port.speed && farPort.speed && port.speed !== farPort.speed) {
|
|
188
|
+
report(`speed ${port.speed}, and ${farPortWhere} is ${farPort.speed}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const mtu = near.settings.mtu ?? 1500;
|
|
192
|
+
const farMtu = away.settings.mtu ?? 1500;
|
|
193
|
+
if (mtu !== farMtu) report(`MTU ${mtu} on ${near.where}, and ${link.device} ${away.where} has ${farMtu}`);
|
|
194
|
+
|
|
195
|
+
const disagreement = CrossChecks.addressDisagreement(near.settings, away.settings, far, away.where);
|
|
196
|
+
if (disagreement) report(`${near.where} ${disagreement}`);
|
|
197
|
+
|
|
198
|
+
// One VLAN under two names works, but reads as two VLANs.
|
|
199
|
+
const farNames = CrossChecks.vlanNames(far, away.settings);
|
|
200
|
+
for (const [id, name] of CrossChecks.vlanNames(device, near.settings)) {
|
|
201
|
+
const farName = farNames.get(id);
|
|
202
|
+
if (farName && farName !== name) report(`VLAN ${id} is ${name} here and ${farName} on ${link.device}`, "warning");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return findings;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** No address is held by two devices in one VRF. Two loopbacks may hold one on purpose, for anycast, so that is a warning. */
|
|
211
|
+
private static addresses(holders: AddressHolder[]): Finding[] {
|
|
212
|
+
const findings: Finding[] = [];
|
|
213
|
+
const first = new Map<string, AddressHolder>();
|
|
214
|
+
|
|
215
|
+
for (const holder of holders) {
|
|
216
|
+
const key = `${holder.vrf ?? ""} ${CrossChecks.keyOf(Address.host(holder.address))!}`;
|
|
217
|
+
const earlier = first.get(key);
|
|
218
|
+
|
|
219
|
+
if (!earlier) {
|
|
220
|
+
first.set(key, holder);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Two on one device are reported by the device's own checks.
|
|
225
|
+
if (earlier.device === holder.device) continue;
|
|
226
|
+
|
|
227
|
+
const anycast = earlier.loopback && holder.loopback;
|
|
228
|
+
|
|
229
|
+
findings.push({
|
|
230
|
+
level: anycast ? "warning" : "error",
|
|
231
|
+
device: holder.device.name,
|
|
232
|
+
message: `${Address.host(holder.address)} on ${holder.where} is also on ${earlier.device.name} ${earlier.where}${anycast ? ", which is right only for anycast" : ""}`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return findings;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Each BGP neighbor's address, AS and source, with what it takes from its group. */
|
|
240
|
+
private static sessionsOf(device: Device) {
|
|
241
|
+
const bgp = device.routing?.bgp;
|
|
242
|
+
|
|
243
|
+
return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
|
|
244
|
+
const group = neighbor.neighbor_group ? bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
name,
|
|
248
|
+
address: neighbor.address,
|
|
249
|
+
remoteAs: neighbor.remote_as ?? group?.remote_as,
|
|
250
|
+
localAddress: neighbor.local_address ?? group?.local_address,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 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. */
|
|
256
|
+
private static sessions(devices: readonly Device[], owners: Map<string, AddressHolder>): Finding[] {
|
|
257
|
+
const findings: Finding[] = [];
|
|
258
|
+
|
|
259
|
+
for (const device of devices) {
|
|
260
|
+
for (const session of CrossChecks.sessionsOf(device)) {
|
|
261
|
+
const far = owners.get(CrossChecks.keyOf(session.address) ?? "")?.device;
|
|
262
|
+
if (!far || far === device) continue;
|
|
263
|
+
|
|
264
|
+
const report = (message: string) =>
|
|
265
|
+
findings.push({ level: "error", device: device.name, message: `routing.bgp.neighbors.${session.name}: ${message}` });
|
|
266
|
+
|
|
267
|
+
const farBgp = far.routing?.bgp;
|
|
268
|
+
if (!farBgp) {
|
|
269
|
+
report(`${session.address} is ${far.name}'s, which runs no BGP`);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (session.remoteAs !== undefined && session.remoteAs !== farBgp.asn) {
|
|
274
|
+
report(`expects AS${session.remoteAs}, and ${far.name} is AS${farBgp.asn}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Two routers often hold one session per family, and a session of the other family does not answer this one.
|
|
278
|
+
const back = CrossChecks.sessionsOf(far).find(
|
|
279
|
+
(other) =>
|
|
280
|
+
owners.get(CrossChecks.keyOf(other.address) ?? "")?.device === device &&
|
|
281
|
+
Address.family(other.address) === Address.family(session.address)
|
|
282
|
+
);
|
|
283
|
+
if (!back) {
|
|
284
|
+
report(`${far.name} has no session back to ${device.name}`);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// The far end only accepts the session from the address it names.
|
|
289
|
+
if (session.localAddress && CrossChecks.keyOf(session.localAddress) !== CrossChecks.keyOf(back.address)) {
|
|
290
|
+
report(
|
|
291
|
+
`starts from ${session.localAddress}, and ${far.name}'s routing.bgp.neighbors.${back.name} expects ${back.address}`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return findings;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** On one segment, a VRRP id has one address and an address has one id, on every device. */
|
|
301
|
+
private static vrrp(holders: AddressHolder[]): Finding[] {
|
|
302
|
+
const findings: Finding[] = [];
|
|
303
|
+
const bySegment = new Map<string, { holder: AddressHolder; id: number; address: string }[]>();
|
|
304
|
+
|
|
305
|
+
for (const holder of holders) {
|
|
306
|
+
const segment = Address.prefixOf(holder.address);
|
|
307
|
+
|
|
308
|
+
for (const group of holder.vrrp) {
|
|
309
|
+
if (!CrossChecks.keyOf(group.address) || Address.family(group.address) !== Address.family(holder.address)) continue;
|
|
310
|
+
if (!Address.contains(segment, group.address)) continue;
|
|
311
|
+
|
|
312
|
+
bySegment.set(segment, [...(bySegment.get(segment) ?? []), { holder, id: group.id, address: group.address }]);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const members of bySegment.values()) {
|
|
317
|
+
for (const member of members) {
|
|
318
|
+
const where = `${member.holder.device.name} ${member.holder.where}`;
|
|
319
|
+
const others = members.filter((other) => other.holder.device !== member.holder.device);
|
|
320
|
+
|
|
321
|
+
const sameId = others.find(
|
|
322
|
+
(other) => other.id === member.id && CrossChecks.keyOf(other.address) !== CrossChecks.keyOf(member.address)
|
|
323
|
+
);
|
|
324
|
+
if (sameId) {
|
|
325
|
+
findings.push({
|
|
326
|
+
level: "error",
|
|
327
|
+
device: member.holder.device.name,
|
|
328
|
+
message: `VRRP ${member.id} on ${member.holder.where} is ${member.address}, and on ${sameId.holder.device.name} ${sameId.holder.where} it is ${sameId.address}`,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const sameAddress = others.find(
|
|
333
|
+
(other) => CrossChecks.keyOf(other.address) === CrossChecks.keyOf(member.address) && other.id !== member.id
|
|
334
|
+
);
|
|
335
|
+
if (sameAddress) {
|
|
336
|
+
findings.push({
|
|
337
|
+
level: "error",
|
|
338
|
+
device: member.holder.device.name,
|
|
339
|
+
message: `VRRP address ${member.address} is id ${member.id} on ${where}, and id ${sameAddress.id} on ${sameAddress.holder.device.name} ${sameAddress.holder.where}`,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return findings;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** 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. */
|
|
349
|
+
private static tunnels(devices: readonly Device[], owners: Map<string, AddressHolder>): Finding[] {
|
|
350
|
+
const findings: Finding[] = [];
|
|
351
|
+
|
|
352
|
+
const ends: TunnelEnd[] = devices.flatMap((device) => [
|
|
353
|
+
...Object.entries(device.interfaces?.gre ?? {}).flatMap(([name, tunnel]) =>
|
|
354
|
+
tunnel ? [{ device, where: `interfaces.gre.${name}`, kind: "gre" as const, tunnel }] : []
|
|
355
|
+
),
|
|
356
|
+
...Object.entries(device.interfaces?.vxlan ?? {}).flatMap(([name, tunnel]) =>
|
|
357
|
+
tunnel ? [{ device, where: `interfaces.vxlan.${name}`, kind: "vxlan" as const, tunnel }] : []
|
|
358
|
+
),
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
const same = (a: string, b: string) => CrossChecks.keyOf(a) !== undefined && CrossChecks.keyOf(a) === CrossChecks.keyOf(b);
|
|
362
|
+
const vniOf = (end: TunnelEnd) => (end.kind === "vxlan" ? (end.tunnel as Vxlan).vni : undefined);
|
|
363
|
+
|
|
364
|
+
for (const end of ends) {
|
|
365
|
+
const far = owners.get(CrossChecks.keyOf(end.tunnel.remote_address) ?? "")?.device;
|
|
366
|
+
if (!far || far === end.device) continue;
|
|
367
|
+
|
|
368
|
+
const report = (message: string) =>
|
|
369
|
+
findings.push({ level: "error", device: end.device.name, message: `${end.where}: ${message}` });
|
|
370
|
+
const kind = end.kind === "gre" ? "GRE" : "VXLAN";
|
|
371
|
+
|
|
372
|
+
// 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.
|
|
373
|
+
const back = ends.filter(
|
|
374
|
+
(other) =>
|
|
375
|
+
other.device === far &&
|
|
376
|
+
other.kind === end.kind &&
|
|
377
|
+
same(other.tunnel.local_address, end.tunnel.remote_address) &&
|
|
378
|
+
same(other.tunnel.remote_address, end.tunnel.local_address)
|
|
379
|
+
);
|
|
380
|
+
const mirror = back.find((other) => vniOf(other) === vniOf(end)) ?? back[0];
|
|
381
|
+
|
|
382
|
+
if (!mirror) {
|
|
383
|
+
report(
|
|
384
|
+
`${end.tunnel.remote_address} is ${far.name}'s, which has no ${kind} tunnel from there back to ${end.tunnel.local_address}`
|
|
385
|
+
);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (end.kind !== "vxlan" || mirror.kind !== "vxlan") continue;
|
|
390
|
+
|
|
391
|
+
const ours = end.tunnel as Vxlan;
|
|
392
|
+
const theirs = mirror.tunnel as Vxlan;
|
|
393
|
+
if (ours.vni !== theirs.vni) report(`VNI ${ours.vni}, and ${far.name} ${mirror.where} has ${theirs.vni}`);
|
|
394
|
+
|
|
395
|
+
const port = ours.port ?? 4789;
|
|
396
|
+
const farPort = theirs.port ?? 4789;
|
|
397
|
+
if (port !== farPort) report(`UDP port ${port}, and ${far.name} ${mirror.where} has ${farPort}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return findings;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Circuit's built-in checks, which a network turns on by name in `checks` (`Checks` in src/config/network.ts). `same_vlan_ids` is run when the project is
|
|
2
|
+
// read, so it is not here.
|
|
3
|
+
|
|
4
|
+
import { Address } from "../core/address";
|
|
5
|
+
import type { Checks } from "../config/network";
|
|
6
|
+
import type { Device, Interface, Switched } from "../model";
|
|
7
|
+
import { Derive } from "../platforms/derive";
|
|
8
|
+
import type { Finding } from "../platforms/finding";
|
|
9
|
+
import { DeviceChecks } from "./device";
|
|
10
|
+
|
|
11
|
+
/** The built-in checks' settings, as `circuit()` takes them. */
|
|
12
|
+
export type NetworkChecks = Checks<string>;
|
|
13
|
+
|
|
14
|
+
/** One BGP neighbor's policies, with what it takes from its group. */
|
|
15
|
+
type NeighborPolicies = { name: string; address: string; role?: string; importPolicy?: string; exportPolicy?: string };
|
|
16
|
+
|
|
17
|
+
/** A route policy rule as the checks read it: a route rule, or a call, which has neither `if` nor `then`. */
|
|
18
|
+
type LooseRule = { if?: { prefix?: string }; then?: string; call?: string };
|
|
19
|
+
|
|
20
|
+
export class BuiltInChecks {
|
|
21
|
+
/** Each check the network turns on. A name that is not one of Circuit's is the project's own, which reading the project ran. */
|
|
22
|
+
static run(devices: readonly Device[], checks: NetworkChecks | undefined): Finding[] {
|
|
23
|
+
if (!checks) return [];
|
|
24
|
+
|
|
25
|
+
return [
|
|
26
|
+
...(checks.exports_end_in_reject ? BuiltInChecks.exportsEndInReject(devices) : []),
|
|
27
|
+
...(checks.imports_end_in_accept_or_reject ? BuiltInChecks.importsEndInAcceptOrReject(devices) : []),
|
|
28
|
+
...(checks.tunnels_outside_offered ? BuiltInChecks.tunnelsOutsideOffered(devices) : []),
|
|
29
|
+
...(checks.native_vlans_are_dead_ends ? BuiltInChecks.nativeVlansAreDeadEnds(devices) : []),
|
|
30
|
+
...(checks.untrusted_vlans ? BuiltInChecks.untrustedVlans(devices, checks.untrusted_vlans) : []),
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Each BGP neighbor of a device, with the role and policies it takes from its group when it sets none. */
|
|
35
|
+
static neighborPolicies(device: Device): NeighborPolicies[] {
|
|
36
|
+
const bgp = device.routing?.bgp;
|
|
37
|
+
|
|
38
|
+
return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
|
|
39
|
+
const group = neighbor.neighbor_group ? bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
address: neighbor.address,
|
|
44
|
+
role: neighbor.role ?? group?.role,
|
|
45
|
+
importPolicy: neighbor.import_policy ?? group?.import_policy,
|
|
46
|
+
exportPolicy: neighbor.export_policy ?? group?.export_policy,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A policy's rules, read loosely: a call rule is one with neither `if` nor `then`. */
|
|
52
|
+
private static rulesOf(device: Device, policy: string | undefined): LooseRule[] {
|
|
53
|
+
return policy ? ((device.routing?.policies?.[policy] ?? []) as readonly LooseRule[]).slice() : [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The rule a policy ends in. A policy that ends by calling another, such as the one Circuit makes per AS for a community scheme's requests, ends where the called one ends. */
|
|
57
|
+
private static lastRuleOf(device: Device, policy: string | undefined, seen = new Set<string>()): LooseRule | undefined {
|
|
58
|
+
const last = BuiltInChecks.rulesOf(device, policy).at(-1);
|
|
59
|
+
if (!last?.call || last.if || seen.has(last.call)) return last;
|
|
60
|
+
|
|
61
|
+
seen.add(policy!);
|
|
62
|
+
return BuiltInChecks.lastRuleOf(device, last.call, seen);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The prefixes an export policy accepts by name. */
|
|
66
|
+
static offered(device: Device, policy: string | undefined): string[] {
|
|
67
|
+
return BuiltInChecks.rulesOf(device, policy)
|
|
68
|
+
.filter((rule) => rule.then === "accept" && rule.if?.prefix)
|
|
69
|
+
.map((rule) => rule.if!.prefix!);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The VLANs a port or LAG carries, untagged or tagged. */
|
|
73
|
+
static vlansOf(port: Pick<Switched, "access_vlan" | "trunk_vlans">): string[] {
|
|
74
|
+
return [port.access_vlan, ...(port.trunk_vlans ?? [])].filter((vlan): vlan is string => vlan !== undefined);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `exports_end_in_reject`: every BGP export ends in a reject that matches everything. An export to a neighbor whose `role` is `customer` is left out. */
|
|
78
|
+
static exportsEndInReject(devices: readonly Device[]): Finding[] {
|
|
79
|
+
const findings: Finding[] = [];
|
|
80
|
+
|
|
81
|
+
for (const device of devices) {
|
|
82
|
+
for (const neighbor of BuiltInChecks.neighborPolicies(device)) {
|
|
83
|
+
if (neighbor.role === "customer" || !neighbor.exportPolicy) continue;
|
|
84
|
+
|
|
85
|
+
const last = BuiltInChecks.lastRuleOf(device, neighbor.exportPolicy);
|
|
86
|
+
if (last?.then !== "reject" || last.if) {
|
|
87
|
+
findings.push({
|
|
88
|
+
level: "error",
|
|
89
|
+
device: device.name,
|
|
90
|
+
message: `routing.bgp.neighbors.${neighbor.name}: export_policy ${neighbor.exportPolicy} does not end in an unconditional reject`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return findings;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `imports_end_in_accept_or_reject`: every BGP import ends in a rule that decides for every route, an accept or a reject without a match. */
|
|
100
|
+
static importsEndInAcceptOrReject(devices: readonly Device[]): Finding[] {
|
|
101
|
+
const findings: Finding[] = [];
|
|
102
|
+
|
|
103
|
+
for (const device of devices) {
|
|
104
|
+
for (const neighbor of BuiltInChecks.neighborPolicies(device)) {
|
|
105
|
+
if (!neighbor.importPolicy) continue;
|
|
106
|
+
|
|
107
|
+
const last = BuiltInChecks.lastRuleOf(device, neighbor.importPolicy);
|
|
108
|
+
if (!last?.then || last.if) {
|
|
109
|
+
findings.push({
|
|
110
|
+
level: "error",
|
|
111
|
+
device: device.name,
|
|
112
|
+
message: `routing.bgp.neighbors.${neighbor.name}: import_policy ${neighbor.importPolicy} does not end in an unconditional accept or reject`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return findings;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `tunnels_outside_offered`: a GRE or VXLAN tunnel's local address is outside what the neighbors over it are offered, or their replies route back into the tunnel. */
|
|
122
|
+
static tunnelsOutsideOffered(devices: readonly Device[]): Finding[] {
|
|
123
|
+
const findings: Finding[] = [];
|
|
124
|
+
|
|
125
|
+
for (const device of devices) {
|
|
126
|
+
const tunnels = Derive.interfaces(device).filter(({ kind }) => kind === "gre" || kind === "vxlan");
|
|
127
|
+
|
|
128
|
+
for (const { name, kind, settings } of tunnels) {
|
|
129
|
+
const local = (settings as Extract<Interface, { local_address: string }>).local_address;
|
|
130
|
+
const subnets = (settings.addresses ?? []).map(DeviceChecks.addressOf);
|
|
131
|
+
|
|
132
|
+
for (const neighbor of BuiltInChecks.neighborPolicies(device)) {
|
|
133
|
+
if (!subnets.some((subnet) => Address.contains(subnet, neighbor.address))) continue;
|
|
134
|
+
|
|
135
|
+
for (const prefix of BuiltInChecks.offered(device, neighbor.exportPolicy)) {
|
|
136
|
+
if (Address.contains(prefix, local)) {
|
|
137
|
+
findings.push({
|
|
138
|
+
level: "error",
|
|
139
|
+
device: device.name,
|
|
140
|
+
message: `interfaces.${kind}.${name} is sourced from ${local}, inside ${prefix}, which routing.bgp.neighbors.${neighbor.name} is offered`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return findings;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* `native_vlans_are_dead_ends`: a native VLAN is native on one trunk (a port or a LAG) of a device at most, and never an access VLAN or a tagged VLAN
|
|
153
|
+
* anywhere on that device.
|
|
154
|
+
*/
|
|
155
|
+
static nativeVlansAreDeadEnds(devices: readonly Device[]): Finding[] {
|
|
156
|
+
const findings: Finding[] = [];
|
|
157
|
+
|
|
158
|
+
for (const device of devices) {
|
|
159
|
+
const carriers = [
|
|
160
|
+
...Derive.ports(device).map(([name, port]) => ({ where: `interfaces.ethernet.${name}`, settings: port as Switched })),
|
|
161
|
+
...Object.entries(device.interfaces?.lag ?? {}).flatMap(([name, lag]) =>
|
|
162
|
+
lag ? [{ where: `interfaces.lag.${name}`, settings: lag as Switched }] : []
|
|
163
|
+
),
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const nativeOn = new Map<string, string[]>();
|
|
167
|
+
for (const { where, settings } of carriers) {
|
|
168
|
+
if (settings.native_vlan) nativeOn.set(settings.native_vlan, [...(nativeOn.get(settings.native_vlan) ?? []), where]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const [vlan, trunks] of nativeOn) {
|
|
172
|
+
const report = (message: string) =>
|
|
173
|
+
findings.push({ level: "error", device: device.name, message: `native VLAN ${vlan} ${message}` });
|
|
174
|
+
|
|
175
|
+
if (trunks.length > 1) report(`is native on ${trunks.join(", ")}: give each trunk its own`);
|
|
176
|
+
|
|
177
|
+
const members = carriers.filter(({ settings }) => BuiltInChecks.vlansOf(settings).includes(vlan));
|
|
178
|
+
for (const { where } of members) report(`is also an access or tagged VLAN on ${where}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return findings;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* `untrusted_vlans`: traffic from outside reaches your own VLANs only through a filtering device. A port or LAG carrying an untrusted VLAN beside any other
|
|
187
|
+
* is on a filtering device or faces one, and only a filtering device gives an untrusted VLAN an address. The untrusted VLANs are those written
|
|
188
|
+
* `untrusted: true` on any device.
|
|
189
|
+
*/
|
|
190
|
+
static untrustedVlans(devices: readonly Device[], settings: NonNullable<NetworkChecks["untrusted_vlans"]>): Finding[] {
|
|
191
|
+
const untrusted = new Set(
|
|
192
|
+
devices.flatMap((device) =>
|
|
193
|
+
Object.entries(device.switching?.vlans ?? {}).flatMap(([name, vlan]) => (vlan?.untrusted === true ? [name] : []))
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
const filtering = new Set<string>(settings.filtering_devices);
|
|
197
|
+
const mixed = (a: string, b: string) => (settings.mixed_links ?? []).some(([x, y]) => (x === a && y === b) || (x === b && y === a));
|
|
198
|
+
const findings: Finding[] = [];
|
|
199
|
+
|
|
200
|
+
for (const device of devices) {
|
|
201
|
+
const filters = filtering.has(device.name);
|
|
202
|
+
|
|
203
|
+
// What carries VLANs: a port on its own, or a LAG, which faces whatever its members link to.
|
|
204
|
+
const ports = Derive.ports(device);
|
|
205
|
+
const carriers = [
|
|
206
|
+
...ports
|
|
207
|
+
.filter(([, port]) => !port.in_lag)
|
|
208
|
+
.map(([name, port]) => ({
|
|
209
|
+
where: `interfaces.ethernet.${name}`,
|
|
210
|
+
settings: port as Switched,
|
|
211
|
+
faces: port.link ? [port.link.device] : [],
|
|
212
|
+
})),
|
|
213
|
+
...Object.entries(device.interfaces?.lag ?? {}).flatMap(([name, lag]) =>
|
|
214
|
+
lag
|
|
215
|
+
? [
|
|
216
|
+
{
|
|
217
|
+
where: `interfaces.lag.${name}`,
|
|
218
|
+
settings: lag as Switched,
|
|
219
|
+
faces: ports.filter(([, port]) => port.in_lag === name && port.link).map(([, port]) => port.link!.device),
|
|
220
|
+
},
|
|
221
|
+
]
|
|
222
|
+
: []
|
|
223
|
+
),
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
for (const { where, settings: carrier, faces } of carriers) {
|
|
227
|
+
const carried = BuiltInChecks.vlansOf(carrier);
|
|
228
|
+
const mixes = carried.some((vlan) => untrusted.has(vlan)) && carried.some((vlan) => !untrusted.has(vlan));
|
|
229
|
+
const facesFilter = filters || faces.some((name) => filtering.has(name));
|
|
230
|
+
const isMixedLink = faces.length > 0 && faces.every((name) => mixed(device.name, name));
|
|
231
|
+
|
|
232
|
+
if (mixes && !facesFilter && !isMixedLink) {
|
|
233
|
+
findings.push({
|
|
234
|
+
level: "error",
|
|
235
|
+
device: device.name,
|
|
236
|
+
message: `${where} carries an untrusted VLAN beside a trusted one and does not face one of filtering_devices`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (filters) continue;
|
|
242
|
+
|
|
243
|
+
for (const [name, vlanInterface] of Object.entries(device.interfaces?.vlan_interface ?? {})) {
|
|
244
|
+
if (vlanInterface && untrusted.has(vlanInterface.vlan)) {
|
|
245
|
+
findings.push({
|
|
246
|
+
level: "error",
|
|
247
|
+
device: device.name,
|
|
248
|
+
message: `interfaces.vlan_interface.${name} puts an address on untrusted VLAN ${vlanInterface.vlan}; only one of filtering_devices may`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return findings;
|
|
255
|
+
}
|
|
256
|
+
}
|