@takodotid/circuit 0.39.0 → 0.41.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/cli/index.js +305 -29
- package/dist/helpers/index.js +129 -147
- package/dist/types/core/validator.d.ts +2 -0
- package/dist/types/helpers/bgp.d.ts +53 -9
- package/dist/types/helpers/checks.d.ts +4 -0
- package/dist/types/helpers/index.d.ts +2 -2
- package/dist/types/helpers/upstreams.d.ts +115 -181
- package/dist/types/platforms/derive.d.ts +1 -1
- package/dist/types/platforms/fortios/firewall.d.ts +2 -0
- package/dist/types/platforms/fortios/index.d.ts +7 -1
- package/dist/types/platforms/fortios/render.d.ts +2 -0
- package/dist/types/platforms/fortios/vdoms.d.ts +50 -0
- package/dist/types/platforms/routeros/render.d.ts +7 -1
- package/dist/types/schema/device.d.ts +21 -1
- package/dist/types/schema/firewall.d.ts +5 -1
- package/dist/types/schema/policy.d.ts +15 -6
- package/package.json +1 -1
- package/src/core/validator.ts +35 -2
- package/src/helpers/bgp.ts +117 -67
- package/src/helpers/checks.ts +23 -0
- package/src/helpers/index.ts +2 -2
- package/src/helpers/upstreams.ts +157 -271
- package/src/platforms/derive.ts +3 -1
- package/src/platforms/fortios/firewall.ts +9 -0
- package/src/platforms/fortios/index.ts +90 -28
- package/src/platforms/fortios/render.ts +27 -5
- package/src/platforms/fortios/vdoms.ts +217 -0
- package/src/platforms/routeros/TRAPS.md +1 -0
- package/src/platforms/routeros/render.ts +36 -3
- package/src/schema/device.ts +22 -1
- package/src/schema/firewall.ts +5 -1
- package/src/schema/policy.ts +17 -6
package/src/helpers/upstreams.ts
CHANGED
|
@@ -6,263 +6,173 @@ import type {
|
|
|
6
6
|
Gre,
|
|
7
7
|
InterfaceAddress,
|
|
8
8
|
IP,
|
|
9
|
-
LargeCommunity,
|
|
10
9
|
Neighbor,
|
|
11
10
|
NeighborSettings,
|
|
12
11
|
PolicyRule,
|
|
13
12
|
Port,
|
|
14
13
|
Prefix,
|
|
15
14
|
Secret,
|
|
16
|
-
StandardCommunity,
|
|
17
15
|
Vlan,
|
|
18
16
|
VlanInterface,
|
|
19
17
|
} from "../schema";
|
|
20
|
-
import { Bgp, type Announced } from "./bgp";
|
|
21
18
|
|
|
22
|
-
/**
|
|
19
|
+
/** A route policy an upstream's sessions use: its name on the device, and its rules, in order. */
|
|
20
|
+
export type UpstreamPolicy = {
|
|
21
|
+
/** The policy's name, as the device carries it. */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Its rules, in order. Nothing is added to them unless `mapImports` or `mapExports` adds it. */
|
|
24
|
+
rules: readonly PolicyRule<string, string>[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** A VLAN by its name and its ID. */
|
|
28
|
+
export type NamedVlan = { name: string; id: number };
|
|
29
|
+
|
|
30
|
+
/** One network we exchange routes with: a transit we buy, an exchange's route servers, or a peer. Every name it puts on a device is written here. */
|
|
23
31
|
export type Upstream = {
|
|
24
32
|
/** Its name as people write it, for descriptions: `Transit A`. The key when absent. */
|
|
25
33
|
name?: string;
|
|
26
34
|
asn: number;
|
|
27
35
|
/** What it is to us: `provider` for a transit, `rs` for an exchange's route servers, `peer` for a direct session. */
|
|
28
36
|
role: "provider" | "rs" | "peer";
|
|
29
|
-
/** Where it lands on our side: a VLAN,
|
|
37
|
+
/** Where it lands on our side: a VLAN, the router's interface on it, and the switch port it arrives on. A peer over a tunnel, or over an exchange's VLAN, has none. */
|
|
30
38
|
handoff?: {
|
|
31
|
-
/** The VLAN
|
|
32
|
-
vlan:
|
|
33
|
-
/** It sends its VLAN tagged: its port gets
|
|
34
|
-
native?:
|
|
35
|
-
/**
|
|
36
|
-
|
|
39
|
+
/** The VLAN it arrives on. */
|
|
40
|
+
vlan: NamedVlan;
|
|
41
|
+
/** It sends its VLAN tagged: its switch port gets this native VLAN, which nothing else is in. Untagged when absent. */
|
|
42
|
+
native?: NamedVlan;
|
|
43
|
+
/** The router's VLAN interface on it. */
|
|
44
|
+
interface: string;
|
|
37
45
|
/** Our addresses on it. `{ address, peer }` for a point-to-point address whose far end is outside its own prefix. */
|
|
38
46
|
addresses: readonly (InterfaceAddress | { address: InterfaceAddress; peer: IP })[];
|
|
39
|
-
/**
|
|
40
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The switch port it arrives on. LLDP and spanning tree are off there unless `lldp` or `stp` says otherwise: an upstream's port faces someone else's network, which should neither learn ours nor join our spanning tree.
|
|
49
|
+
*/
|
|
50
|
+
port?: {
|
|
51
|
+
device: string;
|
|
52
|
+
port: string;
|
|
53
|
+
storm_control?: Port<string, string, string, string>["storm_control"];
|
|
54
|
+
/** LLDP on the port. Off when absent. */
|
|
55
|
+
lldp?: boolean;
|
|
56
|
+
/** Spanning tree on the port. Off when absent. */
|
|
57
|
+
stp?: boolean;
|
|
58
|
+
};
|
|
41
59
|
/** An exchange's LAN, for `circuit peeringdb`. */
|
|
42
60
|
exchange?: ExchangeLan;
|
|
43
61
|
};
|
|
44
|
-
/** A GRE tunnel to it
|
|
45
|
-
tunnel?: { local: IP; remote: IP; addresses: readonly InterfaceAddress[]; mtu?: number
|
|
62
|
+
/** A GRE tunnel to it, and the router's interface for it. */
|
|
63
|
+
tunnel?: { interface: string; local: IP; remote: IP; addresses: readonly InterfaceAddress[]; mtu?: number };
|
|
46
64
|
/** Our BGP sessions with it, by name: its address, and ours when it is not the interface's. */
|
|
47
65
|
sessions: Readonly<Record<string, { address: IP; local_address?: IP }>>;
|
|
48
|
-
/**
|
|
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. */
|
|
67
|
+
group?: string;
|
|
68
|
+
/** The sessions are more than one hop away, from this address of ours. */
|
|
49
69
|
multihop?: { local_address: IP; families?: readonly Family[]; password?: Secret };
|
|
50
70
|
/** The most prefixes a session may send, by its family: about twice what it sends. Over it, the session closes until it is cleared. */
|
|
51
71
|
limits?: { ipv4?: number; ipv6?: number };
|
|
52
|
-
/** What we take. Nothing when absent. */
|
|
53
|
-
import?:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
learn?: { local_pref?: number; before?: readonly PolicyRule<never, never>[] };
|
|
58
|
-
};
|
|
59
|
-
/** Rules of yours in its export, between the community actions and what we announce, such as a prepend. */
|
|
60
|
-
export?: readonly PolicyRule<never, never>[];
|
|
61
|
-
/** What we announce to it. Everything else is rejected. */
|
|
62
|
-
announce: readonly Announced[];
|
|
63
|
-
/** Drop RFC 1918 sources arriving from it. `privateDrops()` lists the VLANs. */
|
|
72
|
+
/** What we take, as its policy. Nothing when absent. */
|
|
73
|
+
import?: UpstreamPolicy;
|
|
74
|
+
/** What we send it, as its policy. */
|
|
75
|
+
export: UpstreamPolicy;
|
|
76
|
+
/** Its VLAN is listed by `privateDrops()`, for an ACL that drops RFC 1918 sources arriving from it. Nothing else is done with it. */
|
|
64
77
|
drop_private?: true;
|
|
65
78
|
};
|
|
66
79
|
|
|
67
|
-
/** Upstreams by key.
|
|
80
|
+
/** Upstreams by key. */
|
|
68
81
|
export type UpstreamCatalog = Readonly<Record<string, Upstream>>;
|
|
69
82
|
|
|
70
|
-
/**
|
|
71
|
-
type
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
export type UpstreamNames = {
|
|
77
|
-
/** The VLAN a handoff is on. `{key}` when absent. */
|
|
78
|
-
vlan?: ByRole;
|
|
79
|
-
/** The native VLAN of a handoff sent tagged. `{key}-native` when absent. */
|
|
80
|
-
native?: Pattern;
|
|
81
|
-
/** The router's interface: per role, and `tunnel` for one reached over a tunnel. `{key}` when absent. */
|
|
82
|
-
interface?: Pattern | { readonly provider?: Pattern; readonly rs?: Pattern; readonly peer?: Pattern; readonly tunnel?: Pattern };
|
|
83
|
-
/** The import policy. `{KEY}-IN` when absent. */
|
|
84
|
-
import?: Pattern;
|
|
85
|
-
/** The export policy. `{KEY}-OUT` when absent. */
|
|
86
|
-
export?: Pattern;
|
|
87
|
-
/** The BGP group of an exchange's route servers or of multihop sessions. `{key}` when absent. */
|
|
88
|
-
group?: Pattern;
|
|
89
|
-
/** The description of a handoff's switch port. Its `name` when absent. No device names it, so it may be any function. */
|
|
90
|
-
port?: (key: string, upstream: Upstream) => string;
|
|
83
|
+
/** What every upstream's policies share, written once: plain functions over each policy's rules. */
|
|
84
|
+
export type UpstreamOptions = {
|
|
85
|
+
/** Each import's rules as the device gets them, from the rules the upstream writes. The rules as written when absent. */
|
|
86
|
+
mapImports?: (rules: readonly PolicyRule<string, string>[], key: string, upstream: Upstream) => readonly PolicyRule<string, string>[];
|
|
87
|
+
/** Each export's rules as the device gets them, from the rules the upstream writes. The rules as written when absent. */
|
|
88
|
+
mapExports?: (rules: readonly PolicyRule<string, string>[], key: string, upstream: Upstream) => readonly PolicyRule<string, string>[];
|
|
91
89
|
};
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
import: "{KEY}-IN",
|
|
111
|
-
export: "{KEY}-OUT",
|
|
112
|
-
group: "{key}",
|
|
113
|
-
} as const;
|
|
114
|
-
|
|
115
|
-
type Defaults = typeof DEFAULTS;
|
|
116
|
-
type Field = keyof Defaults;
|
|
117
|
-
|
|
118
|
-
type Key<C> = keyof C & string;
|
|
119
|
-
/** The upstreams that land on a VLAN of ours. */
|
|
120
|
-
type Landed<C> = { [K in Key<C>]: C[K] extends { handoff: object } ? K : never }[Key<C>];
|
|
121
|
-
/** The upstreams that send their VLAN tagged. */
|
|
122
|
-
type Tagged<C> = { [K in Key<C>]: C[K] extends { handoff: { native: number } } ? K : never }[Key<C>];
|
|
123
|
-
/** The upstreams reached over a tunnel. */
|
|
124
|
-
type Tunneled<C> = { [K in Key<C>]: C[K] extends { tunnel: object } ? K : never }[Key<C>];
|
|
125
|
-
/** The upstreams whose sessions share a group: an exchange's route servers, or multihop sessions. */
|
|
126
|
-
type Grouped<C> = { [K in Key<C>]: C[K] extends { role: "rs" } | { multihop: object } ? K : never }[Key<C>];
|
|
127
|
-
/** The upstreams we take routes from. */
|
|
128
|
-
type Importing<C> = { [K in Key<C>]: C[K] extends { import: object } ? K : never }[Key<C>];
|
|
129
|
-
|
|
130
|
-
/** The project's pattern for a name, or the default. */
|
|
131
|
-
type PatternOf<N, F extends Field> = N extends { [X in F]: infer P } ? P : Defaults[F];
|
|
132
|
-
/** The pattern an upstream takes: the only one, its tunnel's, its role's, or the default. */
|
|
133
|
-
type Chosen<P, U, F extends Field> = P extends string
|
|
134
|
-
? P
|
|
135
|
-
: U extends { tunnel: object }
|
|
136
|
-
? P extends { tunnel: infer T extends string }
|
|
137
|
-
? T
|
|
138
|
-
: Defaults[F]
|
|
139
|
-
: U extends { role: infer R extends string }
|
|
140
|
-
? P extends { [X in R]: infer T extends string }
|
|
141
|
-
? T
|
|
142
|
-
: Defaults[F]
|
|
143
|
-
: Defaults[F];
|
|
144
|
-
/** Every `From` in `S` replaced. */
|
|
145
|
-
type Replace<S extends string, From extends string, To extends string> = S extends `${infer A}${From}${infer B}`
|
|
146
|
-
? `${A}${To}${Replace<B, From, To>}`
|
|
147
|
-
: S;
|
|
148
|
-
/** An upstream's `handoff.number` or `tunnel.number`. */
|
|
149
|
-
type NumberOf<U> = U extends { handoff: { number: infer N extends number } }
|
|
150
|
-
? N
|
|
151
|
-
: U extends { tunnel: { number: infer N extends number } }
|
|
152
|
-
? N
|
|
153
|
-
: number;
|
|
154
|
-
/** The name built for upstream `K`: its pattern with the key, the key in capitals and its number filled in. */
|
|
155
|
-
type Named<C, N, F extends Field, K extends Key<C>> = Replace<
|
|
156
|
-
Replace<Replace<Chosen<PatternOf<N, F>, C[K], F>, "{key}", K>, "{KEY}", Uppercase<K>>,
|
|
157
|
-
"{number}",
|
|
158
|
-
`${NumberOf<C[K]>}`
|
|
159
|
-
>;
|
|
160
|
-
/** Each of these upstreams' names. */
|
|
161
|
-
type Names<C, N, F extends Field, Ks extends Key<C>> = { [K in Ks]: Named<C, N, F, K> }[Ks];
|
|
162
|
-
|
|
163
|
-
/** The catalog as written, with any key the shape does not have refused, at every level. */
|
|
164
|
-
type Exact<S, T> = T extends readonly unknown[] | string | number | boolean | Secret | ((...args: never[]) => unknown)
|
|
165
|
-
? T
|
|
166
|
-
: { [K in keyof T]: K extends keyof S ? Exact<NonNullable<S[K]>, T[K]> : never };
|
|
167
|
-
|
|
168
|
-
/** The names as written, with a name or a role Upstreams does not make refused. */
|
|
169
|
-
type ExactNames<N> = {
|
|
170
|
-
[F in keyof N]: F extends keyof UpstreamNames
|
|
171
|
-
? N[F] extends string | ((...args: never[]) => unknown)
|
|
172
|
-
? N[F]
|
|
173
|
-
: { [R in keyof N[F]]: R extends "provider" | "rs" | "peer" | "tunnel" ? N[F][R] : never }
|
|
174
|
-
: never;
|
|
175
|
-
};
|
|
91
|
+
type Values<T> = T[keyof T];
|
|
92
|
+
/** Each VLAN name the catalog writes. */
|
|
93
|
+
type VlanName<C> = Values<{ [K in keyof C]: C[K] extends { handoff: { vlan: { name: infer N extends string } } } ? N : never }>;
|
|
94
|
+
/** Each native VLAN name. */
|
|
95
|
+
type NativeName<C> = Values<{ [K in keyof C]: C[K] extends { handoff: { native: { name: infer N extends string } } } ? N : never }>;
|
|
96
|
+
/** Each router interface on a handoff. */
|
|
97
|
+
type HandoffInterface<C> = Values<{ [K in keyof C]: C[K] extends { handoff: { interface: infer N extends string } } ? N : never }>;
|
|
98
|
+
/** Each router interface for a tunnel. */
|
|
99
|
+
type TunnelInterface<C> = Values<{ [K in keyof C]: C[K] extends { tunnel: { interface: infer N extends string } } ? N : never }>;
|
|
100
|
+
/** Each policy name. */
|
|
101
|
+
type PolicyName<C> = Values<{
|
|
102
|
+
[K in keyof C]:
|
|
103
|
+
| (C[K] extends { import: { name: infer N extends string } } ? N : never)
|
|
104
|
+
| (C[K] extends { export: { name: infer N extends string } } ? N : never);
|
|
105
|
+
}>;
|
|
106
|
+
/** Each BGP group name. */
|
|
107
|
+
type GroupName<C> = Values<{ [K in keyof C]: C[K] extends { group: infer N extends string } ? N : never }>;
|
|
176
108
|
|
|
177
109
|
/** A network's upstreams written once, and each device's part built from them. */
|
|
178
110
|
export class Upstreams {
|
|
179
111
|
/**
|
|
180
|
-
* Each upstream written once: its AS, where it lands, its sessions, limits and
|
|
181
|
-
*
|
|
182
|
-
* - VLAN `<key>`, and `<key>-native` for one that sends its VLAN tagged. The router's interface is `<key>` too, a VLAN interface or a GRE tunnel.
|
|
183
|
-
* - Policies `<KEY>-IN` and `<KEY>-OUT`; a BGP group `<key>` for an exchange's route servers or multihop sessions.
|
|
112
|
+
* Each upstream written once: its AS, where it lands, its sessions, limits and policies, and every name it puts on a device. Everything a device needs is built from it:
|
|
184
113
|
*
|
|
185
|
-
*
|
|
114
|
+
* - On the router: `vlans()`, `interfaces()`, `uplinks()`, `groups()`, `neighbors()`, `policies()`, `tunnelRules()`, `addresses()`.
|
|
115
|
+
* - On a switch: `vlans(device)`, `ports(device)`, `vlansOn(device)`.
|
|
116
|
+
* - For checks and ACLs: `untrusted()`, `natives()`, `privateDrops()`.
|
|
186
117
|
*
|
|
187
|
-
*
|
|
118
|
+
* An import or export is the rules the upstream writes, in order, passed through `mapImports` or `mapExports` when given: what every upstream shares, such as a call to a sanity policy or the community actions, goes there. Nothing else is added. Lists follow the catalog's order, where order counts on a device. A key the shape doesn't have is refused by the type, by its name.
|
|
188
119
|
*/
|
|
189
|
-
static define<const C extends UpstreamCatalog
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
type T = Names<C, N, "native", Tagged<C>>;
|
|
195
|
-
type I = Names<C, N, "interface", Landed<C>>;
|
|
196
|
-
type G = Names<C, N, "interface", Tunneled<C>>;
|
|
197
|
-
type PolicyName = Names<C, N, "import", Importing<C>> | Names<C, N, "export", Key<C>>;
|
|
198
|
-
type Group = Names<C, N, "group", Grouped<C>>;
|
|
199
|
-
|
|
200
|
-
const names: UpstreamNames = options.names ?? {};
|
|
201
|
-
const all = Object.entries(catalog) as [Key<C>, Upstream][];
|
|
120
|
+
static define<const C extends UpstreamCatalog>(catalog: C & UpstreamCatalog, options: UpstreamOptions = {}) {
|
|
121
|
+
type V = VlanName<C>;
|
|
122
|
+
type T = NativeName<C>;
|
|
123
|
+
|
|
124
|
+
const all = Object.entries(catalog) as [string, Upstream][];
|
|
202
125
|
const nameOf = (key: string, upstream: Upstream) => upstream.name ?? key;
|
|
203
|
-
const grouped = (upstream: Upstream) => upstream.role === "rs" || Boolean(upstream.multihop);
|
|
204
|
-
|
|
205
|
-
/** The name built for one upstream: its pattern with the key, the key in capitals and its number filled in. */
|
|
206
|
-
const named = (field: Field, key: string, upstream: Upstream): string => {
|
|
207
|
-
const given = names[field];
|
|
208
|
-
const pattern =
|
|
209
|
-
given === undefined || typeof given === "string"
|
|
210
|
-
? (given ?? DEFAULTS[field])
|
|
211
|
-
: ((upstream.tunnel && "tunnel" in given ? given.tunnel : given[upstream.role]) ?? DEFAULTS[field]);
|
|
212
|
-
|
|
213
|
-
return pattern
|
|
214
|
-
.replaceAll("{key}", key)
|
|
215
|
-
.replaceAll("{KEY}", key.toUpperCase())
|
|
216
|
-
.replaceAll("{number}", () => {
|
|
217
|
-
const number = upstream.handoff?.number ?? upstream.tunnel?.number;
|
|
218
|
-
if (number === undefined) throw new Error(`Upstreams: ${key} needs a number for ${field} name ${pattern}`);
|
|
219
|
-
return String(number);
|
|
220
|
-
});
|
|
221
|
-
};
|
|
222
126
|
|
|
223
127
|
// Two upstreams under one name would replace each other on the device.
|
|
224
|
-
|
|
128
|
+
const unique = (what: string, names: [string, string | undefined][]) => {
|
|
225
129
|
const taken = new Map<string, string>();
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (field === "native" && upstream.handoff?.native === undefined) continue;
|
|
229
|
-
if (field === "import" && !upstream.import) continue;
|
|
230
|
-
if (field === "group" && !grouped(upstream)) continue;
|
|
231
|
-
if ((field === "vlan" && !upstream.handoff) || (field === "interface" && !upstream.handoff && !upstream.tunnel)) continue;
|
|
232
|
-
|
|
233
|
-
const name = named(field, key, upstream);
|
|
130
|
+
for (const [key, name] of names) {
|
|
131
|
+
if (name === undefined) continue;
|
|
234
132
|
const other = taken.get(name);
|
|
235
|
-
if (other) throw new Error(`Upstreams: ${other} and ${key} both have the ${
|
|
133
|
+
if (other) throw new Error(`Upstreams: ${other} and ${key} both have the ${what} ${name}`);
|
|
236
134
|
taken.set(name, key);
|
|
237
135
|
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
136
|
+
};
|
|
137
|
+
unique("VLAN", [
|
|
138
|
+
...all.map(([key, upstream]): [string, string | undefined] => [key, upstream.handoff?.vlan.name]),
|
|
139
|
+
...all.map(([key, upstream]): [string, string | undefined] => [key, upstream.handoff?.native?.name]),
|
|
140
|
+
]);
|
|
141
|
+
unique(
|
|
142
|
+
"interface",
|
|
143
|
+
all.map(([key, upstream]) => [key, upstream.handoff?.interface ?? upstream.tunnel?.interface])
|
|
144
|
+
);
|
|
145
|
+
unique("policy", [
|
|
146
|
+
...all.map(([key, upstream]): [string, string | undefined] => [key, upstream.import?.name]),
|
|
147
|
+
...all.map(([key, upstream]): [string, string | undefined] => [key, upstream.export.name]),
|
|
148
|
+
]);
|
|
149
|
+
unique(
|
|
150
|
+
"BGP group",
|
|
151
|
+
all.map(([key, upstream]) => [key, upstream.group])
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
/** Each landed upstream's VLAN and native, or with a device, those of the upstreams arriving on its ports. */
|
|
155
|
+
const vlans = (device?: string) => {
|
|
246
156
|
const result: Record<string, Vlan> = {};
|
|
247
157
|
|
|
248
158
|
for (const [key, upstream] of all) {
|
|
249
159
|
const handoff = upstream.handoff;
|
|
250
160
|
if (!handoff || (device !== undefined && handoff.port?.device !== device)) continue;
|
|
251
161
|
|
|
252
|
-
result[vlan
|
|
253
|
-
if (
|
|
254
|
-
result[native
|
|
255
|
-
id: handoff.native,
|
|
162
|
+
result[handoff.vlan.name] = { id: handoff.vlan.id, description: nameOf(key, upstream) };
|
|
163
|
+
if (handoff.native) {
|
|
164
|
+
result[handoff.native.name] = {
|
|
165
|
+
id: handoff.native.id,
|
|
256
166
|
description: `Native toward ${nameOf(key, upstream)}, nothing else in it`,
|
|
257
167
|
};
|
|
258
168
|
}
|
|
259
169
|
}
|
|
260
170
|
|
|
261
|
-
return result as
|
|
171
|
+
return result as { [K in V | T]: Vlan };
|
|
262
172
|
};
|
|
263
173
|
|
|
264
174
|
const vlansOn = (device: string) =>
|
|
265
|
-
all.
|
|
175
|
+
all.flatMap(([, upstream]) => (upstream.handoff?.port?.device === device ? [upstream.handoff.vlan.name as V] : []));
|
|
266
176
|
|
|
267
177
|
const ports = (device: string) => {
|
|
268
178
|
const result: Record<string, Port<V | T, never, never, never>> = {};
|
|
@@ -272,12 +182,12 @@ export class Upstreams {
|
|
|
272
182
|
if (!handoff?.port || handoff.port.device !== device) continue;
|
|
273
183
|
|
|
274
184
|
result[handoff.port.port] = {
|
|
275
|
-
description:
|
|
276
|
-
...(handoff.native
|
|
277
|
-
? { trunk_vlans: [vlan
|
|
278
|
-
: { access_vlan: vlan
|
|
279
|
-
lldp: false,
|
|
280
|
-
stp: false,
|
|
185
|
+
description: nameOf(key, upstream),
|
|
186
|
+
...(handoff.native
|
|
187
|
+
? { trunk_vlans: [handoff.vlan.name as V], native_vlan: handoff.native.name as T }
|
|
188
|
+
: { access_vlan: handoff.vlan.name as V }),
|
|
189
|
+
lldp: handoff.port.lldp ?? false,
|
|
190
|
+
stp: handoff.port.stp ?? false,
|
|
281
191
|
...(handoff.port.storm_control ? { storm_control: handoff.port.storm_control } : {}),
|
|
282
192
|
};
|
|
283
193
|
}
|
|
@@ -290,55 +200,46 @@ export class Upstreams {
|
|
|
290
200
|
|
|
291
201
|
for (const [key, upstream] of all) {
|
|
292
202
|
const description = nameOf(key, upstream);
|
|
293
|
-
const name = named("interface", key, upstream);
|
|
294
203
|
|
|
295
204
|
if (upstream.tunnel) {
|
|
296
|
-
const { local, remote, addresses, mtu } = upstream.tunnel;
|
|
205
|
+
const { interface: name, local, remote, addresses, mtu } = upstream.tunnel;
|
|
297
206
|
result[name] = { type: "gre", description, local, remote, addresses, ...(mtu ? { mtu } : {}) };
|
|
298
207
|
} else if (upstream.handoff) {
|
|
299
|
-
const { addresses, exchange } = upstream.handoff;
|
|
300
|
-
result[name] = { type: "vlan", vlan: vlan
|
|
208
|
+
const { interface: name, vlan, addresses, exchange } = upstream.handoff;
|
|
209
|
+
result[name] = { type: "vlan", vlan: vlan.name as V, description, addresses, ...(exchange ? { exchange } : {}) };
|
|
301
210
|
}
|
|
302
211
|
}
|
|
303
212
|
|
|
304
|
-
return result as { [K in
|
|
213
|
+
return result as { [K in HandoffInterface<C>]: VlanInterface<V, never> } & { [K in TunnelInterface<C>]: Gre<never> };
|
|
305
214
|
};
|
|
306
215
|
|
|
307
|
-
/** The router's
|
|
308
|
-
const uplinks = () =>
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
216
|
+
/** The router's interface toward each handoff, for NAT and lists like it. */
|
|
217
|
+
const uplinks = () => all.flatMap(([, upstream]) => (upstream.handoff ? [upstream.handoff.interface as HandoffInterface<C>] : []));
|
|
218
|
+
|
|
219
|
+
/** What a session, or the group it is in, takes from its upstream. */
|
|
220
|
+
const settings = (key: string, upstream: Upstream): NeighborSettings<PolicyName<C>> => {
|
|
221
|
+
const multihop = upstream.multihop;
|
|
222
|
+
return {
|
|
223
|
+
remote_as: upstream.asn,
|
|
224
|
+
description: nameOf(key, upstream),
|
|
225
|
+
role: upstream.role,
|
|
226
|
+
...(upstream.import ? { import: upstream.import.name as PolicyName<C> } : {}),
|
|
227
|
+
export: upstream.export.name as PolicyName<C>,
|
|
228
|
+
...(multihop
|
|
229
|
+
? {
|
|
230
|
+
local_address: multihop.local_address,
|
|
231
|
+
multihop: true,
|
|
232
|
+
...(multihop.families ? { families: multihop.families } : {}),
|
|
233
|
+
...(multihop.password ? { password: multihop.password } : {}),
|
|
234
|
+
}
|
|
235
|
+
: {}),
|
|
236
|
+
};
|
|
237
|
+
};
|
|
319
238
|
|
|
320
239
|
const groups = () =>
|
|
321
240
|
Object.fromEntries(
|
|
322
|
-
all
|
|
323
|
-
|
|
324
|
-
.map(([key, upstream]) => {
|
|
325
|
-
const multihop = upstream.multihop;
|
|
326
|
-
return [
|
|
327
|
-
named("group", key, upstream),
|
|
328
|
-
{
|
|
329
|
-
...settings(key, upstream),
|
|
330
|
-
...(multihop
|
|
331
|
-
? {
|
|
332
|
-
local_address: multihop.local_address,
|
|
333
|
-
multihop: true,
|
|
334
|
-
...(multihop.families ? { families: multihop.families } : {}),
|
|
335
|
-
...(multihop.password ? { password: multihop.password } : {}),
|
|
336
|
-
}
|
|
337
|
-
: {}),
|
|
338
|
-
},
|
|
339
|
-
];
|
|
340
|
-
})
|
|
341
|
-
) as Record<Group, NeighborSettings<PolicyName>>;
|
|
241
|
+
all.flatMap(([key, upstream]) => (upstream.group ? [[upstream.group, settings(key, upstream)]] : []))
|
|
242
|
+
) as Record<GroupName<C>, NeighborSettings<PolicyName<C>>>;
|
|
342
243
|
|
|
343
244
|
const neighbors = () =>
|
|
344
245
|
Object.fromEntries(
|
|
@@ -348,59 +249,44 @@ export class Upstreams {
|
|
|
348
249
|
return [
|
|
349
250
|
name,
|
|
350
251
|
{
|
|
252
|
+
...(upstream.group ? { group: upstream.group } : settings(key, upstream)),
|
|
351
253
|
...session,
|
|
352
|
-
...(grouped(upstream) ? { group: named("group", key, upstream) } : settings(key, upstream)),
|
|
353
254
|
...(limit ? { max_prefixes: limit } : {}),
|
|
354
255
|
},
|
|
355
256
|
];
|
|
356
257
|
})
|
|
357
258
|
)
|
|
358
|
-
) as Record<string, Neighbor<PolicyName
|
|
259
|
+
) as Record<string, Neighbor<PolicyName<C>, GroupName<C>>>;
|
|
359
260
|
|
|
360
|
-
|
|
361
|
-
|
|
261
|
+
/**
|
|
262
|
+
* 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.
|
|
263
|
+
*/
|
|
264
|
+
const policies = () => {
|
|
265
|
+
const result: Record<string, readonly PolicyRule<string, string>[]> = {};
|
|
362
266
|
|
|
363
267
|
for (const [key, upstream] of all) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (!taken) continue;
|
|
372
|
-
|
|
373
|
-
const tag = options.tag?.(key, upstream);
|
|
374
|
-
result[policy("import", key, upstream)] = [
|
|
375
|
-
...(taken.default
|
|
376
|
-
? Bgp.defaultOnly({ ...taken.default, ...(options.sources ? { sources: options.sources } : {}) })
|
|
377
|
-
: []),
|
|
378
|
-
...(taken.learn
|
|
379
|
-
? Bgp.learn<P>({
|
|
380
|
-
...(taken.learn.before ? { before: taken.learn.before } : {}),
|
|
381
|
-
...(options.sanity ? { sanity: options.sanity } : {}),
|
|
382
|
-
...(options.sources ? { sources: options.sources } : {}),
|
|
383
|
-
...(taken.learn.local_pref !== undefined ? { local_pref: taken.learn.local_pref } : {}),
|
|
384
|
-
...(tag !== undefined ? { tag } : {}),
|
|
385
|
-
})
|
|
386
|
-
: [{ description: "Nothing else", action: "reject" as const }]),
|
|
387
|
-
];
|
|
268
|
+
if (upstream.import) {
|
|
269
|
+
const rules = upstream.import.rules;
|
|
270
|
+
result[upstream.import.name] = options.mapImports ? options.mapImports(rules, key, upstream) : rules;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const rules = upstream.export.rules;
|
|
274
|
+
result[upstream.export.name] = options.mapExports ? options.mapExports(rules, key, upstream) : rules;
|
|
388
275
|
}
|
|
389
276
|
|
|
390
|
-
return result as Record<PolicyName
|
|
277
|
+
return result as Record<PolicyName<C>, PolicyRule<never, never>[]>;
|
|
391
278
|
};
|
|
392
279
|
|
|
393
280
|
/** The VLANs straight off an upstream, natives included: `untrusted` for `Checks.trustBoundary`. */
|
|
394
281
|
const untrusted = () => [
|
|
395
|
-
...all.
|
|
282
|
+
...all.flatMap(([, upstream]) => (upstream.handoff ? [upstream.handoff.vlan.name as V] : [])),
|
|
396
283
|
...natives(),
|
|
397
284
|
];
|
|
398
285
|
|
|
399
|
-
const natives = () =>
|
|
400
|
-
all.filter(([, upstream]) => upstream.handoff?.native !== undefined).map(([key, upstream]) => native(key, upstream));
|
|
286
|
+
const natives = () => all.flatMap(([, upstream]) => (upstream.handoff?.native ? [upstream.handoff.native.name as T] : []));
|
|
401
287
|
|
|
402
288
|
const privateDrops = () =>
|
|
403
|
-
all.
|
|
289
|
+
all.flatMap(([, upstream]) => (upstream.handoff && upstream.drop_private ? [upstream.handoff.vlan.name as V] : []));
|
|
404
290
|
|
|
405
291
|
const tunnelRules = (): FilterRule<never, never>[] =>
|
|
406
292
|
all.flatMap(([key, upstream]) =>
|
package/src/platforms/derive.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { Address } from "../core/address";
|
|
|
4
4
|
import type { Device } from "../schema";
|
|
5
5
|
|
|
6
6
|
/** A feature a platform renders once proved on one of its devices. */
|
|
7
|
-
export type ProvedFeature =
|
|
7
|
+
export type ProvedFeature =
|
|
8
|
+
"bfd-timers" | "static-route-bfd" | "multihop-bfd" | "alarm-severity" | "mac-limit" | "ethertype-filter" | "management-vdom";
|
|
8
9
|
|
|
9
10
|
export class Derive {
|
|
10
11
|
/** Each LAG's number: its own `id`, or its position among the device's LAGs. */
|
|
@@ -41,6 +42,7 @@ export class Derive {
|
|
|
41
42
|
const has = (feature: ProvedFeature) => proved.includes(feature);
|
|
42
43
|
|
|
43
44
|
if (routing?.bfd && !has("bfd-timers")) found.push(`routing.bfd, BFD timers are ${unproved}`);
|
|
45
|
+
if (device.vdoms && !has("management-vdom")) found.push(`vdoms, a management VDOM is ${unproved}`);
|
|
44
46
|
|
|
45
47
|
for (const route of routing?.static ?? []) {
|
|
46
48
|
if (route.bfd && !has("static-route-bfd"))
|
|
@@ -155,6 +155,15 @@ export class FortiOsFirewall {
|
|
|
155
155
|
);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/** Who reaches the device's own services, alone: for a management VDOM, which routes no traffic of its own. */
|
|
159
|
+
static managementOnly(root: FortiOsBlock, device: Device): void {
|
|
160
|
+
const localIns = { ipv4: 0, ipv6: 0 };
|
|
161
|
+
const localInEntry = (family: Family) =>
|
|
162
|
+
root.block(family === "ipv4" ? "firewall local-in-policy" : "firewall local-in-policy6").entry(++localIns[family]);
|
|
163
|
+
|
|
164
|
+
FortiOsFirewall.management(localInEntry, new Objects(root, device), device);
|
|
165
|
+
}
|
|
166
|
+
|
|
158
167
|
/** Packet filtering, who reaches the device, NAT and stateless ACLs. */
|
|
159
168
|
static render(root: FortiOsBlock, device: Device): void {
|
|
160
169
|
const fw = device.firewall;
|