@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
|
@@ -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/firewall.ts
CHANGED
|
@@ -104,7 +104,11 @@ export type Firewall<I extends string, AS extends string> = {
|
|
|
104
104
|
};
|
|
105
105
|
/** Helpers that run. None when absent. */
|
|
106
106
|
helpers?: readonly Helper[];
|
|
107
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Traffic to and from these prefixes passes without connection tracking. A flood toward them cannot fill the connection table, and rules see its packets in the state `untracked`. Everything is tracked when absent.
|
|
109
|
+
*
|
|
110
|
+
* Each address NAT uses inside one of these prefixes (a source translation's `to`, a destination translation's `to` or matched `dst`) stays tracked: Circuit writes two accept rules for it, one for traffic to it and one for traffic from it, before the untracked rules. Without them, a reply to that address would skip tracking and never be translated back.
|
|
111
|
+
*/
|
|
108
112
|
untracked?: readonly Prefix[];
|
|
109
113
|
};
|
|
110
114
|
|
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/policy.ts
CHANGED
|
@@ -83,15 +83,23 @@ export type Sets = {
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* A rule without `action` changes the route and moves on to the next rule.
|
|
86
|
+
* 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.
|
|
89
87
|
*/
|
|
90
|
-
export type
|
|
88
|
+
export type CallRule<P extends string> = {
|
|
91
89
|
/** Shown beside the rule on the device. */
|
|
92
90
|
description?: string;
|
|
93
|
-
/**
|
|
94
|
-
call
|
|
91
|
+
/** The policy to run. */
|
|
92
|
+
call: P;
|
|
93
|
+
match?: never;
|
|
94
|
+
set?: never;
|
|
95
|
+
action?: never;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Change and decide on the routes that match. A rule without `action` changes the route and moves on to the next rule. */
|
|
99
|
+
export type MatchRule<S extends string> = {
|
|
100
|
+
/** Shown beside the rule on the device. */
|
|
101
|
+
description?: string;
|
|
102
|
+
call?: never;
|
|
95
103
|
/** Which routes the rule applies to. Every route when absent. */
|
|
96
104
|
match?: Match<S>;
|
|
97
105
|
/** What to change on a matching route. */
|
|
@@ -100,5 +108,8 @@ export type PolicyRule<P extends string, S extends string> = {
|
|
|
100
108
|
action?: "accept" | "reject";
|
|
101
109
|
};
|
|
102
110
|
|
|
111
|
+
/** One step of a route policy: a call to another policy, or a rule that matches routes, changes them and may decide on them. */
|
|
112
|
+
export type PolicyRule<P extends string, S extends string> = CallRule<P> | MatchRule<S>;
|
|
113
|
+
|
|
103
114
|
/** Rules evaluated in order, first decision wins. A route no rule accepts is rejected. */
|
|
104
115
|
export type RoutePolicy<P extends string, S extends string> = readonly PolicyRule<P, S>[];
|
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: {
|
|
@@ -1,94 +1,73 @@
|
|
|
1
|
-
// What a tenant is, and what each device carries for it. A tenant's file
|
|
1
|
+
// What a tenant is, and what each device carries for it. A tenant's file writes everything its devices carry: its VLANs, the router's addresses for it, its ports. These functions only gather them for one device.
|
|
2
2
|
|
|
3
|
-
import type { AclRule, Interface,
|
|
3
|
+
import type { AclRule, Interface, InterfaceAddress, Port, Prefix, Vlan } from "@takodotid/circuit";
|
|
4
4
|
import { Address } from "@takodotid/circuit";
|
|
5
5
|
|
|
6
|
-
/** A
|
|
7
|
-
type
|
|
8
|
-
|
|
9
|
-
export type Tenant
|
|
10
|
-
/** Lowercase, and never reused.
|
|
11
|
-
name:
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
/** A VLAN with its name. */
|
|
7
|
+
export type NamedVlan = Vlan & { name: string };
|
|
8
|
+
|
|
9
|
+
export type Tenant = {
|
|
10
|
+
/** Lowercase, and never reused. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** The tenant's VLAN to the internet. The router has it, with the tenant's gateways, and so does every switch, to carry it up to the router. */
|
|
13
|
+
internet: {
|
|
14
|
+
vlan: NamedVlan;
|
|
15
|
+
/** The router's addresses on it, each the gateway the tenant's servers use inside one of its blocks, such as `198.51.100.9/29`. */
|
|
16
|
+
gateways: readonly InterfaceAddress[];
|
|
17
|
+
};
|
|
18
|
+
/** A VLAN between the tenant's own servers. It is only on a switch where one of the tenant's ports uses it, and never on the router. */
|
|
19
|
+
private?: { vlan: NamedVlan };
|
|
16
20
|
/** Rules for traffic toward the tenant's IPv4 blocks from the transit and the exchange, such as a cap on UDP. Everything reaches the tenant when absent. */
|
|
17
21
|
acl?: readonly (Omit<AclRule<never>, "match"> & { match?: Omit<NonNullable<AclRule<never>["match"]>, "vlan" | "dst"> })[];
|
|
18
|
-
/** The tenant's ports, by switch, then by port
|
|
19
|
-
ports: Readonly<
|
|
20
|
-
Record<string, Readonly<Record<string, { description: string; access?: TenantNetwork; trunk?: readonly TenantNetwork[] }>>>
|
|
21
|
-
>;
|
|
22
|
+
/** The tenant's ports, by switch, then by port, written as any port is. */
|
|
23
|
+
ports: Readonly<Record<string, Readonly<Record<string, Port<string, string, string, string>>>>>;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const internetVlan = <N extends string>(t: Tenant<N>) => `${t.name}-internet` as const;
|
|
28
|
-
const privateVlan = <N extends string>(t: Tenant<N>) => `${t.name}-private` as const;
|
|
26
|
+
/** A VLAN as `vlans` takes it, keyed by its name. */
|
|
27
|
+
const entry = ({ name, ...vlan }: NamedVlan): [string, Vlan] => [name, vlan];
|
|
29
28
|
|
|
30
|
-
/**
|
|
31
|
-
export const
|
|
32
|
-
Object.fromEntries(tenants.map((t) => [internetVlan(t), { id: 1000 + t.number, description: `Tenant ${t.name} internet` }])) as Record<
|
|
33
|
-
`${N}-internet`,
|
|
34
|
-
Vlan
|
|
35
|
-
>;
|
|
29
|
+
/** The tenant's blocks: the network of each gateway. */
|
|
30
|
+
export const blocksOf = (t: Tenant): Prefix[] => t.internet.gateways.map((gateway) => Address.prefixOf(gateway));
|
|
36
31
|
|
|
37
|
-
/**
|
|
38
|
-
export const
|
|
32
|
+
/** Every tenant's internet VLAN, for the router. */
|
|
33
|
+
export const routerVlans = (tenants: readonly Tenant[]): Record<string, Vlan> =>
|
|
34
|
+
Object.fromEntries(tenants.map((t) => entry(t.internet.vlan)));
|
|
39
35
|
|
|
40
|
-
/** The
|
|
41
|
-
export const
|
|
36
|
+
/** The VLANs one switch carries: every tenant's internet VLAN, and the private VLAN of each tenant with a port on it that uses it. */
|
|
37
|
+
export const switchVlans = (tenants: readonly Tenant[], device: string): Record<string, Vlan> =>
|
|
42
38
|
Object.fromEntries(
|
|
43
|
-
tenants
|
|
44
|
-
.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
export function portsOn<N extends string>(tenants: readonly Tenant<N>[], device: string) {
|
|
52
|
-
const ports: Record<string, Port<`${N}-internet` | `${N}-private`, string, string, string>> = {};
|
|
53
|
-
|
|
54
|
-
for (const t of tenants) {
|
|
55
|
-
const vlanOf = (network: TenantNetwork) => (network === "internet" ? internetVlan(t) : privateVlan(t));
|
|
39
|
+
tenants.flatMap((t) => {
|
|
40
|
+
const ports = Object.values(t.ports[device] ?? {});
|
|
41
|
+
const usesPrivate =
|
|
42
|
+
t.private &&
|
|
43
|
+
ports.some((port) => port.access_vlan === t.private!.vlan.name || port.trunk_vlans?.includes(t.private!.vlan.name));
|
|
44
|
+
return [entry(t.internet.vlan), ...(usesPrivate ? [entry(t.private!.vlan)] : [])];
|
|
45
|
+
})
|
|
46
|
+
);
|
|
56
47
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
description,
|
|
60
|
-
...(access ? { access_vlan: vlanOf(access) } : {}),
|
|
61
|
-
...(trunk ? { trunk_vlans: trunk.map(vlanOf) } : {}),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
48
|
+
/** The names of every tenant's internet VLAN, for a trunk toward the router. */
|
|
49
|
+
export const internetVlanNames = (tenants: readonly Tenant[]) => tenants.map((t) => t.internet.vlan.name);
|
|
65
50
|
|
|
66
|
-
|
|
67
|
-
|
|
51
|
+
/** The tenants' ports on one switch. */
|
|
52
|
+
export const portsOn = (tenants: readonly Tenant[], device: string): Record<string, Port<string, string, string, string>> =>
|
|
53
|
+
Object.assign({}, ...tenants.map((t) => t.ports[device] ?? {}));
|
|
68
54
|
|
|
69
55
|
/** The router's interface on each tenant's internet VLAN, holding the tenant's gateways. */
|
|
70
|
-
export
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
description: `Tenant ${t.name}`,
|
|
78
|
-
addresses: t.blocks.map(({ prefix, gateway }): Prefix => `${gateway}/${Address.split(prefix).len}`),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return interfaces;
|
|
83
|
-
}
|
|
56
|
+
export const gateways = (tenants: readonly Tenant[]): Record<string, Interface<string, never>> =>
|
|
57
|
+
Object.fromEntries(
|
|
58
|
+
tenants.map((t) => [
|
|
59
|
+
t.internet.vlan.name,
|
|
60
|
+
{ type: "vlan", vlan: t.internet.vlan.name, description: `Tenant ${t.name}`, addresses: t.internet.gateways },
|
|
61
|
+
])
|
|
62
|
+
);
|
|
84
63
|
|
|
85
64
|
/** Each tenant's `acl` rules on one VLAN from outside, one per rule and IPv4 block, each limited to traffic toward that block. */
|
|
86
65
|
export const edgeRules = <V extends string>(tenants: readonly Tenant[], vlan: V): AclRule<V>[] =>
|
|
87
66
|
tenants.flatMap((t) =>
|
|
88
67
|
(t.acl ?? []).flatMap((rule) =>
|
|
89
|
-
t
|
|
90
|
-
.filter((
|
|
91
|
-
.map((
|
|
68
|
+
blocksOf(t)
|
|
69
|
+
.filter((prefix) => Address.family(prefix) === "ipv4")
|
|
70
|
+
.map((prefix) => ({
|
|
92
71
|
...rule,
|
|
93
72
|
...(rule.description ? { description: `${vlan}: ${rule.description}` } : {}),
|
|
94
73
|
match: { ...rule.match, vlan, dst: prefix },
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// An example tenant. Copy this file for each new tenant, and add it to index.ts.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { Tenant } from "../tenancy";
|
|
4
4
|
|
|
5
|
-
export default
|
|
5
|
+
export default {
|
|
6
6
|
name: "acme",
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
internet: {
|
|
8
|
+
vlan: { name: "acme-internet", id: 1001, description: "Tenant acme internet" },
|
|
9
|
+
gateways: ["198.51.100.9/29", "2001:db8:100:1::1/64"],
|
|
10
|
+
},
|
|
11
|
+
private: { vlan: { name: "acme-private", id: 2001, description: "Tenant acme private" } },
|
|
12
12
|
acl: [{ description: "UDP to acme above 1G", match: { protocol: "udp" }, action: "accept", rate: "1G" }],
|
|
13
13
|
ports: {
|
|
14
14
|
"tor-01": {
|
|
15
|
-
"10g-1": { description: "acme server 1",
|
|
16
|
-
"10g-2": { description: "acme server 2",
|
|
15
|
+
"10g-1": { description: "acme server 1", trunk_vlans: ["acme-internet", "acme-private"] },
|
|
16
|
+
"10g-2": { description: "acme server 2", trunk_vlans: ["acme-internet", "acme-private"] },
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
|
-
}
|
|
19
|
+
} satisfies Tenant;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Every tenant. A tenant joins by being added here, and leaves by being removed: the next apply takes away everything it had.
|
|
2
2
|
|
|
3
|
+
import type { Tenant } from "../tenancy";
|
|
3
4
|
import acme from "./acme";
|
|
4
5
|
|
|
5
|
-
export const TENANTS = [acme]
|
|
6
|
+
export const TENANTS: readonly Tenant[] = [acme];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The top-of-rack switch: the tenants' servers, and one uplink to the router.
|
|
2
2
|
|
|
3
3
|
import { defineDevice, secret, Utils } from "@takodotid/circuit";
|
|
4
|
-
import { internetVlanNames,
|
|
4
|
+
import { internetVlanNames, portsOn, switchVlans } from "./tenancy";
|
|
5
5
|
import { TENANTS } from "./tenants";
|
|
6
6
|
|
|
7
7
|
export default defineDevice({
|
|
@@ -17,7 +17,7 @@ export default defineDevice({
|
|
|
17
17
|
lldp: true,
|
|
18
18
|
stp: {},
|
|
19
19
|
|
|
20
|
-
vlans: Utils.merge({ mgmt: { id: 99, description: "Management" } },
|
|
20
|
+
vlans: Utils.merge({ mgmt: { id: 99, description: "Management" } }, switchVlans(TENANTS, "tor-01")),
|
|
21
21
|
|
|
22
22
|
// Every tenant's ports, from its file in tenants/, beside the switch's own.
|
|
23
23
|
ports: Utils.merge(portsOn(TENANTS, "tor-01"), {
|