@takodotid/circuit 0.41.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 +121 -47
- package/dist/helpers/index.js +14 -9
- 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/cables.d.ts +14 -11
- package/dist/types/helpers/exchange.d.ts +7 -1
- package/dist/types/helpers/upstreams.d.ts +11 -3
- 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/interfaces.d.ts +1 -1
- 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 +60 -0
- package/src/helpers/cables.ts +20 -15
- package/src/helpers/exchange.ts +9 -3
- package/src/helpers/upstreams.ts +9 -4
- 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/interfaces.ts +1 -1
- 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
|
@@ -59,7 +59,7 @@ export type Upstream = {
|
|
|
59
59
|
address: IP;
|
|
60
60
|
local_address?: IP;
|
|
61
61
|
}>>;
|
|
62
|
-
/** A BGP group its sessions share, by this name: the group holds the AS, role, policies and multihop settings, and each session names it. Each session holds them itself when absent. */
|
|
62
|
+
/** A BGP group its sessions share, by this name: the group holds the AS, role, policies, table and multihop settings, and each session names it. Each session holds them itself when absent. */
|
|
63
63
|
group?: string;
|
|
64
64
|
/** The sessions are more than one hop away, from this address of ours. */
|
|
65
65
|
multihop?: {
|
|
@@ -67,6 +67,8 @@ export type Upstream = {
|
|
|
67
67
|
families?: readonly Family[];
|
|
68
68
|
password?: Secret;
|
|
69
69
|
};
|
|
70
|
+
/** The routing table its sessions install the routes they learn in, one of the router's `routing.tables`: a transit that carries only one block's traffic out. The main table when absent. */
|
|
71
|
+
table?: string;
|
|
70
72
|
/** The most prefixes a session may send, by its family: about twice what it sends. Over it, the session closes until it is cleared. */
|
|
71
73
|
limits?: {
|
|
72
74
|
ipv4?: number;
|
|
@@ -143,6 +145,12 @@ type GroupName<C> = Values<{
|
|
|
143
145
|
group: infer N extends string;
|
|
144
146
|
} ? N : never;
|
|
145
147
|
}>;
|
|
148
|
+
/** Each routing table name. */
|
|
149
|
+
type TableName<C> = Values<{
|
|
150
|
+
[K in keyof C]: C[K] extends {
|
|
151
|
+
table: infer N extends string;
|
|
152
|
+
} ? N : never;
|
|
153
|
+
}>;
|
|
146
154
|
/** A network's upstreams written once, and each device's part built from them. */
|
|
147
155
|
export declare class Upstreams {
|
|
148
156
|
/**
|
|
@@ -160,8 +168,8 @@ export declare class Upstreams {
|
|
|
160
168
|
ports: (device: string) => Record<string, Port<NativeName<C> | VlanName<C>, never, never, never>>;
|
|
161
169
|
interfaces: () => { [K in HandoffInterface<C>]: VlanInterface<VlanName<C>, never>; } & { [K in TunnelInterface<C>]: Gre<never>; };
|
|
162
170
|
uplinks: () => HandoffInterface<C>[];
|
|
163
|
-
groups: () => Record<GroupName<C>, NeighborSettings<PolicyName<C>>>;
|
|
164
|
-
neighbors: () => Record<string, Neighbor<PolicyName<C>, GroupName<C>>>;
|
|
171
|
+
groups: () => Record<GroupName<C>, NeighborSettings<PolicyName<C>, TableName<C>>>;
|
|
172
|
+
neighbors: () => Record<string, Neighbor<PolicyName<C>, GroupName<C>, TableName<C>>>;
|
|
165
173
|
policies: () => Record<PolicyName<C>, PolicyRule<never, never>[]>;
|
|
166
174
|
untrusted: () => (NativeName<C> | VlanName<C>)[];
|
|
167
175
|
natives: () => NativeName<C>[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Device } from "../schema/index.js";
|
|
2
2
|
/** A feature a platform renders once proved on one of its devices. */
|
|
3
|
-
export type ProvedFeature = "bfd-timers" | "static-route-bfd" | "multihop-bfd" | "alarm-severity" | "mac-limit" | "ethertype-filter" | "management-vdom";
|
|
3
|
+
export type ProvedFeature = "bfd-timers" | "static-route-bfd" | "multihop-bfd" | "alarm-severity" | "mac-limit" | "ethertype-filter" | "management-vdom" | "routing-tables";
|
|
4
4
|
export declare class Derive {
|
|
5
5
|
/** Each LAG's number: its own `id`, or its position among the device's LAGs. */
|
|
6
6
|
static lagIds(device: Device): Map<string, number>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Device, Family, Neighbor, NeighborSettings, PolicyRule } from "../../schema/index.js";
|
|
2
2
|
import { FortiOsBlock } from "./tree.js";
|
|
3
3
|
type AnyRule = PolicyRule<string, string>;
|
|
4
|
-
type AnyNeighbor = Neighbor<string, string>;
|
|
4
|
+
type AnyNeighbor = Neighbor<string, string, string>;
|
|
5
5
|
/** The lengths a prefix list's rule takes within its prefix. */
|
|
6
6
|
type Lengths = {
|
|
7
7
|
ge?: number;
|
|
@@ -23,7 +23,7 @@ export declare class FortiOsRouting {
|
|
|
23
23
|
readonly ipv6: 128;
|
|
24
24
|
};
|
|
25
25
|
/** A neighbor's settings with its group's beneath them. */
|
|
26
|
-
static neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string>;
|
|
26
|
+
static neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string, string>;
|
|
27
27
|
/** The families a session carries: its own, or the family of its address. */
|
|
28
28
|
static familiesOfNeighbor(device: Device, neighbor: AnyNeighbor): Family[];
|
|
29
29
|
/** Whether a static route, OSPF or BGP detects a dead neighbor with BFD, which the device then runs. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Device, Neighbor, NeighborSettings } from "../../schema/index.js";
|
|
2
2
|
import { type JunosStatements } from "./render.js";
|
|
3
|
-
type AnyNeighbor = Neighbor<string, string>;
|
|
3
|
+
type AnyNeighbor = Neighbor<string, string, string>;
|
|
4
4
|
/** BGP sessions, the policies they use, prefix sets and RPKI servers, added to one device's statements. */
|
|
5
5
|
export declare class JunosBgp {
|
|
6
6
|
private readonly out;
|
|
@@ -19,7 +19,7 @@ export declare class JunosBgp {
|
|
|
19
19
|
/** Add the device's prefix sets, RPKI servers, BGP sessions and the policies they use. */
|
|
20
20
|
static render(out: JunosStatements, device: Device): void;
|
|
21
21
|
/** A neighbor's settings with its group's beneath them. */
|
|
22
|
-
static settingsOf(device: Device, neighbor: AnyNeighbor): NeighborSettings<string>;
|
|
22
|
+
static settingsOf(device: Device, neighbor: AnyNeighbor): NeighborSettings<string, string>;
|
|
23
23
|
/** A name made of a value's words, for a community or a path-length list: `64500:*` becomes `64500-any`. */
|
|
24
24
|
private static slug;
|
|
25
25
|
/** A community as the platform writes a member: a large community carries the `large:` prefix. */
|
|
@@ -199,6 +199,10 @@ export declare class RouterOsRender {
|
|
|
199
199
|
/** Every switch rule the device's hardware ACLs make, in order: one for each port with an ACL, each rule of it, and each source and destination port or range of the rule. */
|
|
200
200
|
static switchRules(device: Device): SwitchRule[];
|
|
201
201
|
private routing;
|
|
202
|
+
/** A table beside the main one. `fib` puts its routes in the forwarding table, so traffic can be routed by them. */
|
|
203
|
+
private routingTables;
|
|
204
|
+
/** Rules in the order written. `lookup-only-in-table` drops what the table has no route for, and `lookup` falls back to the main table. */
|
|
205
|
+
private routingRules;
|
|
202
206
|
private staticRoutes;
|
|
203
207
|
private rpki;
|
|
204
208
|
/** The default instance exists on every device; it is stated in the form the export prints it. */
|
|
@@ -65,9 +65,9 @@ export type Vrf = {
|
|
|
65
65
|
/**
|
|
66
66
|
* Everything one device runs. What is not written here is removed from the device or returned to its default.
|
|
67
67
|
*
|
|
68
|
-
* Type parameters are the names the device declares, so a reference to a name that does not exist fails to compile: N ports, V VLANs, I interfaces, P policies, S prefix sets, G BGP groups, A ACLs, AS address sets, U users, F VRFs, C certificates.
|
|
68
|
+
* Type parameters are the names the device declares, so a reference to a name that does not exist fails to compile: N ports, V VLANs, I interfaces, P policies, S prefix sets, G BGP groups, A ACLs, AS address sets, U users, F VRFs, C certificates, T routing tables.
|
|
69
69
|
*/
|
|
70
|
-
export type Device<Pl extends string = string, M extends string = string, N extends string = string, V extends string = string, I extends string = string, P extends string = string, S extends string = string, G extends string = string, A extends string = string, AS extends string = string, U extends string = string, F extends string = string, C extends string = string> = {
|
|
70
|
+
export type Device<Pl extends string = string, M extends string = string, N extends string = string, V extends string = string, I extends string = string, P extends string = string, S extends string = string, G extends string = string, A extends string = string, AS extends string = string, U extends string = string, F extends string = string, C extends string = string, T extends string = string> = {
|
|
71
71
|
/** Hostname. */
|
|
72
72
|
name: string;
|
|
73
73
|
/** Operating system. It decides which adapter renders and applies the device. */
|
|
@@ -92,8 +92,8 @@ export type Device<Pl extends string = string, M extends string = string, N exte
|
|
|
92
92
|
interfaces?: Record<I, Interface<NoInfer<V>, NoInfer<F>>>;
|
|
93
93
|
/** Separate routing tables, keyed by name. */
|
|
94
94
|
vrfs?: Record<F, Vrf>;
|
|
95
|
-
/**
|
|
96
|
-
routing?: Routing<NoInfer<P>, G, NoInfer<I | N>, NoInfer<F
|
|
95
|
+
/** Routing tables and rules, static routes, BGP, OSPF and route validation. */
|
|
96
|
+
routing?: Routing<NoInfer<P>, G, NoInfer<I | N>, NoInfer<F>, T>;
|
|
97
97
|
/** Route policies, keyed by name. */
|
|
98
98
|
policies?: Record<P, RoutePolicy<NoInfer<P>, NoInfer<S>>>;
|
|
99
99
|
/** Prefix sets that policies match on, keyed by name. */
|
|
@@ -109,7 +109,7 @@ export type Port<V extends string, A extends string, L extends string, F extends
|
|
|
109
109
|
acl?: A;
|
|
110
110
|
/** The LAG this port is in. Every other setting goes on the LAG. */
|
|
111
111
|
lag?: L;
|
|
112
|
-
/** Another of our devices at the other end of the cable.
|
|
112
|
+
/** Another of our devices at the other end of the cable. */
|
|
113
113
|
link?: Link;
|
|
114
114
|
};
|
|
115
115
|
/** Settings every interface that is not a physical port shares. */
|
|
@@ -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,6 +341,52 @@ 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 ?? {};
|
package/src/helpers/cables.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { Catalog } from "../platforms/catalog";
|
|
2
|
+
|
|
3
|
+
/** The far end of a port, for a description of your own: the device, its port by Circuit's name, and the port as printed on the device when that end names its platform and model. */
|
|
4
|
+
export type FarEnd = { device: string; port: string; label?: string };
|
|
5
|
+
|
|
6
|
+
/** One end of a cable entry: the device, and its platform and model if a description should name its ports as printed on it. */
|
|
7
|
+
type End = { device: string; platform?: string; model?: string };
|
|
3
8
|
|
|
4
9
|
/** What every cable of one entry shares. */
|
|
5
10
|
type Shared = {
|
|
6
11
|
/** What is between the two ends, such as a carrier's local loop. Both ends get it on their `link`. */
|
|
7
12
|
through?: string;
|
|
8
|
-
/** A port's description from its far end
|
|
13
|
+
/** A port's description from its far end, such as `(far) => \`${far.device} ${far.label}\``. No description when absent. */
|
|
9
14
|
describe?: (far: FarEnd) => string;
|
|
10
15
|
};
|
|
11
16
|
|
|
@@ -26,12 +31,10 @@ export class Cables {
|
|
|
26
31
|
* // router-1: ports: { ...uplink.a }
|
|
27
32
|
* ```
|
|
28
33
|
*/
|
|
29
|
-
static link<const NA extends string, const NB extends string>(
|
|
30
|
-
spec: Shared & { a: { device: string; port: NA }; b: { device: string; port: NB } }
|
|
31
|
-
) {
|
|
34
|
+
static link<const NA extends string, const NB extends string>(spec: Shared & { a: End & { port: NA }; b: End & { port: NB } }) {
|
|
32
35
|
return {
|
|
33
|
-
a: { [spec.a.port]: Cables.end(spec, spec.b
|
|
34
|
-
b: { [spec.b.port]: Cables.end(spec, spec.a
|
|
36
|
+
a: { [spec.a.port]: Cables.end(spec, spec.b, spec.b.port) } as Record<NA, CablePort<never>>,
|
|
37
|
+
b: { [spec.b.port]: Cables.end(spec, spec.a, spec.a.port) } as Record<NB, CablePort<never>>,
|
|
35
38
|
};
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -39,7 +42,7 @@ export class Cables {
|
|
|
39
42
|
* The cables of a LAG, both ends, from one list read from end A: `{ "25g-1": "10g-2" }` is a cable from A's 25g-1 to B's 10g-2. Each port is a member of its end's LAG. Spread each end into its device's `ports`; the LAG itself stays on each device.
|
|
40
43
|
*/
|
|
41
44
|
static lag<const LA extends string, const LB extends string, const C extends Readonly<Record<string, string>>>(
|
|
42
|
-
spec: Shared & { a:
|
|
45
|
+
spec: Shared & { a: End & { lag: LA }; b: End & { lag: LB }; cables: C }
|
|
43
46
|
) {
|
|
44
47
|
const pairs = Object.entries(spec.cables);
|
|
45
48
|
|
|
@@ -50,8 +53,8 @@ export class Cables {
|
|
|
50
53
|
taken.set(far, near);
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b
|
|
54
|
-
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a
|
|
56
|
+
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b, far), lag: spec.a.lag }]));
|
|
57
|
+
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a, near), lag: spec.b.lag }]));
|
|
55
58
|
|
|
56
59
|
return {
|
|
57
60
|
a: a as Record<keyof C & string, CablePort<LA>>,
|
|
@@ -59,11 +62,13 @@ export class Cables {
|
|
|
59
62
|
};
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
/** A port facing `port` on the far
|
|
63
|
-
private static end(shared: Shared,
|
|
65
|
+
/** A port facing `port` on the far end. */
|
|
66
|
+
private static end(shared: Shared, far: End, port: string): CablePort<never> {
|
|
67
|
+
const label = far.platform && far.model ? Catalog.spell(far.platform, far.model, port) : undefined;
|
|
68
|
+
|
|
64
69
|
return {
|
|
65
|
-
link: { device, port, ...(shared.through ? { through: shared.through } : {}) },
|
|
66
|
-
...(shared.describe ? { description: shared.describe({ device, port }) } : {}),
|
|
70
|
+
link: { device: far.device, port, ...(shared.through ? { through: shared.through } : {}) },
|
|
71
|
+
...(shared.describe ? { description: shared.describe({ device: far.device, port, ...(label ? { label } : {}) }) } : {}),
|
|
67
72
|
};
|
|
68
73
|
}
|
|
69
74
|
}
|
package/src/helpers/exchange.ts
CHANGED
|
@@ -30,6 +30,10 @@ export type InternetExchange<V extends string> = {
|
|
|
30
30
|
mac_limit?: number;
|
|
31
31
|
/** The only kinds of frame a member port takes in, usually `ipv4`, `ipv6` and `arp`. Every kind when absent. No platform renders it yet, so `validate` refuses it. */
|
|
32
32
|
ethertypes?: readonly Ethertype[];
|
|
33
|
+
/** LLDP on member ports. Off when absent: a member's router has no business learning the fabric. */
|
|
34
|
+
lldp?: boolean;
|
|
35
|
+
/** Spanning tree on member ports. Off when absent: a member's port faces a router, which must not join the fabric's spanning tree. */
|
|
36
|
+
stp?: boolean;
|
|
33
37
|
};
|
|
34
38
|
|
|
35
39
|
/** An internet exchange's fabric: its member ports, a check of its members, and its route servers' config. */
|
|
@@ -37,7 +41,9 @@ export class Exchange {
|
|
|
37
41
|
/**
|
|
38
42
|
* Member ports and a check for an internet exchange's fabric.
|
|
39
43
|
*
|
|
40
|
-
* - `ports(device)`: every member's port on one switch
|
|
44
|
+
* - `ports(device)`: every member's port on one switch, each written as
|
|
45
|
+
* `{ description: "<name>, AS<asn>", access_vlan: vlan, stp: false, lldp: false, storm_control: { broadcast, multicast, unknown_unicast: { percent: storm_percent } } }`,
|
|
46
|
+
* plus `mac_limit` and `ethertypes` when the exchange sets them. `stp` and `lldp` are the exchange's own when it sets them, and storm control is 1 % when `storm_percent` is absent.
|
|
41
47
|
* - `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.
|
|
42
48
|
* - `bird(server)`: one route server's BIRD 2 config, with a session to every member. Write it to a file for the server; Circuit never logs in to a route server.
|
|
43
49
|
* - `openbgpd(server)`: the same route server's config for OpenBGPD, its `bgpd.conf`, with the same sessions and the same rules.
|
|
@@ -55,8 +61,8 @@ export class Exchange {
|
|
|
55
61
|
result[port] = {
|
|
56
62
|
description: `${member.name}, AS${member.asn}`,
|
|
57
63
|
access_vlan: exchange.vlan,
|
|
58
|
-
stp: false,
|
|
59
|
-
lldp: false,
|
|
64
|
+
stp: exchange.stp ?? false,
|
|
65
|
+
lldp: exchange.lldp ?? false,
|
|
60
66
|
storm_control: { broadcast: { percent }, multicast: { percent }, unknown_unicast: { percent } },
|
|
61
67
|
...(exchange.mac_limit !== undefined ? { mac_limit: exchange.mac_limit } : {}),
|
|
62
68
|
...(exchange.ethertypes ? { ethertypes: exchange.ethertypes } : {}),
|