@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
package/src/helpers/upstreams.ts
CHANGED
|
@@ -63,10 +63,12 @@ export type Upstream = {
|
|
|
63
63
|
tunnel?: { interface: string; local: IP; remote: IP; addresses: readonly InterfaceAddress[]; mtu?: number };
|
|
64
64
|
/** Our BGP sessions with it, by name: its address, and ours when it is not the interface's. */
|
|
65
65
|
sessions: Readonly<Record<string, { address: IP; local_address?: IP }>>;
|
|
66
|
-
/** 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. */
|
|
66
|
+
/** 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. */
|
|
67
67
|
group?: string;
|
|
68
68
|
/** The sessions are more than one hop away, from this address of ours. */
|
|
69
69
|
multihop?: { local_address: IP; families?: readonly Family[]; password?: Secret };
|
|
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?: { ipv4?: number; ipv6?: number };
|
|
72
74
|
/** What we take, as its policy. Nothing when absent. */
|
|
@@ -105,6 +107,8 @@ type PolicyName<C> = Values<{
|
|
|
105
107
|
}>;
|
|
106
108
|
/** Each BGP group name. */
|
|
107
109
|
type GroupName<C> = Values<{ [K in keyof C]: C[K] extends { group: infer N extends string } ? N : never }>;
|
|
110
|
+
/** Each routing table name. */
|
|
111
|
+
type TableName<C> = Values<{ [K in keyof C]: C[K] extends { table: infer N extends string } ? N : never }>;
|
|
108
112
|
|
|
109
113
|
/** A network's upstreams written once, and each device's part built from them. */
|
|
110
114
|
export class Upstreams {
|
|
@@ -217,7 +221,7 @@ export class Upstreams {
|
|
|
217
221
|
const uplinks = () => all.flatMap(([, upstream]) => (upstream.handoff ? [upstream.handoff.interface as HandoffInterface<C>] : []));
|
|
218
222
|
|
|
219
223
|
/** What a session, or the group it is in, takes from its upstream. */
|
|
220
|
-
const settings = (key: string, upstream: Upstream): NeighborSettings<PolicyName<C>> => {
|
|
224
|
+
const settings = (key: string, upstream: Upstream): NeighborSettings<PolicyName<C>, TableName<C>> => {
|
|
221
225
|
const multihop = upstream.multihop;
|
|
222
226
|
return {
|
|
223
227
|
remote_as: upstream.asn,
|
|
@@ -225,6 +229,7 @@ export class Upstreams {
|
|
|
225
229
|
role: upstream.role,
|
|
226
230
|
...(upstream.import ? { import: upstream.import.name as PolicyName<C> } : {}),
|
|
227
231
|
export: upstream.export.name as PolicyName<C>,
|
|
232
|
+
...(upstream.table ? { table: upstream.table as TableName<C> } : {}),
|
|
228
233
|
...(multihop
|
|
229
234
|
? {
|
|
230
235
|
local_address: multihop.local_address,
|
|
@@ -239,7 +244,7 @@ export class Upstreams {
|
|
|
239
244
|
const groups = () =>
|
|
240
245
|
Object.fromEntries(
|
|
241
246
|
all.flatMap(([key, upstream]) => (upstream.group ? [[upstream.group, settings(key, upstream)]] : []))
|
|
242
|
-
) as Record<GroupName<C>, NeighborSettings<PolicyName<C>>>;
|
|
247
|
+
) as Record<GroupName<C>, NeighborSettings<PolicyName<C>, TableName<C>>>;
|
|
243
248
|
|
|
244
249
|
const neighbors = () =>
|
|
245
250
|
Object.fromEntries(
|
|
@@ -256,7 +261,7 @@ export class Upstreams {
|
|
|
256
261
|
];
|
|
257
262
|
})
|
|
258
263
|
)
|
|
259
|
-
) as Record<string, Neighbor<PolicyName<C>, GroupName<C>>>;
|
|
264
|
+
) as Record<string, Neighbor<PolicyName<C>, GroupName<C>, TableName<C>>>;
|
|
260
265
|
|
|
261
266
|
/**
|
|
262
267
|
* Each upstream's import and export, by name. The policies and prefix sets their rules name are checked by `validate`, not by the editor, so the rules are typed to fit any device.
|
package/src/platforms/derive.ts
CHANGED
|
@@ -5,7 +5,14 @@ import type { Device } from "../schema";
|
|
|
5
5
|
|
|
6
6
|
/** A feature a platform renders once proved on one of its devices. */
|
|
7
7
|
export type ProvedFeature =
|
|
8
|
-
|
|
8
|
+
| "bfd-timers"
|
|
9
|
+
| "static-route-bfd"
|
|
10
|
+
| "multihop-bfd"
|
|
11
|
+
| "alarm-severity"
|
|
12
|
+
| "mac-limit"
|
|
13
|
+
| "ethertype-filter"
|
|
14
|
+
| "management-vdom"
|
|
15
|
+
| "routing-tables";
|
|
9
16
|
|
|
10
17
|
export class Derive {
|
|
11
18
|
/** Each LAG's number: its own `id`, or its position among the device's LAGs. */
|
|
@@ -47,11 +54,26 @@ export class Derive {
|
|
|
47
54
|
for (const route of routing?.static ?? []) {
|
|
48
55
|
if (route.bfd && !has("static-route-bfd"))
|
|
49
56
|
found.push(`routing.static ${route.prefix} bfd, BFD on a static route is ${unproved}`);
|
|
57
|
+
if (route.table && !has("routing-tables"))
|
|
58
|
+
found.push(`routing.static ${route.prefix} table, a routing table beside the main one is ${unproved}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!has("routing-tables")) {
|
|
62
|
+
if (routing?.tables) found.push(`routing.tables, a routing table beside the main one is ${unproved}`);
|
|
63
|
+
if (routing?.rules) found.push(`routing.rules, routing rules are ${unproved}`);
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
const bgp = routing?.bgp;
|
|
53
67
|
|
|
68
|
+
for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
|
|
69
|
+
if (group.table && !has("routing-tables"))
|
|
70
|
+
found.push(`routing.bgp.groups.${name}.table, a routing table beside the main one is ${unproved}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
54
73
|
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
74
|
+
if (neighbor.table && !has("routing-tables"))
|
|
75
|
+
found.push(`routing.bgp.neighbors.${name}.table, a routing table beside the main one is ${unproved}`);
|
|
76
|
+
|
|
55
77
|
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
56
78
|
const bfd = neighbor.bfd ?? group?.bfd;
|
|
57
79
|
const multihop = neighbor.multihop ?? group?.multihop;
|
|
@@ -7,7 +7,7 @@ import { FortiOsNames } from "./names";
|
|
|
7
7
|
import { FortiOsBlock, FortiOsTree } from "./tree";
|
|
8
8
|
|
|
9
9
|
type AnyRule = PolicyRule<string, string>;
|
|
10
|
-
type AnyNeighbor = Neighbor<string, string>;
|
|
10
|
+
type AnyNeighbor = Neighbor<string, string, string>;
|
|
11
11
|
|
|
12
12
|
/** The lengths a prefix list's rule takes within its prefix. */
|
|
13
13
|
type Lengths = { ge?: number; le?: number };
|
|
@@ -167,7 +167,7 @@ export class FortiOsRouting {
|
|
|
167
167
|
static readonly BITS = { ipv4: 32, ipv6: 128 } as const;
|
|
168
168
|
|
|
169
169
|
/** A neighbor's settings with its group's beneath them. */
|
|
170
|
-
static neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string> {
|
|
170
|
+
static neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string, string> {
|
|
171
171
|
const group = neighbor.group ? device.routing?.bgp?.groups?.[neighbor.group] : undefined;
|
|
172
172
|
return { ...group, ...neighbor };
|
|
173
173
|
}
|
|
@@ -6,7 +6,7 @@ import type { Device, Family, Neighbor, NeighborSettings, PolicyRule } from "../
|
|
|
6
6
|
import { JunosRender, type JunosStatements } from "./render";
|
|
7
7
|
|
|
8
8
|
type AnyRule = PolicyRule<string, string>;
|
|
9
|
-
type AnyNeighbor = Neighbor<string, string>;
|
|
9
|
+
type AnyNeighbor = Neighbor<string, string, string>;
|
|
10
10
|
|
|
11
11
|
/** BGP sessions, the policies they use, prefix sets and RPKI servers, added to one device's statements. */
|
|
12
12
|
export class JunosBgp {
|
|
@@ -41,7 +41,7 @@ export class JunosBgp {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** A neighbor's settings with its group's beneath them. */
|
|
44
|
-
static settingsOf(device: Device, neighbor: AnyNeighbor): NeighborSettings<string> {
|
|
44
|
+
static settingsOf(device: Device, neighbor: AnyNeighbor): NeighborSettings<string, string> {
|
|
45
45
|
const group = neighbor.group ? device.routing?.bgp?.groups?.[neighbor.group] : undefined;
|
|
46
46
|
return { ...group, ...neighbor };
|
|
47
47
|
}
|
|
@@ -63,3 +63,7 @@ Behaviour that looks like something else. Each one has been hit on a real device
|
|
|
63
63
|
- `use-bfd=no`, set on a connection that had `use-bfd=yes`, is printed by the export; it is the default, and the plan sets it to take BFD off.
|
|
64
64
|
- No MAC learning limit on 7.23.5: `/interface bridge port` has `learn` but no limit, and `/interface ethernet switch port` has none (CCR2216). `mac_limit` stays refused.
|
|
65
65
|
- A switch rule matches a kind of frame with `mac-protocol` (`ip`, `ipv6`, `arp` among 23 kinds), but cannot match "not" one. An ethertype filter is a rule that lets each kind through and a last one with an empty `new-dst-ports` for the rest, after the port's ACL rules, which match only `ip` or `ipv6`. Four such rules on a free port took the router to 70 switch rules, none invalid (CCR2216, 7.23.5).
|
|
66
|
+
- A routing table holds no routes the router forwards by unless it has `fib`: `/routing table add fib name=...`. The export prints the flag between the properties, `add comment=... fib name=...`.
|
|
67
|
+
- A BGP connection's `routing-table` installs what it learns in that table and not in main; the next hop still resolves through main. Proved on a CCR2216 on 7.23.5 with a session to a test speaker: its routes showed `routing-table=<table>`, active, and main kept only its own static route. The export prints `routing-table` only when it is not `main`.
|
|
68
|
+
- `/routing rule` is an ordered list without names, like a filter chain, and the plan rewrites it whole in the config's order. `lookup-only-in-table` drops a packet its table has no route for (a ping printed `no route`); `lookup` routes it by main instead (the ping left). An interface to match is `interface=`. A `drop` or `unreachable` rule is printed with `!realm`, which Circuit does not write (CCR2216, 7.23.5).
|
|
69
|
+
- An eBGP connection to an address on no connected network, without `multihop=yes`, takes the TCP connection, logs `connection <local> from <remote>`, and closes it with no OPEN and nothing else in the log, even with `bgp,debug` logging on (CCR2216, 7.23.5).
|
|
@@ -137,6 +137,7 @@ export class RouterOsExport {
|
|
|
137
137
|
/** Menus whose entries are an ordered list without names. They are rewritten whole, in the order the render gives. */
|
|
138
138
|
static readonly ORDERED = new Set([
|
|
139
139
|
"/routing filter rule",
|
|
140
|
+
"/routing rule",
|
|
140
141
|
"/ip firewall filter",
|
|
141
142
|
"/ipv6 firewall filter",
|
|
142
143
|
"/ip firewall nat",
|
|
@@ -207,7 +207,16 @@ export class RouterOs extends Platform {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
// BFD on a static route and a MAC limit are refused above, for the reason the device gave, and an ethertype filter on a LAG as unproved.
|
|
210
|
-
found.push(
|
|
210
|
+
found.push(
|
|
211
|
+
...Derive.unproved(device, [
|
|
212
|
+
"bfd-timers",
|
|
213
|
+
"multihop-bfd",
|
|
214
|
+
"static-route-bfd",
|
|
215
|
+
"mac-limit",
|
|
216
|
+
"ethertype-filter",
|
|
217
|
+
"routing-tables",
|
|
218
|
+
])
|
|
219
|
+
);
|
|
211
220
|
|
|
212
221
|
return found;
|
|
213
222
|
}
|
|
@@ -549,7 +558,7 @@ export class RouterOs extends Platform {
|
|
|
549
558
|
|
|
550
559
|
return down.map((bgp) =>
|
|
551
560
|
bgp.limit
|
|
552
|
-
? `BGP session ${bgp.neighbor} was established before the apply and closed on its limit${bgp.limit.max !== undefined ? ` of ${bgp.limit.max} prefixes` : ""}: the neighbor sends more than it may. Find out why, then: circuit bgp ${device.name}
|
|
561
|
+
? `BGP session ${bgp.neighbor} was established before the apply and closed on its limit${bgp.limit.max !== undefined ? ` of ${bgp.limit.max} prefixes` : ""}: the neighbor sends more than it may. Find out why, then: circuit bgp ${device.name} --neighbors '^${bgp.neighbor}$' --clear-limit`
|
|
553
562
|
: `BGP session ${bgp.neighbor} was established before the apply and is not ${limits.seconds} s after it (${bgp.reason ?? (bgp.uptime === "no session" ? "no session" : "not established")}). Look with: circuit bgp ${device.name} --log`
|
|
554
563
|
);
|
|
555
564
|
}
|
|
@@ -30,6 +30,9 @@ export class RouterOsPlan {
|
|
|
30
30
|
"/interface bridge filter",
|
|
31
31
|
"/interface list",
|
|
32
32
|
"/ip vrf",
|
|
33
|
+
// Routes, rules and BGP connections name a table, so it exists first.
|
|
34
|
+
"/routing table",
|
|
35
|
+
"/routing rule",
|
|
33
36
|
"/ip address",
|
|
34
37
|
"/ipv6 address",
|
|
35
38
|
"/ip pool",
|
|
@@ -32,7 +32,7 @@ export type AnyInterface = Interface<string, string>;
|
|
|
32
32
|
|
|
33
33
|
type Speed = NonNullable<AnyPort["speed"]>;
|
|
34
34
|
type AnyRule = PolicyRule<string, string>;
|
|
35
|
-
type AnyNeighbor = Neighbor<string, string>;
|
|
35
|
+
type AnyNeighbor = Neighbor<string, string, string>;
|
|
36
36
|
|
|
37
37
|
/** One device's render: the device, and the commands collected under their menu. */
|
|
38
38
|
export class RouterOsRender {
|
|
@@ -1101,6 +1101,8 @@ export class RouterOsRender {
|
|
|
1101
1101
|
}
|
|
1102
1102
|
|
|
1103
1103
|
private routing(): void {
|
|
1104
|
+
this.routingTables();
|
|
1105
|
+
this.routingRules();
|
|
1104
1106
|
this.staticRoutes();
|
|
1105
1107
|
this.rpki();
|
|
1106
1108
|
if (this.device.routing?.bgp) {
|
|
@@ -1113,6 +1115,28 @@ export class RouterOsRender {
|
|
|
1113
1115
|
this.bfd();
|
|
1114
1116
|
}
|
|
1115
1117
|
|
|
1118
|
+
/** A table beside the main one. `fib` puts its routes in the forwarding table, so traffic can be routed by them. */
|
|
1119
|
+
private routingTables(): void {
|
|
1120
|
+
for (const [name, table] of Object.entries(this.device.routing?.tables ?? {})) {
|
|
1121
|
+
this.add("/routing table", "add fib", { comment: table.description, name });
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/** 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. */
|
|
1126
|
+
private routingRules(): void {
|
|
1127
|
+
for (const rule of this.device.routing?.rules ?? []) {
|
|
1128
|
+
const onlyInTable = rule.table !== "main" && (rule.fallback ?? "none") === "none";
|
|
1129
|
+
this.add("/routing rule", "add", {
|
|
1130
|
+
action: onlyInTable ? "lookup-only-in-table" : "lookup",
|
|
1131
|
+
comment: rule.description,
|
|
1132
|
+
"dst-address": rule.match?.dst,
|
|
1133
|
+
interface: rule.match?.in_interface && this.name(rule.match.in_interface),
|
|
1134
|
+
"src-address": rule.match?.src,
|
|
1135
|
+
table: rule.table,
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1116
1140
|
private staticRoutes(): void {
|
|
1117
1141
|
for (const route of this.device.routing?.static ?? []) {
|
|
1118
1142
|
const menu = Address.family(Address.host(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
|
|
@@ -1123,7 +1147,7 @@ export class RouterOsRender {
|
|
|
1123
1147
|
distance: route.distance,
|
|
1124
1148
|
"dst-address": route.prefix,
|
|
1125
1149
|
gateway,
|
|
1126
|
-
"routing-table": route.vrf,
|
|
1150
|
+
"routing-table": route.vrf ?? route.table,
|
|
1127
1151
|
});
|
|
1128
1152
|
}
|
|
1129
1153
|
}
|
|
@@ -1146,7 +1170,7 @@ export class RouterOsRender {
|
|
|
1146
1170
|
}
|
|
1147
1171
|
|
|
1148
1172
|
/** A neighbor's settings with its group's beneath them. */
|
|
1149
|
-
private settingsOf(neighbor: AnyNeighbor): NeighborSettings<string> {
|
|
1173
|
+
private settingsOf(neighbor: AnyNeighbor): NeighborSettings<string, string> {
|
|
1150
1174
|
const group = neighbor.group ? this.device.routing?.bgp?.groups?.[neighbor.group] : undefined;
|
|
1151
1175
|
return { ...group, ...neighbor };
|
|
1152
1176
|
}
|
|
@@ -1268,6 +1292,7 @@ export class RouterOsRender {
|
|
|
1268
1292
|
"output.network": originates ? RouterOsRender.NETWORKS[family] : undefined,
|
|
1269
1293
|
"remote.address": `${neighbor.address}/${addressFamily === "ipv4" ? 32 : 128}`,
|
|
1270
1294
|
"remote.as": settings.remote_as,
|
|
1295
|
+
"routing-table": settings.table,
|
|
1271
1296
|
"tcp-md5-key": settings.password && Secrets.marker(settings.password),
|
|
1272
1297
|
"use-bfd": settings.bfd ? "yes" : undefined,
|
|
1273
1298
|
});
|
package/src/schema/device.ts
CHANGED
|
@@ -71,7 +71,7 @@ export type Vrf = {
|
|
|
71
71
|
/**
|
|
72
72
|
* Everything one device runs. What is not written here is removed from the device or returned to its default.
|
|
73
73
|
*
|
|
74
|
-
* 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.
|
|
74
|
+
* 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.
|
|
75
75
|
*/
|
|
76
76
|
export type Device<
|
|
77
77
|
Pl extends string = string,
|
|
@@ -87,6 +87,7 @@ export type Device<
|
|
|
87
87
|
U extends string = string,
|
|
88
88
|
F extends string = string,
|
|
89
89
|
C extends string = string,
|
|
90
|
+
T extends string = string,
|
|
90
91
|
> = {
|
|
91
92
|
/** Hostname. */
|
|
92
93
|
name: string;
|
|
@@ -112,8 +113,8 @@ export type Device<
|
|
|
112
113
|
interfaces?: Record<I, Interface<NoInfer<V>, NoInfer<F>>>;
|
|
113
114
|
/** Separate routing tables, keyed by name. */
|
|
114
115
|
vrfs?: Record<F, Vrf>;
|
|
115
|
-
/**
|
|
116
|
-
routing?: Routing<NoInfer<P>, G, NoInfer<I | N>, NoInfer<F
|
|
116
|
+
/** Routing tables and rules, static routes, BGP, OSPF and route validation. */
|
|
117
|
+
routing?: Routing<NoInfer<P>, G, NoInfer<I | N>, NoInfer<F>, T>;
|
|
117
118
|
/** Route policies, keyed by name. */
|
|
118
119
|
policies?: Record<P, RoutePolicy<NoInfer<P>, NoInfer<S>>>;
|
|
119
120
|
/** Prefix sets that policies match on, keyed by name. */
|
package/src/schema/interfaces.ts
CHANGED
|
@@ -113,7 +113,7 @@ export type Port<V extends string, A extends string, L extends string, F extends
|
|
|
113
113
|
acl?: A;
|
|
114
114
|
/** The LAG this port is in. Every other setting goes on the LAG. */
|
|
115
115
|
lag?: L;
|
|
116
|
-
/** Another of our devices at the other end of the cable.
|
|
116
|
+
/** Another of our devices at the other end of the cable. */
|
|
117
117
|
link?: Link;
|
|
118
118
|
};
|
|
119
119
|
|
package/src/schema/routing.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Family, IP, Prefix, Secret } from "./common";
|
|
2
2
|
|
|
3
3
|
/** A route the device has without learning it. */
|
|
4
|
-
export type StaticRoute<I extends string, F extends string> = {
|
|
4
|
+
export type StaticRoute<I extends string, F extends string, T extends string = never> = {
|
|
5
5
|
/** Destination. */
|
|
6
6
|
prefix: Prefix;
|
|
7
7
|
/** Next-hop address. */
|
|
@@ -14,8 +14,10 @@ export type StaticRoute<I extends string, F extends string> = {
|
|
|
14
14
|
bfd?: true;
|
|
15
15
|
/** Preference against other routes to the same prefix. Lower wins. 1 when absent. */
|
|
16
16
|
distance?: number;
|
|
17
|
-
/** The VRF the route is in. The
|
|
17
|
+
/** The VRF the route is in. The main table when absent. */
|
|
18
18
|
vrf?: F;
|
|
19
|
+
/** 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. */
|
|
20
|
+
table?: T;
|
|
19
21
|
/** Why the route exists. */
|
|
20
22
|
description?: string;
|
|
21
23
|
};
|
|
@@ -26,7 +28,7 @@ export type StaticRoute<I extends string, F extends string> = {
|
|
|
26
28
|
export type BgpRole = "provider" | "customer" | "peer" | "rs" | "rs-client";
|
|
27
29
|
|
|
28
30
|
/** Settings a BGP neighbor can set itself or take from its group. */
|
|
29
|
-
export type NeighborSettings<P extends string> = {
|
|
31
|
+
export type NeighborSettings<P extends string, T extends string = never> = {
|
|
30
32
|
/** The neighbor's AS. Our own AS makes the session internal. */
|
|
31
33
|
remote_as?: number;
|
|
32
34
|
/** Who the neighbor is. */
|
|
@@ -55,10 +57,12 @@ export type NeighborSettings<P extends string> = {
|
|
|
55
57
|
passive?: boolean;
|
|
56
58
|
/** Detect a dead link in under a second with BFD. */
|
|
57
59
|
bfd?: boolean;
|
|
60
|
+
/** 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. */
|
|
61
|
+
table?: T;
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
/** One BGP session. Settings it does not set come from its group. */
|
|
61
|
-
export type Neighbor<P extends string, G extends string> = NeighborSettings<P> & {
|
|
65
|
+
export type Neighbor<P extends string, G extends string, T extends string = never> = NeighborSettings<P, T> & {
|
|
62
66
|
/** The neighbor's address. */
|
|
63
67
|
address: IP;
|
|
64
68
|
/** The group this neighbor takes a setting from when it does not set that setting itself. */
|
|
@@ -66,15 +70,15 @@ export type Neighbor<P extends string, G extends string> = NeighborSettings<P> &
|
|
|
66
70
|
};
|
|
67
71
|
|
|
68
72
|
/** BGP on this device. */
|
|
69
|
-
export type Bgp<P extends string, G extends string> = {
|
|
73
|
+
export type Bgp<P extends string, G extends string, T extends string = never> = {
|
|
70
74
|
/** Our AS number. */
|
|
71
75
|
asn: number;
|
|
72
76
|
/** Prefixes this device originates. Each needs a route, for example a static blackhole. Export policy still decides who receives them. */
|
|
73
77
|
networks?: readonly Prefix[];
|
|
74
78
|
/** Settings shared by several neighbors, keyed by group name. */
|
|
75
|
-
groups?: Record<G, NeighborSettings<P>>;
|
|
79
|
+
groups?: Record<G, NeighborSettings<P, T>>;
|
|
76
80
|
/** Sessions keyed by name. */
|
|
77
|
-
neighbors?: Record<string, Neighbor<P, G>>;
|
|
81
|
+
neighbors?: Record<string, Neighbor<P, G, T>>;
|
|
78
82
|
};
|
|
79
83
|
|
|
80
84
|
/** How fast BFD notices a dead neighbor: after `multiplier` packets in a row are missed, so in `interval` times `multiplier` milliseconds. */
|
|
@@ -120,14 +124,46 @@ export type Ospf<I extends string> = {
|
|
|
120
124
|
redistribute?: readonly ("connected" | "static")[];
|
|
121
125
|
};
|
|
122
126
|
|
|
127
|
+
/** 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. */
|
|
128
|
+
export type RoutingTable = {
|
|
129
|
+
/** What the table is for. */
|
|
130
|
+
description?: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** Traffic a routing rule matches: all of what is given. Every packet when absent. */
|
|
134
|
+
export type RoutingRuleMatch<I extends string> = {
|
|
135
|
+
/** Source prefix. */
|
|
136
|
+
src?: Prefix;
|
|
137
|
+
/** Destination prefix. */
|
|
138
|
+
dst?: Prefix;
|
|
139
|
+
/** The interface the traffic arrives on. */
|
|
140
|
+
in_interface?: I;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** One routing rule: traffic it matches is routed by `table` instead of the main table. */
|
|
144
|
+
export type RoutingRule<I extends string, T extends string> = {
|
|
145
|
+
/** Why the rule exists. */
|
|
146
|
+
description?: string;
|
|
147
|
+
/** What the traffic must match. Every packet when absent, so such a rule is the last one. */
|
|
148
|
+
match?: RoutingRuleMatch<I>;
|
|
149
|
+
/** The table that routes the traffic: one of `routing.tables`, or `main`. */
|
|
150
|
+
table: T | "main";
|
|
151
|
+
/** 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`. */
|
|
152
|
+
fallback?: "none" | "main";
|
|
153
|
+
};
|
|
154
|
+
|
|
123
155
|
/** Routing on this device. */
|
|
124
|
-
export type Routing<P extends string, G extends string, I extends string, F extends string> = {
|
|
156
|
+
export type Routing<P extends string, G extends string, I extends string, F extends string, T extends string = never> = {
|
|
125
157
|
/** Identifies this router to its neighbors. */
|
|
126
158
|
router_id?: IP;
|
|
159
|
+
/** Routing tables beside the main one, keyed by name. Not `main`, and not the name of a VRF. */
|
|
160
|
+
tables?: Record<T, RoutingTable>;
|
|
161
|
+
/** 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. */
|
|
162
|
+
rules?: readonly RoutingRule<I, NoInfer<T>>[];
|
|
127
163
|
/** Routes the device has without learning them. */
|
|
128
|
-
static?: readonly StaticRoute<I, F
|
|
164
|
+
static?: readonly StaticRoute<I, F, NoInfer<T>>[];
|
|
129
165
|
/** BGP sessions and what they exchange. */
|
|
130
|
-
bgp?: Bgp<P, G
|
|
166
|
+
bgp?: Bgp<P, G, NoInfer<T>>;
|
|
131
167
|
/** OSPF areas and interfaces. */
|
|
132
168
|
ospf?: Ospf<I>;
|
|
133
169
|
/** Validators answering route origin validation. */
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Check, Finding } from "@takodotid/circuit";
|
|
4
4
|
import { Address } from "@takodotid/circuit";
|
|
5
5
|
import { Checks } from "@takodotid/circuit/helpers";
|
|
6
|
+
import { blocksOf, type Tenant } from "./tenancy";
|
|
6
7
|
import { TENANTS } from "./tenants";
|
|
7
8
|
|
|
8
9
|
export default [
|
|
@@ -15,21 +16,27 @@ export default [
|
|
|
15
16
|
// A native VLAN stays on one trunk and nowhere else, or it joins every trunk it is native on.
|
|
16
17
|
Checks.nativeVlansAreDeadEnds,
|
|
17
18
|
|
|
18
|
-
// Tenants never share a name, a
|
|
19
|
+
// Tenants never share a name, a VLAN, an address or a port. A port given twice would go to the last tenant alone.
|
|
19
20
|
() => {
|
|
20
21
|
const findings: Finding[] = [];
|
|
21
22
|
const error = (message: string) => findings.push({ level: "error", message });
|
|
23
|
+
const vlansOf = (t: Tenant) => [t.internet.vlan, ...(t.private ? [t.private.vlan] : [])];
|
|
22
24
|
|
|
23
25
|
TENANTS.forEach((t, index) => {
|
|
24
26
|
for (const other of TENANTS.slice(index + 1)) {
|
|
25
27
|
if (other.name === t.name) error(`tenant ${t.name} is declared twice`);
|
|
26
|
-
if (other.number === t.number) error(`tenants ${t.name} and ${other.name} share number ${t.number}`);
|
|
27
28
|
|
|
28
|
-
for (const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
for (const vlan of vlansOf(t)) {
|
|
30
|
+
for (const theirs of vlansOf(other)) {
|
|
31
|
+
if (theirs.id === vlan.id)
|
|
32
|
+
error(`VLAN ${vlan.id} is both ${vlan.name} of ${t.name} and ${theirs.name} of ${other.name}`);
|
|
33
|
+
if (theirs.name === vlan.name) error(`VLAN ${vlan.name} is declared by both ${t.name} and ${other.name}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const block of blocksOf(t)) {
|
|
38
|
+
const clash = blocksOf(other).find((prefix) => Address.contains(prefix, block) || Address.contains(block, prefix));
|
|
39
|
+
if (clash) error(`${block} of ${t.name} overlaps ${clash} of ${other.name}`);
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
|
|
@@ -39,10 +46,6 @@ export default [
|
|
|
39
46
|
if (sharer) error(`port ${port} on ${device} is given to both ${t.name} and ${sharer.name}`);
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
|
-
|
|
43
|
-
for (const { prefix, gateway } of t.blocks) {
|
|
44
|
-
if (!Address.contains(prefix, gateway)) error(`the gateway ${gateway} of ${t.name} is outside ${prefix}`);
|
|
45
|
-
}
|
|
46
49
|
});
|
|
47
50
|
|
|
48
51
|
return findings;
|
|
@@ -6,7 +6,7 @@ import type { Prefix } from "@takodotid/circuit";
|
|
|
6
6
|
import { defineDevice, secret, Utils } from "@takodotid/circuit";
|
|
7
7
|
import { Bgp, Filters } from "@takodotid/circuit/helpers";
|
|
8
8
|
import { ASN, communities } from "./routing";
|
|
9
|
-
import { edgeRules, gateways, internetVlanNames,
|
|
9
|
+
import { edgeRules, gateways, internetVlanNames, routerVlans } from "./tenancy";
|
|
10
10
|
import { TENANTS } from "./tenants";
|
|
11
11
|
|
|
12
12
|
/** The address space this router announces. */
|
|
@@ -34,7 +34,7 @@ export default defineDevice({
|
|
|
34
34
|
transit: { id: 300, description: "IP transit from Hurricane Electric, AS6939" },
|
|
35
35
|
ix: { id: 200, description: "Peering LAN of SGIX" },
|
|
36
36
|
},
|
|
37
|
-
|
|
37
|
+
routerVlans(TENANTS)
|
|
38
38
|
),
|
|
39
39
|
|
|
40
40
|
ports: {
|