@takodotid/circuit 0.5.0 → 0.6.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/package.json +1 -1
- package/src/core/merge.ts +19 -0
- package/src/index.ts +1 -0
- package/src/presets/index.ts +80 -2
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Combining records from several sources, such as a device's own interfaces and those a module derives for it.
|
|
2
|
+
|
|
3
|
+
type Merged<T extends readonly object[]> = T extends readonly [infer Head, ...infer Tail extends readonly object[]]
|
|
4
|
+
? Head & Merged<Tail>
|
|
5
|
+
: unknown;
|
|
6
|
+
|
|
7
|
+
/** Combine records into one. Unlike spreading them, a name two of them hold is an error rather than one silently replacing the other. */
|
|
8
|
+
export function merge<const T extends readonly Record<string, unknown>[]>(...records: T): Merged<T> {
|
|
9
|
+
const result: Record<string, unknown> = {};
|
|
10
|
+
|
|
11
|
+
for (const record of records) {
|
|
12
|
+
for (const [key, value] of Object.entries(record)) {
|
|
13
|
+
if (key in result) throw new Error(`merge: ${key} is declared twice`);
|
|
14
|
+
result[key] = value;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return result as Merged<T>;
|
|
19
|
+
}
|
package/src/index.ts
CHANGED
package/src/presets/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Building blocks most networks write the same way: address lists, edge filters, a BGP import sanity policy, and checks. Each returns plain config to place where it belongs, so nothing here applies itself.
|
|
1
|
+
// Building blocks most networks write the same way: address lists, edge filters, a BGP import sanity policy, checks, and helpers for checks of your own. Each returns plain config to place where it belongs, so nothing here applies itself.
|
|
2
2
|
|
|
3
3
|
import { contains } from "../core/addr";
|
|
4
4
|
import type { Check, Finding } from "../core/define";
|
|
@@ -67,7 +67,7 @@ export function bgpSanity(lengths: SanityLengths = {}): PolicyRule<never, never>
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** Each BGP neighbor's own policies, or its group's. */
|
|
70
|
-
function neighborPolicies(device: Device) {
|
|
70
|
+
export function neighborPolicies(device: Device) {
|
|
71
71
|
const bgp = device.routing?.bgp;
|
|
72
72
|
|
|
73
73
|
return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
|
|
@@ -103,6 +103,84 @@ export const exportsEndInReject: Check = (devices) => {
|
|
|
103
103
|
return findings;
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
/** The VLANs a port carries, untagged or tagged. */
|
|
107
|
+
export const vlansOf = (port: { access_vlan?: string; trunk_vlans?: readonly string[] }): string[] =>
|
|
108
|
+
[port.access_vlan, ...(port.trunk_vlans ?? [])].filter((vlan): vlan is string => vlan !== undefined);
|
|
109
|
+
|
|
110
|
+
/** Where traffic from outside, before anything filters it, may go. */
|
|
111
|
+
export type TrustBoundary = {
|
|
112
|
+
/** VLANs carrying traffic straight from outside: transits, exchanges, an ISP handoff. */
|
|
113
|
+
untrusted: readonly string[];
|
|
114
|
+
/** VLANs that must not meet them on a port: customers, servers, management. */
|
|
115
|
+
trusted: readonly string[];
|
|
116
|
+
/** The devices that filter: the only ones that may give an untrusted VLAN an address, and the only ones a port carrying both may face. */
|
|
117
|
+
routers: readonly string[];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** A check: untrusted traffic reaches trusted VLANs only through a router. A port carrying both faces a router or is on one, and only a router gives an untrusted VLAN an address. */
|
|
121
|
+
export const trustBoundary =
|
|
122
|
+
(boundary: TrustBoundary): Check =>
|
|
123
|
+
(devices) => {
|
|
124
|
+
const untrusted = new Set(boundary.untrusted);
|
|
125
|
+
const trusted = new Set(boundary.trusted);
|
|
126
|
+
const routers = new Set(boundary.routers);
|
|
127
|
+
const findings: Finding[] = [];
|
|
128
|
+
|
|
129
|
+
for (const device of devices) {
|
|
130
|
+
const isRouter = routers.has(device.name);
|
|
131
|
+
const ports = Object.entries(device.ports ?? {}).flatMap(([name, port]) => (port ? [{ name, port }] : []));
|
|
132
|
+
|
|
133
|
+
// What carries VLANs: a port on its own, or a LAG, which faces whatever its members link to.
|
|
134
|
+
const carriers = [
|
|
135
|
+
...ports
|
|
136
|
+
.filter(({ port }) => !port.lag)
|
|
137
|
+
.map(({ name, port }) => ({
|
|
138
|
+
what: `port ${name}`,
|
|
139
|
+
carried: vlansOf(port),
|
|
140
|
+
faces: port.link ? [port.link.device] : [],
|
|
141
|
+
})),
|
|
142
|
+
...Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) =>
|
|
143
|
+
iface.type === "lag"
|
|
144
|
+
? [
|
|
145
|
+
{
|
|
146
|
+
what: `LAG ${name}`,
|
|
147
|
+
carried: vlansOf(iface),
|
|
148
|
+
faces: ports.filter(({ port }) => port.lag === name && port.link).map(({ port }) => port.link!.device),
|
|
149
|
+
},
|
|
150
|
+
]
|
|
151
|
+
: []
|
|
152
|
+
),
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
for (const { what, carried, faces } of carriers) {
|
|
156
|
+
const mixes = carried.some((vlan) => untrusted.has(vlan)) && carried.some((vlan) => trusted.has(vlan));
|
|
157
|
+
const facesRouter = isRouter || faces.some((name) => routers.has(name));
|
|
158
|
+
|
|
159
|
+
if (mixes && !facesRouter) {
|
|
160
|
+
findings.push({
|
|
161
|
+
level: "error",
|
|
162
|
+
device: device.name,
|
|
163
|
+
message: `${what} carries an untrusted VLAN beside a trusted one and does not face a router`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (isRouter) continue;
|
|
169
|
+
|
|
170
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
171
|
+
if (iface.type === "vlan" && untrusted.has(iface.vlan)) {
|
|
172
|
+
findings.push({
|
|
173
|
+
level: "error",
|
|
174
|
+
device: device.name,
|
|
175
|
+
message: `interface ${name} puts an address on untrusted VLAN ${iface.vlan}; only a router may`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return findings;
|
|
182
|
+
};
|
|
183
|
+
|
|
106
184
|
/** 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. */
|
|
107
185
|
export const tunnelsOutsideOffered: Check = (devices) => {
|
|
108
186
|
const findings: Finding[] = [];
|