@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
package/dist/helpers/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Catalog
|
|
3
|
+
} from "../chunks/index-6wb41q67.js";
|
|
1
4
|
import {
|
|
2
5
|
Address2
|
|
3
6
|
} from "../chunks/index-n7w3tg5y.js";
|
|
@@ -40,16 +43,17 @@ class Bgp {
|
|
|
40
43
|
{ description: "RPKI invalid", match: { rpki: "invalid" }, action: "reject" }
|
|
41
44
|
];
|
|
42
45
|
}
|
|
43
|
-
static announce(prefixes) {
|
|
46
|
+
static announce(prefixes, options = {}) {
|
|
44
47
|
return [
|
|
45
48
|
...prefixes.map(({ prefix, description }) => ({
|
|
46
49
|
...description ? { description } : {},
|
|
47
50
|
match: { prefix },
|
|
48
51
|
action: "accept"
|
|
49
52
|
})),
|
|
50
|
-
|
|
53
|
+
...options.reject === false ? [] : [Bgp.NOTHING_ELSE]
|
|
51
54
|
];
|
|
52
55
|
}
|
|
56
|
+
static NOTHING_ELSE = { description: "Nothing else", action: "reject" };
|
|
53
57
|
static learn(options = {}) {
|
|
54
58
|
const { tag } = options;
|
|
55
59
|
return [
|
|
@@ -57,12 +61,12 @@ class Bgp {
|
|
|
57
61
|
...options.sanity ? [{ call: options.sanity }] : Bgp.sanity(),
|
|
58
62
|
...(options.sources ?? []).map((source) => ({ set: { preferred_source: source } })),
|
|
59
63
|
...options.local_pref !== undefined ? [{ set: { local_pref: options.local_pref } }] : [],
|
|
60
|
-
...tag === undefined ? [] : [
|
|
64
|
+
...tag === undefined ? [] : ["standard" in tag ? Bgp.adding(tag.standard) : ("large" in tag) ? Bgp.adding(tag.large) : tag],
|
|
61
65
|
{ action: "accept" }
|
|
62
66
|
];
|
|
63
67
|
}
|
|
64
68
|
static defaultOnly(options = {}) {
|
|
65
|
-
|
|
69
|
+
const defaults = Bgp.DEFAULTS.map(({ family, prefix }) => {
|
|
66
70
|
const source = options.sources?.find((address) => Address2.family(address) === family);
|
|
67
71
|
const set = {
|
|
68
72
|
...options.local_pref !== undefined ? { local_pref: options.local_pref } : {},
|
|
@@ -75,6 +79,7 @@ class Bgp {
|
|
|
75
79
|
action: "accept"
|
|
76
80
|
};
|
|
77
81
|
});
|
|
82
|
+
return options.reject ? [...defaults, Bgp.NOTHING_ELSE] : defaults;
|
|
78
83
|
}
|
|
79
84
|
static adding(community) {
|
|
80
85
|
const large = community.split(":").length === 3;
|
|
@@ -142,58 +147,51 @@ class Bgp {
|
|
|
142
147
|
action: "accept"
|
|
143
148
|
}));
|
|
144
149
|
};
|
|
145
|
-
const
|
|
146
|
-
|
|
150
|
+
const passOnTo = (neighborAs) => scheme.blackhole?.upstreams?.[neighborAs];
|
|
151
|
+
const blackholeExport = (neighborAs) => {
|
|
152
|
+
if (scheme.blackhole === undefined)
|
|
153
|
+
return [];
|
|
154
|
+
const passOn = passOnTo(neighborAs);
|
|
155
|
+
const match = { large_community: community(scheme.blackhole.function, 0) };
|
|
156
|
+
return passOn ? [
|
|
157
|
+
{
|
|
158
|
+
description: `Blackhole passed on to AS${neighborAs}`,
|
|
159
|
+
match,
|
|
160
|
+
set: { remove_communities: ours.standard, remove_large_communities: ours.large, add_communities: [passOn] },
|
|
161
|
+
action: "accept"
|
|
162
|
+
}
|
|
163
|
+
] : [{ description: "Blackholed here, never announced", match, action: "reject" }];
|
|
164
|
+
};
|
|
165
|
+
const rejectDoNotAnnounce = (neighborAs) => {
|
|
166
|
+
const fn = scheme.do_not_announce;
|
|
167
|
+
if (fn === undefined)
|
|
168
|
+
return [];
|
|
147
169
|
return [
|
|
148
|
-
|
|
149
|
-
{
|
|
150
|
-
description: "Blackholed here, never announced",
|
|
151
|
-
match: { large_community: community(scheme.blackhole.function, 0) },
|
|
152
|
-
action: "reject"
|
|
153
|
-
}
|
|
154
|
-
] : [],
|
|
155
|
-
...scheme.do_not_announce !== undefined ? [
|
|
156
|
-
{
|
|
157
|
-
description: "Asked not to be announced anywhere",
|
|
158
|
-
match: { large_community: community(scheme.do_not_announce, 0) },
|
|
159
|
-
action: "reject"
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
description: `Asked not to be announced to AS${neighborAs}`,
|
|
163
|
-
match: { large_community: community(scheme.do_not_announce, neighborAs) },
|
|
164
|
-
action: "reject"
|
|
165
|
-
}
|
|
166
|
-
] : [],
|
|
167
|
-
...scheme.blackhole !== undefined && passOn ? [
|
|
168
|
-
{
|
|
169
|
-
description: `Blackhole passed on to AS${neighborAs}`,
|
|
170
|
-
match: { large_community: community(scheme.blackhole.function, 0) },
|
|
171
|
-
set: {
|
|
172
|
-
remove_communities: ours.standard,
|
|
173
|
-
remove_large_communities: ours.large,
|
|
174
|
-
add_communities: [passOn]
|
|
175
|
-
},
|
|
176
|
-
action: "accept"
|
|
177
|
-
}
|
|
178
|
-
] : [],
|
|
179
|
-
...Bgp.PREPENDS.flatMap(([name, times]) => {
|
|
180
|
-
const fn = scheme.prepend?.[name];
|
|
181
|
-
if (fn === undefined)
|
|
182
|
-
return [];
|
|
183
|
-
return [
|
|
184
|
-
{
|
|
185
|
-
description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
|
|
186
|
-
match: { large_community: community(fn, neighborAs) },
|
|
187
|
-
set: { prepend: times }
|
|
188
|
-
}
|
|
189
|
-
];
|
|
190
|
-
}),
|
|
170
|
+
{ description: "Asked not to be announced anywhere", match: { large_community: community(fn, 0) }, action: "reject" },
|
|
191
171
|
{
|
|
192
|
-
description:
|
|
193
|
-
|
|
172
|
+
description: `Asked not to be announced to AS${neighborAs}`,
|
|
173
|
+
match: { large_community: community(fn, neighborAs) },
|
|
174
|
+
action: "reject"
|
|
194
175
|
}
|
|
195
176
|
];
|
|
196
177
|
};
|
|
178
|
+
const prepends = (neighborAs) => Bgp.PREPENDS.flatMap(([name, times]) => {
|
|
179
|
+
const fn = scheme.prepend?.[name];
|
|
180
|
+
if (fn === undefined)
|
|
181
|
+
return [];
|
|
182
|
+
return [
|
|
183
|
+
{
|
|
184
|
+
description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
|
|
185
|
+
match: { large_community: community(fn, neighborAs) },
|
|
186
|
+
set: { prepend: times }
|
|
187
|
+
}
|
|
188
|
+
];
|
|
189
|
+
});
|
|
190
|
+
const stripOurs = () => ({
|
|
191
|
+
description: "Our communities stay inside our network",
|
|
192
|
+
set: { remove_communities: ours.standard, remove_large_communities: ours.large }
|
|
193
|
+
});
|
|
194
|
+
const actions = (neighborAs) => passOnTo(neighborAs) ? [...rejectDoNotAnnounce(neighborAs), ...blackholeExport(neighborAs), ...prepends(neighborAs), stripOurs()] : [...blackholeExport(neighborAs), ...rejectDoNotAnnounce(neighborAs), ...prepends(neighborAs), stripOurs()];
|
|
197
195
|
const catalogue = [
|
|
198
196
|
...Object.entries(scheme.learned_from?.classes ?? {}).map(([name, value]) => ({
|
|
199
197
|
community: written(scheme.learned_from.function, value),
|
|
@@ -222,7 +220,18 @@ class Bgp {
|
|
|
222
220
|
}
|
|
223
221
|
] : []
|
|
224
222
|
];
|
|
225
|
-
return {
|
|
223
|
+
return {
|
|
224
|
+
community,
|
|
225
|
+
learned_from: learnedFrom,
|
|
226
|
+
tag,
|
|
227
|
+
actions,
|
|
228
|
+
blackholeExport,
|
|
229
|
+
rejectDoNotAnnounce,
|
|
230
|
+
prepends,
|
|
231
|
+
stripOurs,
|
|
232
|
+
blackhole,
|
|
233
|
+
catalogue
|
|
234
|
+
};
|
|
226
235
|
}
|
|
227
236
|
static article(word) {
|
|
228
237
|
return /^[aeiou]/i.test(word) ? `an ${word}` : `a ${word}`;
|
|
@@ -232,8 +241,8 @@ class Bgp {
|
|
|
232
241
|
class Cables {
|
|
233
242
|
static link(spec) {
|
|
234
243
|
return {
|
|
235
|
-
a: { [spec.a.port]: Cables.end(spec, spec.b
|
|
236
|
-
b: { [spec.b.port]: Cables.end(spec, spec.a
|
|
244
|
+
a: { [spec.a.port]: Cables.end(spec, spec.b, spec.b.port) },
|
|
245
|
+
b: { [spec.b.port]: Cables.end(spec, spec.a, spec.a.port) }
|
|
237
246
|
};
|
|
238
247
|
}
|
|
239
248
|
static lag(spec) {
|
|
@@ -245,17 +254,18 @@ class Cables {
|
|
|
245
254
|
throw new Error(`Cables.lag: ${spec.b.device} ${far} is cabled to both ${other} and ${near} on ${spec.a.device}`);
|
|
246
255
|
taken.set(far, near);
|
|
247
256
|
}
|
|
248
|
-
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b
|
|
249
|
-
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a
|
|
257
|
+
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b, far), lag: spec.a.lag }]));
|
|
258
|
+
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a, near), lag: spec.b.lag }]));
|
|
250
259
|
return {
|
|
251
260
|
a,
|
|
252
261
|
b
|
|
253
262
|
};
|
|
254
263
|
}
|
|
255
|
-
static end(shared,
|
|
264
|
+
static end(shared, far, port) {
|
|
265
|
+
const label = far.platform && far.model ? Catalog.spell(far.platform, far.model, port) : undefined;
|
|
256
266
|
return {
|
|
257
|
-
link: { device, port, ...shared.through ? { through: shared.through } : {} },
|
|
258
|
-
...shared.describe ? { description: shared.describe({ device, port }) } : {}
|
|
267
|
+
link: { device: far.device, port, ...shared.through ? { through: shared.through } : {} },
|
|
268
|
+
...shared.describe ? { description: shared.describe({ device: far.device, port, ...label ? { label } : {} }) } : {}
|
|
259
269
|
};
|
|
260
270
|
}
|
|
261
271
|
}
|
|
@@ -279,6 +289,22 @@ class Checks {
|
|
|
279
289
|
}
|
|
280
290
|
return findings;
|
|
281
291
|
};
|
|
292
|
+
static importsEndInAcceptOrReject = (devices) => {
|
|
293
|
+
const findings = [];
|
|
294
|
+
for (const device of devices) {
|
|
295
|
+
for (const neighbor of Bgp.neighborPolicies(device)) {
|
|
296
|
+
const last = neighbor.import ? device.policies?.[neighbor.import]?.at(-1) : undefined;
|
|
297
|
+
if (neighbor.import && (!last?.action || last.match)) {
|
|
298
|
+
findings.push({
|
|
299
|
+
level: "error",
|
|
300
|
+
device: device.name,
|
|
301
|
+
message: `${neighbor.name}: import ${neighbor.import} does not end in an unconditional accept or reject`
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return findings;
|
|
307
|
+
};
|
|
282
308
|
static tunnelsOutsideOffered = (devices) => {
|
|
283
309
|
const findings = [];
|
|
284
310
|
for (const device of devices) {
|
|
@@ -538,8 +564,8 @@ class Exchange {
|
|
|
538
564
|
result[port] = {
|
|
539
565
|
description: `${member.name}, AS${member.asn}`,
|
|
540
566
|
access_vlan: exchange.vlan,
|
|
541
|
-
stp: false,
|
|
542
|
-
lldp: false,
|
|
567
|
+
stp: exchange.stp ?? false,
|
|
568
|
+
lldp: exchange.lldp ?? false,
|
|
543
569
|
storm_control: { broadcast: { percent }, multicast: { percent }, unknown_unicast: { percent } },
|
|
544
570
|
...exchange.mac_limit !== undefined ? { mac_limit: exchange.mac_limit } : {},
|
|
545
571
|
...exchange.ethertypes ? { ethertypes: exchange.ethertypes } : {}
|
|
@@ -664,69 +690,48 @@ class Filters {
|
|
|
664
690
|
}
|
|
665
691
|
}
|
|
666
692
|
// src/helpers/upstreams.ts
|
|
667
|
-
var DEFAULTS = {
|
|
668
|
-
vlan: "{key}",
|
|
669
|
-
native: "{key}-native",
|
|
670
|
-
interface: "{key}",
|
|
671
|
-
import: "{KEY}-IN",
|
|
672
|
-
export: "{KEY}-OUT",
|
|
673
|
-
group: "{key}"
|
|
674
|
-
};
|
|
675
|
-
|
|
676
693
|
class Upstreams {
|
|
677
694
|
static define(catalog, options = {}) {
|
|
678
|
-
const names = options.names ?? {};
|
|
679
695
|
const all = Object.entries(catalog);
|
|
680
696
|
const nameOf = (key, upstream) => upstream.name ?? key;
|
|
681
|
-
const
|
|
682
|
-
const named = (field, key, upstream) => {
|
|
683
|
-
const given = names[field];
|
|
684
|
-
const pattern = given === undefined || typeof given === "string" ? given ?? DEFAULTS[field] : (upstream.tunnel && "tunnel" in given ? given.tunnel : given[upstream.role]) ?? DEFAULTS[field];
|
|
685
|
-
return pattern.replaceAll("{key}", key).replaceAll("{KEY}", key.toUpperCase()).replaceAll("{number}", () => {
|
|
686
|
-
const number = upstream.handoff?.number ?? upstream.tunnel?.number;
|
|
687
|
-
if (number === undefined)
|
|
688
|
-
throw new Error(`Upstreams: ${key} needs a number for ${field} name ${pattern}`);
|
|
689
|
-
return String(number);
|
|
690
|
-
});
|
|
691
|
-
};
|
|
692
|
-
for (const field of Object.keys(DEFAULTS)) {
|
|
697
|
+
const unique = (what, names) => {
|
|
693
698
|
const taken = new Map;
|
|
694
|
-
for (const [key,
|
|
695
|
-
if (
|
|
696
|
-
continue;
|
|
697
|
-
if (field === "import" && !upstream.import)
|
|
698
|
-
continue;
|
|
699
|
-
if (field === "group" && !grouped(upstream))
|
|
700
|
-
continue;
|
|
701
|
-
if (field === "vlan" && !upstream.handoff || field === "interface" && !upstream.handoff && !upstream.tunnel)
|
|
699
|
+
for (const [key, name] of names) {
|
|
700
|
+
if (name === undefined)
|
|
702
701
|
continue;
|
|
703
|
-
const name = named(field, key, upstream);
|
|
704
702
|
const other = taken.get(name);
|
|
705
703
|
if (other)
|
|
706
|
-
throw new Error(`Upstreams: ${other} and ${key} both have the ${
|
|
704
|
+
throw new Error(`Upstreams: ${other} and ${key} both have the ${what} ${name}`);
|
|
707
705
|
taken.set(name, key);
|
|
708
706
|
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
707
|
+
};
|
|
708
|
+
unique("VLAN", [
|
|
709
|
+
...all.map(([key, upstream]) => [key, upstream.handoff?.vlan.name]),
|
|
710
|
+
...all.map(([key, upstream]) => [key, upstream.handoff?.native?.name])
|
|
711
|
+
]);
|
|
712
|
+
unique("interface", all.map(([key, upstream]) => [key, upstream.handoff?.interface ?? upstream.tunnel?.interface]));
|
|
713
|
+
unique("policy", [
|
|
714
|
+
...all.map(([key, upstream]) => [key, upstream.import?.name]),
|
|
715
|
+
...all.map(([key, upstream]) => [key, upstream.export.name])
|
|
716
|
+
]);
|
|
717
|
+
unique("BGP group", all.map(([key, upstream]) => [key, upstream.group]));
|
|
713
718
|
const vlans = (device) => {
|
|
714
719
|
const result = {};
|
|
715
720
|
for (const [key, upstream] of all) {
|
|
716
721
|
const handoff = upstream.handoff;
|
|
717
722
|
if (!handoff || device !== undefined && handoff.port?.device !== device)
|
|
718
723
|
continue;
|
|
719
|
-
result[vlan
|
|
720
|
-
if (
|
|
721
|
-
result[native
|
|
722
|
-
id: handoff.native,
|
|
724
|
+
result[handoff.vlan.name] = { id: handoff.vlan.id, description: nameOf(key, upstream) };
|
|
725
|
+
if (handoff.native) {
|
|
726
|
+
result[handoff.native.name] = {
|
|
727
|
+
id: handoff.native.id,
|
|
723
728
|
description: `Native toward ${nameOf(key, upstream)}, nothing else in it`
|
|
724
729
|
};
|
|
725
730
|
}
|
|
726
731
|
}
|
|
727
732
|
return result;
|
|
728
733
|
};
|
|
729
|
-
const vlansOn = (device) => all.
|
|
734
|
+
const vlansOn = (device) => all.flatMap(([, upstream]) => upstream.handoff?.port?.device === device ? [upstream.handoff.vlan.name] : []);
|
|
730
735
|
const ports = (device) => {
|
|
731
736
|
const result = {};
|
|
732
737
|
for (const [key, upstream] of all) {
|
|
@@ -734,10 +739,10 @@ class Upstreams {
|
|
|
734
739
|
if (!handoff?.port || handoff.port.device !== device)
|
|
735
740
|
continue;
|
|
736
741
|
result[handoff.port.port] = {
|
|
737
|
-
description:
|
|
738
|
-
...handoff.native
|
|
739
|
-
lldp: false,
|
|
740
|
-
stp: false,
|
|
742
|
+
description: nameOf(key, upstream),
|
|
743
|
+
...handoff.native ? { trunk_vlans: [handoff.vlan.name], native_vlan: handoff.native.name } : { access_vlan: handoff.vlan.name },
|
|
744
|
+
lldp: handoff.port.lldp ?? false,
|
|
745
|
+
stp: handoff.port.stp ?? false,
|
|
741
746
|
...handoff.port.storm_control ? { storm_control: handoff.port.storm_control } : {}
|
|
742
747
|
};
|
|
743
748
|
}
|
|
@@ -747,82 +752,64 @@ class Upstreams {
|
|
|
747
752
|
const result = {};
|
|
748
753
|
for (const [key, upstream] of all) {
|
|
749
754
|
const description = nameOf(key, upstream);
|
|
750
|
-
const name = named("interface", key, upstream);
|
|
751
755
|
if (upstream.tunnel) {
|
|
752
|
-
const { local, remote, addresses, mtu } = upstream.tunnel;
|
|
756
|
+
const { interface: name, local, remote, addresses, mtu } = upstream.tunnel;
|
|
753
757
|
result[name] = { type: "gre", description, local, remote, addresses, ...mtu ? { mtu } : {} };
|
|
754
758
|
} else if (upstream.handoff) {
|
|
755
|
-
const { addresses, exchange } = upstream.handoff;
|
|
756
|
-
result[name] = { type: "vlan", vlan: vlan
|
|
759
|
+
const { interface: name, vlan, addresses, exchange } = upstream.handoff;
|
|
760
|
+
result[name] = { type: "vlan", vlan: vlan.name, description, addresses, ...exchange ? { exchange } : {} };
|
|
757
761
|
}
|
|
758
762
|
}
|
|
759
763
|
return result;
|
|
760
764
|
};
|
|
761
|
-
const uplinks = () => all.
|
|
762
|
-
const settings = (key, upstream) =>
|
|
763
|
-
remote_as: upstream.asn,
|
|
764
|
-
description: nameOf(key, upstream),
|
|
765
|
-
role: upstream.role,
|
|
766
|
-
...upstream.import ? { import: policy("import", key, upstream) } : {},
|
|
767
|
-
export: policy("export", key, upstream)
|
|
768
|
-
});
|
|
769
|
-
const groups = () => Object.fromEntries(all.filter(([, upstream]) => grouped(upstream)).map(([key, upstream]) => {
|
|
765
|
+
const uplinks = () => all.flatMap(([, upstream]) => upstream.handoff ? [upstream.handoff.interface] : []);
|
|
766
|
+
const settings = (key, upstream) => {
|
|
770
767
|
const multihop = upstream.multihop;
|
|
771
|
-
return
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
768
|
+
return {
|
|
769
|
+
remote_as: upstream.asn,
|
|
770
|
+
description: nameOf(key, upstream),
|
|
771
|
+
role: upstream.role,
|
|
772
|
+
...upstream.import ? { import: upstream.import.name } : {},
|
|
773
|
+
export: upstream.export.name,
|
|
774
|
+
...upstream.table ? { table: upstream.table } : {},
|
|
775
|
+
...multihop ? {
|
|
776
|
+
local_address: multihop.local_address,
|
|
777
|
+
multihop: true,
|
|
778
|
+
...multihop.families ? { families: multihop.families } : {},
|
|
779
|
+
...multihop.password ? { password: multihop.password } : {}
|
|
780
|
+
} : {}
|
|
781
|
+
};
|
|
782
|
+
};
|
|
783
|
+
const groups = () => Object.fromEntries(all.flatMap(([key, upstream]) => upstream.group ? [[upstream.group, settings(key, upstream)]] : []));
|
|
784
784
|
const neighbors = () => Object.fromEntries(all.flatMap(([key, upstream]) => Object.entries(upstream.sessions).map(([name, session]) => {
|
|
785
785
|
const limit = upstream.limits?.[Address2.family(session.address)];
|
|
786
786
|
return [
|
|
787
787
|
name,
|
|
788
788
|
{
|
|
789
|
+
...upstream.group ? { group: upstream.group } : settings(key, upstream),
|
|
789
790
|
...session,
|
|
790
|
-
...grouped(upstream) ? { group: named("group", key, upstream) } : settings(key, upstream),
|
|
791
791
|
...limit ? { max_prefixes: limit } : {}
|
|
792
792
|
}
|
|
793
793
|
];
|
|
794
794
|
})));
|
|
795
|
-
const policies = (
|
|
795
|
+
const policies = () => {
|
|
796
796
|
const result = {};
|
|
797
797
|
for (const [key, upstream] of all) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
if (!taken)
|
|
805
|
-
continue;
|
|
806
|
-
const tag = options.tag?.(key, upstream);
|
|
807
|
-
result[policy("import", key, upstream)] = [
|
|
808
|
-
...taken.default ? Bgp.defaultOnly({ ...taken.default, ...options.sources ? { sources: options.sources } : {} }) : [],
|
|
809
|
-
...taken.learn ? Bgp.learn({
|
|
810
|
-
...taken.learn.before ? { before: taken.learn.before } : {},
|
|
811
|
-
...options.sanity ? { sanity: options.sanity } : {},
|
|
812
|
-
...options.sources ? { sources: options.sources } : {},
|
|
813
|
-
...taken.learn.local_pref !== undefined ? { local_pref: taken.learn.local_pref } : {},
|
|
814
|
-
...tag !== undefined ? { tag } : {}
|
|
815
|
-
}) : [{ description: "Nothing else", action: "reject" }]
|
|
816
|
-
];
|
|
798
|
+
if (upstream.import) {
|
|
799
|
+
const rules = upstream.import.rules;
|
|
800
|
+
result[upstream.import.name] = options.mapImports ? options.mapImports(rules, key, upstream) : rules;
|
|
801
|
+
}
|
|
802
|
+
const rules = upstream.export.rules;
|
|
803
|
+
result[upstream.export.name] = options.mapExports ? options.mapExports(rules, key, upstream) : rules;
|
|
817
804
|
}
|
|
818
805
|
return result;
|
|
819
806
|
};
|
|
820
807
|
const untrusted = () => [
|
|
821
|
-
...all.
|
|
808
|
+
...all.flatMap(([, upstream]) => upstream.handoff ? [upstream.handoff.vlan.name] : []),
|
|
822
809
|
...natives()
|
|
823
810
|
];
|
|
824
|
-
const natives = () => all.
|
|
825
|
-
const privateDrops = () => all.
|
|
811
|
+
const natives = () => all.flatMap(([, upstream]) => upstream.handoff?.native ? [upstream.handoff.native.name] : []);
|
|
812
|
+
const privateDrops = () => all.flatMap(([, upstream]) => upstream.handoff && upstream.drop_private ? [upstream.handoff.vlan.name] : []);
|
|
826
813
|
const tunnelRules = () => all.flatMap(([key, upstream]) => upstream.tunnel ? [
|
|
827
814
|
{
|
|
828
815
|
description: `GRE from ${nameOf(key, upstream)}`,
|
|
@@ -31,12 +31,6 @@ export declare class Cabling {
|
|
|
31
31
|
* Only ports where both ends run LLDP, by the config, are compared, and only on devices whose neighbors were read. A link `through` a carrier is not compared. LLDP can be off at the far end, so each is a warning.
|
|
32
32
|
*/
|
|
33
33
|
static compare(devices: readonly Device[], neighbors: ReadonlyMap<string, readonly Neighbor[]>): Finding[];
|
|
34
|
-
/**
|
|
35
|
-
* Each linked port without a description gets one from its far end: the far device and the far port as printed on it, such as `firewall-1 port6`. The far device's own file has its platform and model, so the label is never written twice. A port whose far device is not in the network keeps none.
|
|
36
|
-
*/
|
|
37
|
-
static describeLinks(devices: readonly Device[]): Device[];
|
|
38
|
-
/** A port as printed on the device, or Circuit's name for it when the model has no spelling for it. */
|
|
39
|
-
private static labelOf;
|
|
40
34
|
/** A system name is the device's name, alone or with a domain after it. */
|
|
41
35
|
private static isDevice;
|
|
42
36
|
private static deviceOf;
|
|
@@ -9,7 +9,7 @@ export declare const secretFile: (path: string) => Secret;
|
|
|
9
9
|
*
|
|
10
10
|
* The names it declares (VLANs, interfaces, policies and the rest) are the only names its other fields accept, and the editor suggests them. The port names come from the model.
|
|
11
11
|
*/
|
|
12
|
-
export declare function defineDevice<const Platform extends PlatformName, const Model extends ModelName<Platform>, const Vlans extends string = never, const Interfaces extends string = never, const Policies extends string = never, const PrefixSets extends string = never, const BgpGroups extends string = never, const Acls extends string = never, const AddressSets extends string = never, const Users extends string = never, const Vrfs extends string = never, const Certificates extends string = never>(device: Device<Platform, Model, PortName<Platform, Model>, Vlans, Interfaces, Policies, PrefixSets, BgpGroups, Acls, AddressSets, Users, Vrfs, Certificates>): Device;
|
|
12
|
+
export declare function defineDevice<const Platform extends PlatformName, const Model extends ModelName<Platform>, const Vlans extends string = never, const Interfaces extends string = never, const Policies extends string = never, const PrefixSets extends string = never, const BgpGroups extends string = never, const Acls extends string = never, const AddressSets extends string = never, const Users extends string = never, const Vrfs extends string = never, const Certificates extends string = never, const Tables extends string = never>(device: Device<Platform, Model, PortName<Platform, Model>, Vlans, Interfaces, Policies, PrefixSets, BgpGroups, Acls, AddressSets, Users, Vrfs, Certificates, Tables>): Device;
|
|
13
13
|
/** Something a check found. */
|
|
14
14
|
export type Finding = {
|
|
15
15
|
/** An error stops `diff` and `apply`; a warning is reported only. */
|
|
@@ -14,6 +14,8 @@ export declare class Validator {
|
|
|
14
14
|
private static checkFrames;
|
|
15
15
|
private static checkInterfaces;
|
|
16
16
|
private static checkRouting;
|
|
17
|
+
/** Routing tables have names of their own and something installing routes in them, and rules name a table and can each be reached. */
|
|
18
|
+
private static checkTables;
|
|
17
19
|
private static checkPolicies;
|
|
18
20
|
private static checkFirewall;
|
|
19
21
|
private static checkServices;
|
|
@@ -28,8 +28,12 @@ export type LearnOptions<P extends string> = {
|
|
|
28
28
|
sources?: readonly IP[];
|
|
29
29
|
/** Preference for the routes learned. Higher wins. */
|
|
30
30
|
local_pref?: number;
|
|
31
|
-
/** Where the route came from:
|
|
32
|
-
tag?:
|
|
31
|
+
/** 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. */
|
|
32
|
+
tag?: {
|
|
33
|
+
standard: StandardCommunity;
|
|
34
|
+
} | {
|
|
35
|
+
large: LargeCommunity;
|
|
36
|
+
} | PolicyRule<never, never>;
|
|
33
37
|
};
|
|
34
38
|
/** What `Bgp.defaultOnly` sets on the default routes it takes. */
|
|
35
39
|
export type DefaultOnlyOptions = {
|
|
@@ -39,6 +43,13 @@ export type DefaultOnlyOptions = {
|
|
|
39
43
|
description?: string;
|
|
40
44
|
/** The source address for the default of its family, one per family. */
|
|
41
45
|
sources?: readonly IP[];
|
|
46
|
+
/** 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. */
|
|
47
|
+
reject?: boolean;
|
|
48
|
+
};
|
|
49
|
+
/** What `Bgp.announce` writes after the accepts. */
|
|
50
|
+
export type AnnounceOptions = {
|
|
51
|
+
/** 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. */
|
|
52
|
+
reject?: boolean;
|
|
42
53
|
};
|
|
43
54
|
/** Kinds of neighbor and the number each is tagged with, such as `{ transit: 1, customer: 3 }`. */
|
|
44
55
|
export type CommunityClasses = Readonly<Record<string, number>>;
|
|
@@ -89,16 +100,41 @@ export declare class Bgp {
|
|
|
89
100
|
private static readonly DEFAULTS;
|
|
90
101
|
/** Route policy rules rejecting what no neighbor should send: prefixes too short or too long for the global table, and RPKI invalid. Place it first in an import, or as a policy other imports call. */
|
|
91
102
|
static sanity(lengths?: SanityLengths): PolicyRule<never, never>[];
|
|
92
|
-
/** An export policy that announces each prefix, at exactly its length, then rejects everything else. The last rule is the unconditional reject `Checks.exportsEndInReject` looks for. */
|
|
93
|
-
static announce(prefixes: readonly Announced[]): PolicyRule<never, never>[];
|
|
94
103
|
/**
|
|
95
|
-
* An
|
|
104
|
+
* An export that announces each prefix, at exactly its length. It returns, in order:
|
|
105
|
+
*
|
|
106
|
+
* 1. `{ description, match: { prefix }, action: "accept" }` for each prefix;
|
|
107
|
+
* 2. `{ description: "Nothing else", action: "reject" }`, the unconditional reject `Checks.exportsEndInReject` looks for.
|
|
96
108
|
*
|
|
97
|
-
*
|
|
109
|
+
* 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.
|
|
110
|
+
*/
|
|
111
|
+
static announce(prefixes: readonly Announced[], options?: AnnounceOptions): PolicyRule<never, never>[];
|
|
112
|
+
/** The reject that ends a policy: nothing else gets through. */
|
|
113
|
+
private static readonly NOTHING_ELSE;
|
|
114
|
+
/**
|
|
115
|
+
* An import policy that takes a neighbor's routes. It returns these rules, in order, each only when its option is given:
|
|
116
|
+
*
|
|
117
|
+
* 1. the `before` rules, as written;
|
|
118
|
+
* 2. `{ call: sanity }`, or the rules of `Bgp.sanity()` written in when `sanity` is left out;
|
|
119
|
+
* 3. `{ set: { preferred_source: source } }` for each of `sources`, one per family;
|
|
120
|
+
* 4. `{ set: { local_pref } }`;
|
|
121
|
+
* 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;
|
|
122
|
+
* 6. `{ action: "accept" }`.
|
|
123
|
+
*
|
|
124
|
+
* So `Bgp.learn({ sanity: "SANITY", local_pref: 200, tag: { large: "64500:1:1" } })` is
|
|
125
|
+
* `[{ call: "SANITY" }, { set: { local_pref: 200 } }, { description: "Tag 64500:1:1", set: { add_large_communities: ["64500:1:1"] } }, { action: "accept" }]`.
|
|
126
|
+
*
|
|
127
|
+
* 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.
|
|
98
128
|
*/
|
|
99
129
|
static learn<const P extends string = never>(options?: LearnOptions<P>): PolicyRule<P, never>[];
|
|
100
130
|
/**
|
|
101
|
-
* Import rules that take the default route
|
|
131
|
+
* Import rules that take the default route from a transit. They are, in order:
|
|
132
|
+
*
|
|
133
|
+
* 1. `{ description, match: { prefix: "0.0.0.0/0" }, set: { local_pref, preferred_source }, action: "accept" }`;
|
|
134
|
+
* 2. the same for `::/0`;
|
|
135
|
+
* 3. with `{ reject: true }` only, `{ description: "Nothing else", action: "reject" }`.
|
|
136
|
+
*
|
|
137
|
+
* 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.
|
|
102
138
|
*/
|
|
103
139
|
static defaultOnly(options?: DefaultOnlyOptions): PolicyRule<never, never>[];
|
|
104
140
|
/** A rule adding one community, standard or large. */
|
|
@@ -116,8 +152,12 @@ export declare class Bgp {
|
|
|
116
152
|
/**
|
|
117
153
|
* 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.
|
|
118
154
|
*
|
|
119
|
-
* - `tag(neighborClass, neighborAs, { site, trusted })`:
|
|
120
|
-
* - `actions(neighborAs
|
|
155
|
+
* - `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`.
|
|
156
|
+
* - `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.
|
|
157
|
+
* - `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`.
|
|
158
|
+
* - `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`.
|
|
159
|
+
* - `prepends(neighborAs)`: prepends once, twice or three times toward this neighbor, where a route asks. Nothing without `prepend`.
|
|
160
|
+
* - `stripOurs()`: one rule removing every community of yours, `Our communities stay inside our network`.
|
|
121
161
|
* - `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.
|
|
122
162
|
* - `catalogue`: every community, for `communities` in `defineNetwork`.
|
|
123
163
|
* - `community(fn, parameter)`: one community of yours, for a rule of your own.
|
|
@@ -131,6 +171,10 @@ export declare class Bgp {
|
|
|
131
171
|
trusted?: boolean;
|
|
132
172
|
}) => PolicyRule<never, never>;
|
|
133
173
|
actions: (neighborAs: number) => PolicyRule<never, never>[];
|
|
174
|
+
blackholeExport: (neighborAs: number) => PolicyRule<never, never>[];
|
|
175
|
+
rejectDoNotAnnounce: (neighborAs: number) => PolicyRule<never, never>[];
|
|
176
|
+
prepends: (neighborAs: number) => PolicyRule<never, never>[];
|
|
177
|
+
stripOurs: () => PolicyRule<never, never>;
|
|
134
178
|
blackhole: <S extends string>(prefixSets: readonly S[]) => PolicyRule<never, S>[];
|
|
135
179
|
catalogue: Community[];
|
|
136
180
|
};
|