@takodotid/circuit 0.40.0 → 0.42.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/dist/chunks/index-6wb41q67.js +62 -0
- package/dist/chunks/{index-k38bby43.js → index-yzw86rc8.js} +4 -62
- package/dist/chunks/{server-ggertnce.js → server-cxr9ynxc.js} +1 -1
- package/dist/cli/index.js +127 -49
- package/dist/helpers/index.js +143 -156
- package/dist/types/core/cabling.d.ts +0 -6
- package/dist/types/core/define.d.ts +1 -1
- package/dist/types/core/validator.d.ts +2 -0
- package/dist/types/helpers/bgp.d.ts +53 -9
- package/dist/types/helpers/cables.d.ts +14 -11
- package/dist/types/helpers/checks.d.ts +4 -0
- package/dist/types/helpers/exchange.d.ts +7 -1
- package/dist/types/helpers/index.d.ts +2 -2
- package/dist/types/helpers/upstreams.d.ts +123 -181
- package/dist/types/platforms/derive.d.ts +1 -1
- package/dist/types/platforms/fortios/routing.d.ts +2 -2
- package/dist/types/platforms/junos/bgp.d.ts +2 -2
- package/dist/types/platforms/routeros/render.d.ts +4 -0
- package/dist/types/schema/device.d.ts +4 -4
- package/dist/types/schema/firewall.d.ts +5 -1
- package/dist/types/schema/interfaces.d.ts +1 -1
- package/dist/types/schema/policy.d.ts +15 -6
- package/dist/types/schema/routing.d.ts +43 -10
- package/package.json +1 -1
- package/src/cli/index.ts +26 -29
- package/src/core/cabling.ts +0 -30
- package/src/core/define.ts +3 -1
- package/src/core/validator.ts +70 -2
- package/src/helpers/bgp.ts +117 -67
- package/src/helpers/cables.ts +20 -15
- package/src/helpers/checks.ts +23 -0
- package/src/helpers/exchange.ts +9 -3
- package/src/helpers/index.ts +2 -2
- package/src/helpers/upstreams.ts +162 -274
- package/src/platforms/derive.ts +23 -1
- package/src/platforms/fortios/routing.ts +2 -2
- package/src/platforms/junos/bgp.ts +2 -2
- package/src/platforms/routeros/TRAPS.md +4 -0
- package/src/platforms/routeros/export.ts +1 -0
- package/src/platforms/routeros/index.ts +11 -2
- package/src/platforms/routeros/plan.ts +3 -0
- package/src/platforms/routeros/render.ts +28 -3
- package/src/schema/device.ts +4 -3
- package/src/schema/firewall.ts +5 -1
- package/src/schema/interfaces.ts +1 -1
- package/src/schema/policy.ts +17 -6
- package/src/schema/routing.ts +46 -10
- package/templates/colocation/checks.ts +14 -11
- package/templates/colocation/edge-01.ts +2 -2
- package/templates/colocation/tenancy.ts +50 -71
- package/templates/colocation/tenants/acme.ts +10 -10
- package/templates/colocation/tenants/index.ts +2 -1
- package/templates/colocation/tor-01.ts +2 -2
- package/templates/project/AGENTS.md +15 -15
|
@@ -78,15 +78,22 @@ export type Sets = {
|
|
|
78
78
|
blackhole?: true;
|
|
79
79
|
};
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* A rule without `action` changes the route and moves on to the next rule.
|
|
81
|
+
* Run another policy at this point. A route it accepts or rejects stops there; any other route goes on to the next rule. A call holds nothing else: to call a policy for some routes only, put the match inside that policy.
|
|
84
82
|
*/
|
|
85
|
-
export type
|
|
83
|
+
export type CallRule<P extends string> = {
|
|
86
84
|
/** Shown beside the rule on the device. */
|
|
87
85
|
description?: string;
|
|
88
|
-
/**
|
|
89
|
-
call
|
|
86
|
+
/** The policy to run. */
|
|
87
|
+
call: P;
|
|
88
|
+
match?: never;
|
|
89
|
+
set?: never;
|
|
90
|
+
action?: never;
|
|
91
|
+
};
|
|
92
|
+
/** Change and decide on the routes that match. A rule without `action` changes the route and moves on to the next rule. */
|
|
93
|
+
export type MatchRule<S extends string> = {
|
|
94
|
+
/** Shown beside the rule on the device. */
|
|
95
|
+
description?: string;
|
|
96
|
+
call?: never;
|
|
90
97
|
/** Which routes the rule applies to. Every route when absent. */
|
|
91
98
|
match?: Match<S>;
|
|
92
99
|
/** What to change on a matching route. */
|
|
@@ -94,5 +101,7 @@ export type PolicyRule<P extends string, S extends string> = {
|
|
|
94
101
|
/** Accept or reject a matching route, ending the policy for it. */
|
|
95
102
|
action?: "accept" | "reject";
|
|
96
103
|
};
|
|
104
|
+
/** One step of a route policy: a call to another policy, or a rule that matches routes, changes them and may decide on them. */
|
|
105
|
+
export type PolicyRule<P extends string, S extends string> = CallRule<P> | MatchRule<S>;
|
|
97
106
|
/** Rules evaluated in order, first decision wins. A route no rule accepts is rejected. */
|
|
98
107
|
export type RoutePolicy<P extends string, S extends string> = readonly PolicyRule<P, S>[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Family, IP, Prefix, Secret } from "./common.js";
|
|
2
2
|
/** A route the device has without learning it. */
|
|
3
|
-
export type StaticRoute<I extends string, F extends string> = {
|
|
3
|
+
export type StaticRoute<I extends string, F extends string, T extends string = never> = {
|
|
4
4
|
/** Destination. */
|
|
5
5
|
prefix: Prefix;
|
|
6
6
|
/** Next-hop address. */
|
|
@@ -13,8 +13,10 @@ export type StaticRoute<I extends string, F extends string> = {
|
|
|
13
13
|
bfd?: true;
|
|
14
14
|
/** Preference against other routes to the same prefix. Lower wins. 1 when absent. */
|
|
15
15
|
distance?: number;
|
|
16
|
-
/** The VRF the route is in. The
|
|
16
|
+
/** The VRF the route is in. The main table when absent. */
|
|
17
17
|
vrf?: F;
|
|
18
|
+
/** The table the route is in, one of `routing.tables`. The main table when absent. A route has a `vrf` or a `table`, not both. */
|
|
19
|
+
table?: T;
|
|
18
20
|
/** Why the route exists. */
|
|
19
21
|
description?: string;
|
|
20
22
|
};
|
|
@@ -23,7 +25,7 @@ export type StaticRoute<I extends string, F extends string> = {
|
|
|
23
25
|
*/
|
|
24
26
|
export type BgpRole = "provider" | "customer" | "peer" | "rs" | "rs-client";
|
|
25
27
|
/** Settings a BGP neighbor can set itself or take from its group. */
|
|
26
|
-
export type NeighborSettings<P extends string> = {
|
|
28
|
+
export type NeighborSettings<P extends string, T extends string = never> = {
|
|
27
29
|
/** The neighbor's AS. Our own AS makes the session internal. */
|
|
28
30
|
remote_as?: number;
|
|
29
31
|
/** Who the neighbor is. */
|
|
@@ -52,24 +54,26 @@ export type NeighborSettings<P extends string> = {
|
|
|
52
54
|
passive?: boolean;
|
|
53
55
|
/** Detect a dead link in under a second with BFD. */
|
|
54
56
|
bfd?: boolean;
|
|
57
|
+
/** The table the routes learned are installed in, one of `routing.tables`. The main table when absent. What is announced still comes from `networks` and the main table. */
|
|
58
|
+
table?: T;
|
|
55
59
|
};
|
|
56
60
|
/** One BGP session. Settings it does not set come from its group. */
|
|
57
|
-
export type Neighbor<P extends string, G extends string> = NeighborSettings<P> & {
|
|
61
|
+
export type Neighbor<P extends string, G extends string, T extends string = never> = NeighborSettings<P, T> & {
|
|
58
62
|
/** The neighbor's address. */
|
|
59
63
|
address: IP;
|
|
60
64
|
/** The group this neighbor takes a setting from when it does not set that setting itself. */
|
|
61
65
|
group?: NoInfer<G>;
|
|
62
66
|
};
|
|
63
67
|
/** BGP on this device. */
|
|
64
|
-
export type Bgp<P extends string, G extends string> = {
|
|
68
|
+
export type Bgp<P extends string, G extends string, T extends string = never> = {
|
|
65
69
|
/** Our AS number. */
|
|
66
70
|
asn: number;
|
|
67
71
|
/** Prefixes this device originates. Each needs a route, for example a static blackhole. Export policy still decides who receives them. */
|
|
68
72
|
networks?: readonly Prefix[];
|
|
69
73
|
/** Settings shared by several neighbors, keyed by group name. */
|
|
70
|
-
groups?: Record<G, NeighborSettings<P>>;
|
|
74
|
+
groups?: Record<G, NeighborSettings<P, T>>;
|
|
71
75
|
/** Sessions keyed by name. */
|
|
72
|
-
neighbors?: Record<string, Neighbor<P, G>>;
|
|
76
|
+
neighbors?: Record<string, Neighbor<P, G, T>>;
|
|
73
77
|
};
|
|
74
78
|
/** How fast BFD notices a dead neighbor: after `multiplier` packets in a row are missed, so in `interval` times `multiplier` milliseconds. */
|
|
75
79
|
export type Bfd = {
|
|
@@ -111,14 +115,43 @@ export type Ospf<I extends string> = {
|
|
|
111
115
|
/** Routes from other sources announced into OSPF. */
|
|
112
116
|
redistribute?: readonly ("connected" | "static")[];
|
|
113
117
|
};
|
|
118
|
+
/** A routing table beside the main one. It holds no interfaces: routes go in from static routes and BGP neighbors that name it in `table`, and `routing.rules` send traffic to it. */
|
|
119
|
+
export type RoutingTable = {
|
|
120
|
+
/** What the table is for. */
|
|
121
|
+
description?: string;
|
|
122
|
+
};
|
|
123
|
+
/** Traffic a routing rule matches: all of what is given. Every packet when absent. */
|
|
124
|
+
export type RoutingRuleMatch<I extends string> = {
|
|
125
|
+
/** Source prefix. */
|
|
126
|
+
src?: Prefix;
|
|
127
|
+
/** Destination prefix. */
|
|
128
|
+
dst?: Prefix;
|
|
129
|
+
/** The interface the traffic arrives on. */
|
|
130
|
+
in_interface?: I;
|
|
131
|
+
};
|
|
132
|
+
/** One routing rule: traffic it matches is routed by `table` instead of the main table. */
|
|
133
|
+
export type RoutingRule<I extends string, T extends string> = {
|
|
134
|
+
/** Why the rule exists. */
|
|
135
|
+
description?: string;
|
|
136
|
+
/** What the traffic must match. Every packet when absent, so such a rule is the last one. */
|
|
137
|
+
match?: RoutingRuleMatch<I>;
|
|
138
|
+
/** The table that routes the traffic: one of `routing.tables`, or `main`. */
|
|
139
|
+
table: T | "main";
|
|
140
|
+
/** When `table` has no route for a packet: `none` drops it as unroutable, `main` routes it by the main table instead. `none` when absent. Not for a rule whose table is `main`. */
|
|
141
|
+
fallback?: "none" | "main";
|
|
142
|
+
};
|
|
114
143
|
/** Routing on this device. */
|
|
115
|
-
export type Routing<P extends string, G extends string, I extends string, F extends string> = {
|
|
144
|
+
export type Routing<P extends string, G extends string, I extends string, F extends string, T extends string = never> = {
|
|
116
145
|
/** Identifies this router to its neighbors. */
|
|
117
146
|
router_id?: IP;
|
|
147
|
+
/** Routing tables beside the main one, keyed by name. Not `main`, and not the name of a VRF. */
|
|
148
|
+
tables?: Record<T, RoutingTable>;
|
|
149
|
+
/** Rules that route matching traffic by another table, such as traffic from one block by its own transits' default. In order: the first that matches decides, and traffic no rule matches is routed by the main table. */
|
|
150
|
+
rules?: readonly RoutingRule<I, NoInfer<T>>[];
|
|
118
151
|
/** Routes the device has without learning them. */
|
|
119
|
-
static?: readonly StaticRoute<I, F
|
|
152
|
+
static?: readonly StaticRoute<I, F, NoInfer<T>>[];
|
|
120
153
|
/** BGP sessions and what they exchange. */
|
|
121
|
-
bgp?: Bgp<P, G
|
|
154
|
+
bgp?: Bgp<P, G, NoInfer<T>>;
|
|
122
155
|
/** OSPF areas and interfaces. */
|
|
123
156
|
ospf?: Ospf<I>;
|
|
124
157
|
/** Validators answering route origin validation. */
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -103,20 +103,12 @@ class Cli {
|
|
|
103
103
|
return new Cli(configPath, network, options);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/**
|
|
107
|
-
* The config, a TypeScript file. Bun imports it as it is; Node needs jiti, which also resolves imports written without an extension. A linked port without a description is given one from its far end.
|
|
108
|
-
*/
|
|
106
|
+
/** The config, a TypeScript file. Bun imports it as it is; Node needs jiti, which also resolves imports written without an extension. */
|
|
109
107
|
private static async load(path: string): Promise<Network> {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (process.versions.bun) {
|
|
113
|
-
network = (await import(path)).default;
|
|
114
|
-
} else {
|
|
115
|
-
const { createJiti } = await import("jiti");
|
|
116
|
-
network = await createJiti(import.meta.url).import<Network>(path, { default: true });
|
|
117
|
-
}
|
|
108
|
+
if (process.versions.bun) return (await import(path)).default;
|
|
118
109
|
|
|
119
|
-
|
|
110
|
+
const { createJiti } = await import("jiti");
|
|
111
|
+
return createJiti(import.meta.url).import(path, { default: true });
|
|
120
112
|
}
|
|
121
113
|
|
|
122
114
|
private static fail(message: string): never {
|
|
@@ -238,7 +230,9 @@ class Cli {
|
|
|
238
230
|
.command("bgp")
|
|
239
231
|
.description("each BGP neighbor in the config next to its live session: up or down and why, prefixes, uptime")
|
|
240
232
|
.argument("[device]", "may be left out when only one device has BGP neighbors")
|
|
241
|
-
|
|
233
|
+
// A second argument was the name filter before 0.42; it is refused below with the way to write it now.
|
|
234
|
+
.allowExcessArguments()
|
|
235
|
+
.option("--neighbors <regex>", "only the neighbors whose name matches")
|
|
242
236
|
.option(
|
|
243
237
|
"--wait [count]",
|
|
244
238
|
`read again every ${BgpWait.COMMAND.every} seconds until every neighbor shown, or this many, is up`,
|
|
@@ -248,7 +242,14 @@ class Cli {
|
|
|
248
242
|
.option("--log [minutes]", "also print the device's BGP log lines from the last minutes, 5 by default", Cli.minutes)
|
|
249
243
|
.option("--clear-limit", "open again the neighbors shown that closed on their prefix limit; any other is left alone")
|
|
250
244
|
.option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP)
|
|
251
|
-
.action(async (
|
|
245
|
+
.action(async (device, flags, command) => {
|
|
246
|
+
const [, filter] = command.args;
|
|
247
|
+
if (filter !== undefined)
|
|
248
|
+
Cli.fail(
|
|
249
|
+
`circuit bgp takes one device; filter the neighbors with --neighbors: circuit bgp ${device} --neighbors '${filter}'`
|
|
250
|
+
);
|
|
251
|
+
(await Cli.open(flags.config)).bgp(device, flags);
|
|
252
|
+
});
|
|
252
253
|
|
|
253
254
|
program
|
|
254
255
|
.command("console")
|
|
@@ -613,14 +614,14 @@ class Cli {
|
|
|
613
614
|
}
|
|
614
615
|
|
|
615
616
|
/**
|
|
616
|
-
* Each BGP neighbor in the config next to its live session
|
|
617
|
+
* Each BGP neighbor in the config next to its live session, on the device named or the only one with BGP neighbors, and only those whose name matches `neighbors` when given. With `wait`, reads again until every neighbor shown, or that many, is up, and fails when time runs out. With `clearLimit`, opens again the neighbors shown that closed on their prefix limit, and fails when none did.
|
|
617
618
|
*/
|
|
618
619
|
private async bgp(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
flags: { wait?: true | number; max: number; log?: true | number; clearLimit?: true }
|
|
620
|
+
deviceName: string | undefined,
|
|
621
|
+
flags: { neighbors?: string; wait?: true | number; max: number; log?: true | number; clearLimit?: true }
|
|
622
622
|
): Promise<void> {
|
|
623
|
-
const
|
|
623
|
+
const device = this.bgpDevice(deviceName);
|
|
624
|
+
const pattern = flags.neighbors;
|
|
624
625
|
const adapter = this.adapterOf(device);
|
|
625
626
|
|
|
626
627
|
const supported = Platforms.all()
|
|
@@ -697,19 +698,15 @@ class Cli {
|
|
|
697
698
|
Cli.printBgp(await read());
|
|
698
699
|
}
|
|
699
700
|
|
|
700
|
-
/** The device `circuit bgp` reads
|
|
701
|
-
private
|
|
702
|
-
if (
|
|
703
|
-
return { device: this.one(first!), pattern: second };
|
|
704
|
-
}
|
|
701
|
+
/** The device `circuit bgp` reads: the one named, or the only device with BGP neighbors. */
|
|
702
|
+
private bgpDevice(name: string | undefined): Device {
|
|
703
|
+
if (name !== undefined) return this.one(name);
|
|
705
704
|
|
|
706
705
|
const withBgp = this.network.devices.filter((device) => Object.keys(device.routing?.bgp?.neighbors ?? {}).length);
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (withBgp.length === 1) return { device: withBgp[0]!, pattern: first };
|
|
710
|
-
if (!withBgp.length) Cli.fail(`${named}no device has BGP neighbors`);
|
|
706
|
+
if (withBgp.length === 1) return withBgp[0]!;
|
|
707
|
+
if (!withBgp.length) Cli.fail("no device has BGP neighbors");
|
|
711
708
|
|
|
712
|
-
return Cli.fail(
|
|
709
|
+
return Cli.fail(`several devices have BGP neighbors, so name one: ${withBgp.map((device) => device.name).join(", ")}`);
|
|
713
710
|
}
|
|
714
711
|
|
|
715
712
|
/** One line per neighbor: name, up or down, prefixes received, uptime, remote address. Then how many are up. */
|
package/src/core/cabling.ts
CHANGED
|
@@ -121,36 +121,6 @@ export class Cabling {
|
|
|
121
121
|
return findings;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
/**
|
|
125
|
-
* Each linked port without a description gets one from its far end: the far device and the far port as printed on it, such as `firewall-1 port6`. The far device's own file has its platform and model, so the label is never written twice. A port whose far device is not in the network keeps none.
|
|
126
|
-
*/
|
|
127
|
-
static describeLinks(devices: readonly Device[]): Device[] {
|
|
128
|
-
const byName = new Map(devices.map((device) => [device.name, device]));
|
|
129
|
-
|
|
130
|
-
return devices.map((device) => {
|
|
131
|
-
const ports = Object.entries(device.ports ?? {});
|
|
132
|
-
if (!ports.some(([, port]) => port?.link && port.description === undefined)) return device;
|
|
133
|
-
|
|
134
|
-
const described = ports.map(([name, port]) => {
|
|
135
|
-
const far = port?.link ? byName.get(port.link.device) : undefined;
|
|
136
|
-
if (!port?.link || port.description !== undefined || !far) return [name, port];
|
|
137
|
-
|
|
138
|
-
return [name, { ...port, description: `${far.name} ${Cabling.labelOf(far, port.link.port)}` }];
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
return { ...device, ports: Object.fromEntries(described) } as Device;
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** A port as printed on the device, or Circuit's name for it when the model has no spelling for it. */
|
|
146
|
-
private static labelOf(device: Device, port: string): string {
|
|
147
|
-
try {
|
|
148
|
-
return Catalog.spell(device.platform, device.model, port);
|
|
149
|
-
} catch {
|
|
150
|
-
return port;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
124
|
/** A system name is the device's name, alone or with a domain after it. */
|
|
155
125
|
private static isDevice(system: string, name: string): boolean {
|
|
156
126
|
return system === name || system.startsWith(`${name}.`);
|
package/src/core/define.ts
CHANGED
|
@@ -28,6 +28,7 @@ export function defineDevice<
|
|
|
28
28
|
const Users extends string = never,
|
|
29
29
|
const Vrfs extends string = never,
|
|
30
30
|
const Certificates extends string = never,
|
|
31
|
+
const Tables extends string = never,
|
|
31
32
|
>(
|
|
32
33
|
device: Device<
|
|
33
34
|
Platform,
|
|
@@ -42,7 +43,8 @@ export function defineDevice<
|
|
|
42
43
|
AddressSets,
|
|
43
44
|
Users,
|
|
44
45
|
Vrfs,
|
|
45
|
-
Certificates
|
|
46
|
+
Certificates,
|
|
47
|
+
Tables
|
|
46
48
|
>
|
|
47
49
|
): Device {
|
|
48
50
|
return device as unknown as Device;
|
package/src/core/validator.ts
CHANGED
|
@@ -291,17 +291,29 @@ export class Validator {
|
|
|
291
291
|
if (route.bfd && !route.via) report(`static route ${route.prefix} has bfd without via, the address BFD watches`);
|
|
292
292
|
names.expectInterface(`static route ${route.prefix} interface`, route.interface);
|
|
293
293
|
names.expect(`static route ${route.prefix} VRF`, route.vrf, route.vrf !== undefined && route.vrf in (device.vrfs ?? {}));
|
|
294
|
+
names.expect(
|
|
295
|
+
`static route ${route.prefix} table`,
|
|
296
|
+
route.table,
|
|
297
|
+
route.table !== undefined && route.table in (routing?.tables ?? {})
|
|
298
|
+
);
|
|
299
|
+
if (route.vrf && route.table) report(`static route ${route.prefix} has both vrf and table; a route is in one of them`);
|
|
294
300
|
}
|
|
295
301
|
|
|
302
|
+
Validator.checkTables(device, names, report, cidr);
|
|
303
|
+
|
|
296
304
|
const bgp = routing?.bgp;
|
|
297
305
|
const isPolicy = (policy: string | undefined) => policy !== undefined && policy in policies;
|
|
298
306
|
|
|
299
307
|
for (const prefix of bgp?.networks ?? []) cidr("BGP network", prefix);
|
|
300
308
|
|
|
309
|
+
const isTable = (table: string | undefined) => table !== undefined && table in (routing?.tables ?? {});
|
|
310
|
+
|
|
301
311
|
for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
|
|
302
312
|
for (const policy of [group.import, group.export]) {
|
|
303
313
|
names.expect(`BGP group ${name} policy`, policy, isPolicy(policy));
|
|
304
314
|
}
|
|
315
|
+
|
|
316
|
+
names.expect(`BGP group ${name} table`, group.table, isTable(group.table));
|
|
305
317
|
}
|
|
306
318
|
|
|
307
319
|
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
@@ -316,6 +328,8 @@ export class Validator {
|
|
|
316
328
|
names.expect(`neighbor ${name} policy`, policy, isPolicy(policy));
|
|
317
329
|
}
|
|
318
330
|
|
|
331
|
+
names.expect(`neighbor ${name} table`, neighbor.table, isTable(neighbor.table));
|
|
332
|
+
|
|
319
333
|
const local = neighbor.local_address ?? group?.local_address;
|
|
320
334
|
if (local && Address.family(local) !== Address.family(neighbor.address))
|
|
321
335
|
report(`neighbor ${name} local_address and address differ in family`);
|
|
@@ -327,17 +341,71 @@ export class Validator {
|
|
|
327
341
|
}
|
|
328
342
|
}
|
|
329
343
|
|
|
344
|
+
/** Routing tables have names of their own and something installing routes in them, and rules name a table and can each be reached. */
|
|
345
|
+
private static checkTables(device: Device, names: Names, report: Report, cidr: Checker): void {
|
|
346
|
+
const routing = device.routing;
|
|
347
|
+
const tables = routing?.tables ?? {};
|
|
348
|
+
const bgp = routing?.bgp;
|
|
349
|
+
|
|
350
|
+
const installed = new Set<string>();
|
|
351
|
+
for (const route of routing?.static ?? []) if (route.table) installed.add(route.table);
|
|
352
|
+
for (const neighbor of Object.values(bgp?.neighbors ?? {})) {
|
|
353
|
+
const table = neighbor.table ?? (neighbor.group ? bgp?.groups?.[neighbor.group]?.table : undefined);
|
|
354
|
+
if (table) installed.add(table);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const name of Object.keys(tables)) {
|
|
358
|
+
if (name === "main") report("routing.tables has main, the main table every device has; give the table another name");
|
|
359
|
+
if (name in (device.vrfs ?? {})) report(`routing.tables ${name} has the name of a VRF; give the table another name`);
|
|
360
|
+
if (name !== "main" && !installed.has(name))
|
|
361
|
+
report(
|
|
362
|
+
`routing.tables ${name}: nothing installs routes in it; give a static route or a BGP neighbor table: "${name}", or remove the table`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const rules = routing?.rules ?? [];
|
|
367
|
+
rules.forEach((rule, index) => {
|
|
368
|
+
const where = `routing.rules ${index + 1}${rule.description ? ` (${rule.description})` : ""}`;
|
|
369
|
+
const match = rule.match ?? {};
|
|
370
|
+
|
|
371
|
+
names.expect(`${where} table`, rule.table, rule.table === "main" || rule.table in tables);
|
|
372
|
+
names.expectInterface(`${where} in_interface`, match.in_interface);
|
|
373
|
+
if (match.src) cidr(where, match.src);
|
|
374
|
+
if (match.dst) cidr(where, match.dst);
|
|
375
|
+
|
|
376
|
+
if (match.src && match.dst && Address.isCidr(match.src) && Address.isCidr(match.dst)) {
|
|
377
|
+
if (Address.family(Address.host(match.src)) !== Address.family(Address.host(match.dst)))
|
|
378
|
+
report(`${where}: src and dst differ in family, so no packet matches both`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (rule.table === "main" && rule.fallback)
|
|
382
|
+
report(`${where} looks up main and has fallback; fallback is for another table, so remove it`);
|
|
383
|
+
|
|
384
|
+
const matchesAll = !match.src && !match.dst && !match.in_interface;
|
|
385
|
+
if (matchesAll && index < rules.length - 1)
|
|
386
|
+
report(`${where} matches every packet, so the rules after it never run; give it a match, or move it last`);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
330
390
|
private static checkPolicies(device: Device, names: Names, report: Report, cidr: Checker): void {
|
|
331
391
|
const policies = device.policies ?? {};
|
|
332
392
|
const sets = device.prefix_sets ?? {};
|
|
333
393
|
|
|
334
394
|
for (const [name, rules] of Object.entries(policies)) {
|
|
335
|
-
|
|
395
|
+
rules.forEach((rule, index) => {
|
|
396
|
+
// A device runs a call for every route, so a match beside it would read as a condition it isn't.
|
|
397
|
+
const beside = (["match", "set", "action"] as const).filter((key) => rule.call !== undefined && rule[key] !== undefined);
|
|
398
|
+
if (beside.length) {
|
|
399
|
+
report(
|
|
400
|
+
`policy ${name} rule ${index + 1} has call and ${beside.join(", ")}: a call is a rule of its own, run for every route. Put ${beside.join(", ")} in the next rule, or the match inside ${rule.call}`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
336
404
|
names.expect(`policy ${name} call`, rule.call, rule.call !== undefined && rule.call in policies);
|
|
337
405
|
const set = rule.match?.prefix_set;
|
|
338
406
|
names.expect(`policy ${name} prefix set`, set, set !== undefined && set in sets);
|
|
339
407
|
if (rule.match?.prefix) cidr(`policy ${name}`, rule.match.prefix);
|
|
340
|
-
}
|
|
408
|
+
});
|
|
341
409
|
}
|
|
342
410
|
|
|
343
411
|
// A policy that calls itself, directly or not, never finishes.
|