@takodotid/circuit 0.40.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 +6 -2
- package/dist/helpers/index.js +129 -147
- 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/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 +10 -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 -274
- package/src/schema/firewall.ts +5 -1
- package/src/schema/policy.ts +17 -6
package/src/helpers/bgp.ts
CHANGED
|
@@ -32,8 +32,8 @@ export type LearnOptions<P extends string> = {
|
|
|
32
32
|
sources?: readonly IP[];
|
|
33
33
|
/** Preference for the routes learned. Higher wins. */
|
|
34
34
|
local_pref?: number;
|
|
35
|
-
/** Where the route came from:
|
|
36
|
-
tag?: StandardCommunity | LargeCommunity | PolicyRule<never, never>;
|
|
35
|
+
/** Where the route came from: `{ standard: "64500:100" }` or `{ large: "64500:1:1" }` adds that community, and a rule, such as `communityScheme(...).tag(...)`, is used as it is. */
|
|
36
|
+
tag?: { standard: StandardCommunity } | { large: LargeCommunity } | PolicyRule<never, never>;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
/** What `Bgp.defaultOnly` sets on the default routes it takes. */
|
|
@@ -44,6 +44,14 @@ export type DefaultOnlyOptions = {
|
|
|
44
44
|
description?: string;
|
|
45
45
|
/** The source address for the default of its family, one per family. */
|
|
46
46
|
sources?: readonly IP[];
|
|
47
|
+
/** End with `{ description: "Nothing else", action: "reject" }`, so the policy takes the default route only. Off by default: any other route goes on to the rules after these. */
|
|
48
|
+
reject?: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** What `Bgp.announce` writes after the accepts. */
|
|
52
|
+
export type AnnounceOptions = {
|
|
53
|
+
/** End with `{ description: "Nothing else", action: "reject" }`, so nothing else leaves. On by default: it stops a route leak when the reject is forgotten. Turn it off for an export that adds rules after these. */
|
|
54
|
+
reject?: boolean;
|
|
47
55
|
};
|
|
48
56
|
|
|
49
57
|
/** Kinds of neighbor and the number each is tagged with, such as `{ transit: 1, customer: 3 }`. */
|
|
@@ -126,22 +134,42 @@ export class Bgp {
|
|
|
126
134
|
];
|
|
127
135
|
}
|
|
128
136
|
|
|
129
|
-
/**
|
|
130
|
-
|
|
137
|
+
/**
|
|
138
|
+
* An export that announces each prefix, at exactly its length. It returns, in order:
|
|
139
|
+
*
|
|
140
|
+
* 1. `{ description, match: { prefix }, action: "accept" }` for each prefix;
|
|
141
|
+
* 2. `{ description: "Nothing else", action: "reject" }`, the unconditional reject `Checks.exportsEndInReject` looks for.
|
|
142
|
+
*
|
|
143
|
+
* The reject is there by default, so a forgotten one can't leak routes. `{ reject: false }` leaves it out, for an export that adds rules after these and ends in a reject of its own.
|
|
144
|
+
*/
|
|
145
|
+
static announce(prefixes: readonly Announced[], options: AnnounceOptions = {}): PolicyRule<never, never>[] {
|
|
131
146
|
return [
|
|
132
147
|
...prefixes.map(({ prefix, description }) => ({
|
|
133
148
|
...(description ? { description } : {}),
|
|
134
149
|
match: { prefix },
|
|
135
150
|
action: "accept" as const,
|
|
136
151
|
})),
|
|
137
|
-
|
|
152
|
+
...(options.reject === false ? [] : [Bgp.NOTHING_ELSE]),
|
|
138
153
|
];
|
|
139
154
|
}
|
|
140
155
|
|
|
156
|
+
/** The reject that ends a policy: nothing else gets through. */
|
|
157
|
+
private static readonly NOTHING_ELSE: PolicyRule<never, never> = { description: "Nothing else", action: "reject" };
|
|
158
|
+
|
|
141
159
|
/**
|
|
142
|
-
* An import policy that takes a neighbor's routes
|
|
160
|
+
* An import policy that takes a neighbor's routes. It returns these rules, in order, each only when its option is given:
|
|
143
161
|
*
|
|
144
|
-
*
|
|
162
|
+
* 1. the `before` rules, as written;
|
|
163
|
+
* 2. `{ call: sanity }`, or the rules of `Bgp.sanity()` written in when `sanity` is left out;
|
|
164
|
+
* 3. `{ set: { preferred_source: source } }` for each of `sources`, one per family;
|
|
165
|
+
* 4. `{ set: { local_pref } }`;
|
|
166
|
+
* 5. the tag: `{ description: "Tag 64500:1:1", set: { add_large_communities: ["64500:1:1"] } }` for `{ large: "64500:1:1" }`, `add_communities` for `{ standard }`, or the rule given;
|
|
167
|
+
* 6. `{ action: "accept" }`.
|
|
168
|
+
*
|
|
169
|
+
* So `Bgp.learn({ sanity: "SANITY", local_pref: 200, tag: { large: "64500:1:1" } })` is
|
|
170
|
+
* `[{ call: "SANITY" }, { set: { local_pref: 200 } }, { description: "Tag 64500:1:1", set: { add_large_communities: ["64500:1:1"] } }, { action: "accept" }]`.
|
|
171
|
+
*
|
|
172
|
+
* FortiOS refuses it: a route map stops at the first rule that matches, so it can't hold rules 3 to 5, which change a route and go on; and its sanity rules match RPKI, which FortiOS doesn't have. `validate` names each such rule. On FortiOS, write the import by hand with what rules 3 to 5 set in the rule that accepts.
|
|
145
173
|
*/
|
|
146
174
|
static learn<const P extends string = never>(options: LearnOptions<P> = {}): PolicyRule<P, never>[] {
|
|
147
175
|
const { tag } = options;
|
|
@@ -151,16 +179,22 @@ export class Bgp {
|
|
|
151
179
|
...(options.sanity ? [{ call: options.sanity }] : Bgp.sanity()),
|
|
152
180
|
...(options.sources ?? []).map((source) => ({ set: { preferred_source: source } })),
|
|
153
181
|
...(options.local_pref !== undefined ? [{ set: { local_pref: options.local_pref } }] : []),
|
|
154
|
-
...(tag === undefined ? [] : [
|
|
182
|
+
...(tag === undefined ? [] : ["standard" in tag ? Bgp.adding(tag.standard) : "large" in tag ? Bgp.adding(tag.large) : tag]),
|
|
155
183
|
{ action: "accept" },
|
|
156
184
|
];
|
|
157
185
|
}
|
|
158
186
|
|
|
159
187
|
/**
|
|
160
|
-
* Import rules that take the default route
|
|
188
|
+
* Import rules that take the default route from a transit. They are, in order:
|
|
189
|
+
*
|
|
190
|
+
* 1. `{ description, match: { prefix: "0.0.0.0/0" }, set: { local_pref, preferred_source }, action: "accept" }`;
|
|
191
|
+
* 2. the same for `::/0`;
|
|
192
|
+
* 3. with `{ reject: true }` only, `{ description: "Nothing else", action: "reject" }`.
|
|
193
|
+
*
|
|
194
|
+
* Without `reject`, any other route goes on to the rules after these, so `[...Bgp.defaultOnly(), ...Bgp.learn()]` takes both. Each of `sources` is the source address for the default of its own family; `set` holds only what is given.
|
|
161
195
|
*/
|
|
162
196
|
static defaultOnly(options: DefaultOnlyOptions = {}): PolicyRule<never, never>[] {
|
|
163
|
-
|
|
197
|
+
const defaults = Bgp.DEFAULTS.map(({ family, prefix }) => {
|
|
164
198
|
const source = options.sources?.find((address) => Address.family(address) === family);
|
|
165
199
|
const set = {
|
|
166
200
|
...(options.local_pref !== undefined ? { local_pref: options.local_pref } : {}),
|
|
@@ -174,6 +208,8 @@ export class Bgp {
|
|
|
174
208
|
action: "accept" as const,
|
|
175
209
|
};
|
|
176
210
|
});
|
|
211
|
+
|
|
212
|
+
return options.reject ? [...defaults, Bgp.NOTHING_ELSE] : defaults;
|
|
177
213
|
}
|
|
178
214
|
|
|
179
215
|
/** A rule adding one community, standard or large. */
|
|
@@ -211,8 +247,12 @@ export class Bgp {
|
|
|
211
247
|
/**
|
|
212
248
|
* Policy rules and a catalogue from one community scheme, so the communities your routers set, the ones they act on, and the ones you publish never disagree.
|
|
213
249
|
*
|
|
214
|
-
* - `tag(neighborClass, neighborAs, { site, trusted })`:
|
|
215
|
-
* - `actions(neighborAs
|
|
250
|
+
* - `tag(neighborClass, neighborAs, { site, trusted })`: one rule for an import policy, `{ description: "Learned from AS<n>", set }`. Its `set` removes your standard communities and your large ones (a `trusted` neighbor loses only the "where it came from" functions, so it keeps its requests: do not announce, prepend, blackhole), then adds `learned_from`, `learned_at` and `learned_from_as` where the scheme has them. `site` is needed when the scheme has `learned_at`.
|
|
251
|
+
* - `actions(neighborAs)`: the rules for an export policy toward one neighbor, before the rules that accept. They are `[...blackholeExport(n), ...rejectDoNotAnnounce(n), ...prepends(n), stripOurs()]`; toward a neighbor that takes blackholes (`blackhole.upstreams`), `rejectDoNotAnnounce(n)` comes first, so a route asked not to go there is never passed on as a blackhole.
|
|
252
|
+
* - `blackholeExport(neighborAs)`: a blackholed route goes no further, `Blackholed here, never announced`, unless `blackhole.upstreams` names the neighbor's own blackhole community: then it is passed on with it, so the neighbor drops the traffic too. Nothing without `blackhole`.
|
|
253
|
+
* - `rejectDoNotAnnounce(neighborAs)`: rejects a route asked not to be announced anywhere, then one asked not to be announced to this neighbor. Nothing without `do_not_announce`.
|
|
254
|
+
* - `prepends(neighborAs)`: prepends once, twice or three times toward this neighbor, where a route asks. Nothing without `prepend`.
|
|
255
|
+
* - `stripOurs()`: one rule removing every community of yours, `Our communities stay inside our network`.
|
|
216
256
|
* - `blackhole(prefixSets)`: rules for the import policy of a neighbor you trust with blackholes, before anything that rejects long prefixes. They take a route the neighbor asked to blackhole, but only inside `prefixSets`, its own space, and drop traffic to it here. One rule per set, so one call covers both families.
|
|
217
257
|
* - `catalogue`: every community, for `communities` in `defineNetwork`.
|
|
218
258
|
* - `community(fn, parameter)`: one community of yours, for a rule of your own.
|
|
@@ -291,67 +331,66 @@ export class Bgp {
|
|
|
291
331
|
}));
|
|
292
332
|
};
|
|
293
333
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
334
|
+
/** The neighbor's own blackhole community, when it takes blackholes from us. */
|
|
335
|
+
const passOnTo = (neighborAs: number) => scheme.blackhole?.upstreams?.[neighborAs];
|
|
336
|
+
|
|
337
|
+
const blackholeExport = (neighborAs: number): PolicyRule<never, never>[] => {
|
|
338
|
+
if (scheme.blackhole === undefined) return [];
|
|
339
|
+
|
|
340
|
+
const passOn = passOnTo(neighborAs);
|
|
341
|
+
const match = { large_community: community(scheme.blackhole.function, 0) };
|
|
342
|
+
|
|
343
|
+
return passOn
|
|
344
|
+
? [
|
|
345
|
+
{
|
|
346
|
+
description: `Blackhole passed on to AS${neighborAs}`,
|
|
347
|
+
match,
|
|
348
|
+
set: { remove_communities: ours.standard, remove_large_communities: ours.large, add_communities: [passOn] },
|
|
349
|
+
action: "accept",
|
|
350
|
+
},
|
|
351
|
+
]
|
|
352
|
+
: [{ description: "Blackholed here, never announced", match, action: "reject" }];
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const rejectDoNotAnnounce = (neighborAs: number): PolicyRule<never, never>[] => {
|
|
356
|
+
const fn = scheme.do_not_announce;
|
|
357
|
+
if (fn === undefined) return [];
|
|
297
358
|
|
|
298
359
|
return [
|
|
299
|
-
|
|
300
|
-
? [
|
|
301
|
-
{
|
|
302
|
-
description: "Blackholed here, never announced",
|
|
303
|
-
match: { large_community: community(scheme.blackhole.function, 0) },
|
|
304
|
-
action: "reject" as const,
|
|
305
|
-
},
|
|
306
|
-
]
|
|
307
|
-
: []),
|
|
308
|
-
...(scheme.do_not_announce !== undefined
|
|
309
|
-
? [
|
|
310
|
-
{
|
|
311
|
-
description: "Asked not to be announced anywhere",
|
|
312
|
-
match: { large_community: community(scheme.do_not_announce, 0) },
|
|
313
|
-
action: "reject" as const,
|
|
314
|
-
},
|
|
315
|
-
{
|
|
316
|
-
description: `Asked not to be announced to AS${neighborAs}`,
|
|
317
|
-
match: { large_community: community(scheme.do_not_announce, neighborAs) },
|
|
318
|
-
action: "reject" as const,
|
|
319
|
-
},
|
|
320
|
-
]
|
|
321
|
-
: []),
|
|
322
|
-
...(scheme.blackhole !== undefined && passOn
|
|
323
|
-
? [
|
|
324
|
-
{
|
|
325
|
-
description: `Blackhole passed on to AS${neighborAs}`,
|
|
326
|
-
match: { large_community: community(scheme.blackhole.function, 0) },
|
|
327
|
-
set: {
|
|
328
|
-
remove_communities: ours.standard,
|
|
329
|
-
remove_large_communities: ours.large,
|
|
330
|
-
add_communities: [passOn],
|
|
331
|
-
},
|
|
332
|
-
action: "accept" as const,
|
|
333
|
-
},
|
|
334
|
-
]
|
|
335
|
-
: []),
|
|
336
|
-
...Bgp.PREPENDS.flatMap(([name, times]) => {
|
|
337
|
-
const fn = scheme.prepend?.[name];
|
|
338
|
-
if (fn === undefined) return [];
|
|
339
|
-
|
|
340
|
-
return [
|
|
341
|
-
{
|
|
342
|
-
description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
|
|
343
|
-
match: { large_community: community(fn, neighborAs) },
|
|
344
|
-
set: { prepend: times },
|
|
345
|
-
},
|
|
346
|
-
];
|
|
347
|
-
}),
|
|
360
|
+
{ description: "Asked not to be announced anywhere", match: { large_community: community(fn, 0) }, action: "reject" },
|
|
348
361
|
{
|
|
349
|
-
description:
|
|
350
|
-
|
|
362
|
+
description: `Asked not to be announced to AS${neighborAs}`,
|
|
363
|
+
match: { large_community: community(fn, neighborAs) },
|
|
364
|
+
action: "reject",
|
|
351
365
|
},
|
|
352
366
|
];
|
|
353
367
|
};
|
|
354
368
|
|
|
369
|
+
const prepends = (neighborAs: number): PolicyRule<never, never>[] =>
|
|
370
|
+
Bgp.PREPENDS.flatMap(([name, times]) => {
|
|
371
|
+
const fn = scheme.prepend?.[name];
|
|
372
|
+
if (fn === undefined) return [];
|
|
373
|
+
|
|
374
|
+
return [
|
|
375
|
+
{
|
|
376
|
+
description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
|
|
377
|
+
match: { large_community: community(fn, neighborAs) },
|
|
378
|
+
set: { prepend: times },
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const stripOurs = (): PolicyRule<never, never> => ({
|
|
384
|
+
description: "Our communities stay inside our network",
|
|
385
|
+
set: { remove_communities: ours.standard, remove_large_communities: ours.large },
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// A route asked not to be announced to a neighbor is never passed on to it as a blackhole either; where nothing is passed on, both only reject.
|
|
389
|
+
const actions = (neighborAs: number): PolicyRule<never, never>[] =>
|
|
390
|
+
passOnTo(neighborAs)
|
|
391
|
+
? [...rejectDoNotAnnounce(neighborAs), ...blackholeExport(neighborAs), ...prepends(neighborAs), stripOurs()]
|
|
392
|
+
: [...blackholeExport(neighborAs), ...rejectDoNotAnnounce(neighborAs), ...prepends(neighborAs), stripOurs()];
|
|
393
|
+
|
|
355
394
|
const catalogue: Community[] = [
|
|
356
395
|
...Object.entries<number>(scheme.learned_from?.classes ?? {}).map(([name, value]) => ({
|
|
357
396
|
community: written(scheme.learned_from!.function, value),
|
|
@@ -393,7 +432,18 @@ export class Bgp {
|
|
|
393
432
|
: []),
|
|
394
433
|
];
|
|
395
434
|
|
|
396
|
-
return {
|
|
435
|
+
return {
|
|
436
|
+
community,
|
|
437
|
+
learned_from: learnedFrom,
|
|
438
|
+
tag,
|
|
439
|
+
actions,
|
|
440
|
+
blackholeExport,
|
|
441
|
+
rejectDoNotAnnounce,
|
|
442
|
+
prepends,
|
|
443
|
+
stripOurs,
|
|
444
|
+
blackhole,
|
|
445
|
+
catalogue,
|
|
446
|
+
};
|
|
397
447
|
}
|
|
398
448
|
|
|
399
449
|
/** A word with its article: "a customer", "an upstream". */
|
package/src/helpers/checks.ts
CHANGED
|
@@ -41,6 +41,29 @@ export class Checks {
|
|
|
41
41
|
return findings;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Every BGP import ends in a rule that decides for every route, an accept or a reject without a match, so what the import takes is written in it and not left to the platform's default.
|
|
46
|
+
*/
|
|
47
|
+
static readonly importsEndInAcceptOrReject: Check = (devices) => {
|
|
48
|
+
const findings: Finding[] = [];
|
|
49
|
+
|
|
50
|
+
for (const device of devices) {
|
|
51
|
+
for (const neighbor of Bgp.neighborPolicies(device)) {
|
|
52
|
+
const last = neighbor.import ? device.policies?.[neighbor.import]?.at(-1) : undefined;
|
|
53
|
+
|
|
54
|
+
if (neighbor.import && (!last?.action || last.match)) {
|
|
55
|
+
findings.push({
|
|
56
|
+
level: "error",
|
|
57
|
+
device: device.name,
|
|
58
|
+
message: `${neighbor.name}: import ${neighbor.import} does not end in an unconditional accept or reject`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return findings;
|
|
65
|
+
};
|
|
66
|
+
|
|
44
67
|
/** A GRE or VXLAN tunnel's local address is outside what the neighbors over it are offered, or their replies route back into the tunnel. */
|
|
45
68
|
static readonly tunnelsOutsideOffered: Check = (devices) => {
|
|
46
69
|
const findings: Finding[] = [];
|
package/src/helpers/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Building blocks most networks write the same way: address lists, cables written once for both ends, edge filters, a BGP import sanity policy, an exchange's member ports and route servers, upstreams written once, checks, and helpers for checks of your own. Each returns plain config to place where it belongs, so nothing here applies itself.
|
|
2
2
|
|
|
3
3
|
export { Bgp } from "./bgp";
|
|
4
|
-
export type { Announced, CommunityScheme, DefaultOnlyOptions, LearnOptions, SanityLengths } from "./bgp";
|
|
4
|
+
export type { AnnounceOptions, Announced, CommunityScheme, DefaultOnlyOptions, LearnOptions, SanityLengths } from "./bgp";
|
|
5
5
|
export { Cables } from "./cables";
|
|
6
6
|
export type { FarEnd } from "./cables";
|
|
7
7
|
export { Checks } from "./checks";
|
|
@@ -11,6 +11,6 @@ export type { ExchangeMember, InternetExchange } from "./exchange";
|
|
|
11
11
|
export { Filters } from "./filters";
|
|
12
12
|
export { RouteServerConfig } from "./route-server";
|
|
13
13
|
export { Upstreams } from "./upstreams";
|
|
14
|
-
export type { Upstream, UpstreamCatalog,
|
|
14
|
+
export type { NamedVlan, Upstream, UpstreamCatalog, UpstreamOptions, UpstreamPolicy } from "./upstreams";
|
|
15
15
|
export type { RouteServer } from "./route-server";
|
|
16
16
|
export type { AntiAmplificationOptions } from "./filters";
|