@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.
@@ -40,16 +40,17 @@ class Bgp {
40
40
  { description: "RPKI invalid", match: { rpki: "invalid" }, action: "reject" }
41
41
  ];
42
42
  }
43
- static announce(prefixes) {
43
+ static announce(prefixes, options = {}) {
44
44
  return [
45
45
  ...prefixes.map(({ prefix, description }) => ({
46
46
  ...description ? { description } : {},
47
47
  match: { prefix },
48
48
  action: "accept"
49
49
  })),
50
- { description: "Nothing else", action: "reject" }
50
+ ...options.reject === false ? [] : [Bgp.NOTHING_ELSE]
51
51
  ];
52
52
  }
53
+ static NOTHING_ELSE = { description: "Nothing else", action: "reject" };
53
54
  static learn(options = {}) {
54
55
  const { tag } = options;
55
56
  return [
@@ -57,12 +58,12 @@ class Bgp {
57
58
  ...options.sanity ? [{ call: options.sanity }] : Bgp.sanity(),
58
59
  ...(options.sources ?? []).map((source) => ({ set: { preferred_source: source } })),
59
60
  ...options.local_pref !== undefined ? [{ set: { local_pref: options.local_pref } }] : [],
60
- ...tag === undefined ? [] : [typeof tag === "object" ? tag : Bgp.adding(tag)],
61
+ ...tag === undefined ? [] : ["standard" in tag ? Bgp.adding(tag.standard) : ("large" in tag) ? Bgp.adding(tag.large) : tag],
61
62
  { action: "accept" }
62
63
  ];
63
64
  }
64
65
  static defaultOnly(options = {}) {
65
- return Bgp.DEFAULTS.map(({ family, prefix }) => {
66
+ const defaults = Bgp.DEFAULTS.map(({ family, prefix }) => {
66
67
  const source = options.sources?.find((address) => Address2.family(address) === family);
67
68
  const set = {
68
69
  ...options.local_pref !== undefined ? { local_pref: options.local_pref } : {},
@@ -75,6 +76,7 @@ class Bgp {
75
76
  action: "accept"
76
77
  };
77
78
  });
79
+ return options.reject ? [...defaults, Bgp.NOTHING_ELSE] : defaults;
78
80
  }
79
81
  static adding(community) {
80
82
  const large = community.split(":").length === 3;
@@ -142,58 +144,51 @@ class Bgp {
142
144
  action: "accept"
143
145
  }));
144
146
  };
145
- const actions = (neighborAs) => {
146
- const passOn = scheme.blackhole?.upstreams?.[neighborAs];
147
+ const passOnTo = (neighborAs) => scheme.blackhole?.upstreams?.[neighborAs];
148
+ const blackholeExport = (neighborAs) => {
149
+ if (scheme.blackhole === undefined)
150
+ return [];
151
+ const passOn = passOnTo(neighborAs);
152
+ const match = { large_community: community(scheme.blackhole.function, 0) };
153
+ return passOn ? [
154
+ {
155
+ description: `Blackhole passed on to AS${neighborAs}`,
156
+ match,
157
+ set: { remove_communities: ours.standard, remove_large_communities: ours.large, add_communities: [passOn] },
158
+ action: "accept"
159
+ }
160
+ ] : [{ description: "Blackholed here, never announced", match, action: "reject" }];
161
+ };
162
+ const rejectDoNotAnnounce = (neighborAs) => {
163
+ const fn = scheme.do_not_announce;
164
+ if (fn === undefined)
165
+ return [];
147
166
  return [
148
- ...scheme.blackhole !== undefined && !passOn ? [
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
- }),
167
+ { description: "Asked not to be announced anywhere", match: { large_community: community(fn, 0) }, action: "reject" },
191
168
  {
192
- description: "Our communities stay inside our network",
193
- set: { remove_communities: ours.standard, remove_large_communities: ours.large }
169
+ description: `Asked not to be announced to AS${neighborAs}`,
170
+ match: { large_community: community(fn, neighborAs) },
171
+ action: "reject"
194
172
  }
195
173
  ];
196
174
  };
175
+ const prepends = (neighborAs) => Bgp.PREPENDS.flatMap(([name, times]) => {
176
+ const fn = scheme.prepend?.[name];
177
+ if (fn === undefined)
178
+ return [];
179
+ return [
180
+ {
181
+ description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
182
+ match: { large_community: community(fn, neighborAs) },
183
+ set: { prepend: times }
184
+ }
185
+ ];
186
+ });
187
+ const stripOurs = () => ({
188
+ description: "Our communities stay inside our network",
189
+ set: { remove_communities: ours.standard, remove_large_communities: ours.large }
190
+ });
191
+ const actions = (neighborAs) => passOnTo(neighborAs) ? [...rejectDoNotAnnounce(neighborAs), ...blackholeExport(neighborAs), ...prepends(neighborAs), stripOurs()] : [...blackholeExport(neighborAs), ...rejectDoNotAnnounce(neighborAs), ...prepends(neighborAs), stripOurs()];
197
192
  const catalogue = [
198
193
  ...Object.entries(scheme.learned_from?.classes ?? {}).map(([name, value]) => ({
199
194
  community: written(scheme.learned_from.function, value),
@@ -222,7 +217,18 @@ class Bgp {
222
217
  }
223
218
  ] : []
224
219
  ];
225
- return { community, learned_from: learnedFrom, tag, actions, blackhole, catalogue };
220
+ return {
221
+ community,
222
+ learned_from: learnedFrom,
223
+ tag,
224
+ actions,
225
+ blackholeExport,
226
+ rejectDoNotAnnounce,
227
+ prepends,
228
+ stripOurs,
229
+ blackhole,
230
+ catalogue
231
+ };
226
232
  }
227
233
  static article(word) {
228
234
  return /^[aeiou]/i.test(word) ? `an ${word}` : `a ${word}`;
@@ -279,6 +285,22 @@ class Checks {
279
285
  }
280
286
  return findings;
281
287
  };
288
+ static importsEndInAcceptOrReject = (devices) => {
289
+ const findings = [];
290
+ for (const device of devices) {
291
+ for (const neighbor of Bgp.neighborPolicies(device)) {
292
+ const last = neighbor.import ? device.policies?.[neighbor.import]?.at(-1) : undefined;
293
+ if (neighbor.import && (!last?.action || last.match)) {
294
+ findings.push({
295
+ level: "error",
296
+ device: device.name,
297
+ message: `${neighbor.name}: import ${neighbor.import} does not end in an unconditional accept or reject`
298
+ });
299
+ }
300
+ }
301
+ }
302
+ return findings;
303
+ };
282
304
  static tunnelsOutsideOffered = (devices) => {
283
305
  const findings = [];
284
306
  for (const device of devices) {
@@ -664,69 +686,48 @@ class Filters {
664
686
  }
665
687
  }
666
688
  // 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
689
  class Upstreams {
677
690
  static define(catalog, options = {}) {
678
- const names = options.names ?? {};
679
691
  const all = Object.entries(catalog);
680
692
  const nameOf = (key, upstream) => upstream.name ?? key;
681
- const grouped = (upstream) => upstream.role === "rs" || Boolean(upstream.multihop);
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)) {
693
+ const unique = (what, names) => {
693
694
  const taken = new Map;
694
- for (const [key, upstream] of all) {
695
- if (field === "native" && upstream.handoff?.native === undefined)
696
- continue;
697
- if (field === "import" && !upstream.import)
695
+ for (const [key, name] of names) {
696
+ if (name === undefined)
698
697
  continue;
699
- if (field === "group" && !grouped(upstream))
700
- continue;
701
- if (field === "vlan" && !upstream.handoff || field === "interface" && !upstream.handoff && !upstream.tunnel)
702
- continue;
703
- const name = named(field, key, upstream);
704
698
  const other = taken.get(name);
705
699
  if (other)
706
- throw new Error(`Upstreams: ${other} and ${key} both have the ${field} name ${name}`);
700
+ throw new Error(`Upstreams: ${other} and ${key} both have the ${what} ${name}`);
707
701
  taken.set(name, key);
708
702
  }
709
- }
710
- const vlan = (key, upstream) => named("vlan", key, upstream);
711
- const native = (key, upstream) => named("native", key, upstream);
712
- const policy = (direction, key, upstream) => named(direction, key, upstream);
703
+ };
704
+ unique("VLAN", [
705
+ ...all.map(([key, upstream]) => [key, upstream.handoff?.vlan.name]),
706
+ ...all.map(([key, upstream]) => [key, upstream.handoff?.native?.name])
707
+ ]);
708
+ unique("interface", all.map(([key, upstream]) => [key, upstream.handoff?.interface ?? upstream.tunnel?.interface]));
709
+ unique("policy", [
710
+ ...all.map(([key, upstream]) => [key, upstream.import?.name]),
711
+ ...all.map(([key, upstream]) => [key, upstream.export.name])
712
+ ]);
713
+ unique("BGP group", all.map(([key, upstream]) => [key, upstream.group]));
713
714
  const vlans = (device) => {
714
715
  const result = {};
715
716
  for (const [key, upstream] of all) {
716
717
  const handoff = upstream.handoff;
717
718
  if (!handoff || device !== undefined && handoff.port?.device !== device)
718
719
  continue;
719
- result[vlan(key, upstream)] = { id: handoff.vlan, description: nameOf(key, upstream) };
720
- if (device !== undefined && handoff.native !== undefined) {
721
- result[native(key, upstream)] = {
722
- id: handoff.native,
720
+ result[handoff.vlan.name] = { id: handoff.vlan.id, description: nameOf(key, upstream) };
721
+ if (handoff.native) {
722
+ result[handoff.native.name] = {
723
+ id: handoff.native.id,
723
724
  description: `Native toward ${nameOf(key, upstream)}, nothing else in it`
724
725
  };
725
726
  }
726
727
  }
727
728
  return result;
728
729
  };
729
- const vlansOn = (device) => all.filter(([, upstream]) => upstream.handoff?.port?.device === device).map(([key, upstream]) => vlan(key, upstream));
730
+ const vlansOn = (device) => all.flatMap(([, upstream]) => upstream.handoff?.port?.device === device ? [upstream.handoff.vlan.name] : []);
730
731
  const ports = (device) => {
731
732
  const result = {};
732
733
  for (const [key, upstream] of all) {
@@ -734,10 +735,10 @@ class Upstreams {
734
735
  if (!handoff?.port || handoff.port.device !== device)
735
736
  continue;
736
737
  result[handoff.port.port] = {
737
- description: names.port ? names.port(key, upstream) : nameOf(key, upstream),
738
- ...handoff.native !== undefined ? { trunk_vlans: [vlan(key, upstream)], native_vlan: native(key, upstream) } : { access_vlan: vlan(key, upstream) },
739
- lldp: false,
740
- stp: false,
738
+ description: nameOf(key, upstream),
739
+ ...handoff.native ? { trunk_vlans: [handoff.vlan.name], native_vlan: handoff.native.name } : { access_vlan: handoff.vlan.name },
740
+ lldp: handoff.port.lldp ?? false,
741
+ stp: handoff.port.stp ?? false,
741
742
  ...handoff.port.storm_control ? { storm_control: handoff.port.storm_control } : {}
742
743
  };
743
744
  }
@@ -747,82 +748,63 @@ class Upstreams {
747
748
  const result = {};
748
749
  for (const [key, upstream] of all) {
749
750
  const description = nameOf(key, upstream);
750
- const name = named("interface", key, upstream);
751
751
  if (upstream.tunnel) {
752
- const { local, remote, addresses, mtu } = upstream.tunnel;
752
+ const { interface: name, local, remote, addresses, mtu } = upstream.tunnel;
753
753
  result[name] = { type: "gre", description, local, remote, addresses, ...mtu ? { mtu } : {} };
754
754
  } else if (upstream.handoff) {
755
- const { addresses, exchange } = upstream.handoff;
756
- result[name] = { type: "vlan", vlan: vlan(key, upstream), description, addresses, ...exchange ? { exchange } : {} };
755
+ const { interface: name, vlan, addresses, exchange } = upstream.handoff;
756
+ result[name] = { type: "vlan", vlan: vlan.name, description, addresses, ...exchange ? { exchange } : {} };
757
757
  }
758
758
  }
759
759
  return result;
760
760
  };
761
- const uplinks = () => all.filter(([, upstream]) => upstream.handoff).map(([key, upstream]) => named("interface", key, upstream));
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]) => {
761
+ const uplinks = () => all.flatMap(([, upstream]) => upstream.handoff ? [upstream.handoff.interface] : []);
762
+ const settings = (key, upstream) => {
770
763
  const multihop = upstream.multihop;
771
- return [
772
- named("group", key, upstream),
773
- {
774
- ...settings(key, upstream),
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
- }));
764
+ return {
765
+ remote_as: upstream.asn,
766
+ description: nameOf(key, upstream),
767
+ role: upstream.role,
768
+ ...upstream.import ? { import: upstream.import.name } : {},
769
+ export: upstream.export.name,
770
+ ...multihop ? {
771
+ local_address: multihop.local_address,
772
+ multihop: true,
773
+ ...multihop.families ? { families: multihop.families } : {},
774
+ ...multihop.password ? { password: multihop.password } : {}
775
+ } : {}
776
+ };
777
+ };
778
+ const groups = () => Object.fromEntries(all.flatMap(([key, upstream]) => upstream.group ? [[upstream.group, settings(key, upstream)]] : []));
784
779
  const neighbors = () => Object.fromEntries(all.flatMap(([key, upstream]) => Object.entries(upstream.sessions).map(([name, session]) => {
785
780
  const limit = upstream.limits?.[Address2.family(session.address)];
786
781
  return [
787
782
  name,
788
783
  {
784
+ ...upstream.group ? { group: upstream.group } : settings(key, upstream),
789
785
  ...session,
790
- ...grouped(upstream) ? { group: named("group", key, upstream) } : settings(key, upstream),
791
786
  ...limit ? { max_prefixes: limit } : {}
792
787
  }
793
788
  ];
794
789
  })));
795
- const policies = (options = {}) => {
790
+ const policies = () => {
796
791
  const result = {};
797
792
  for (const [key, upstream] of all) {
798
- result[policy("export", key, upstream)] = [
799
- ...options.actions?.(key, upstream) ?? [],
800
- ...upstream.export ?? [],
801
- ...Bgp.announce(upstream.announce)
802
- ];
803
- const taken = upstream.import;
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
- ];
793
+ if (upstream.import) {
794
+ const rules = upstream.import.rules;
795
+ result[upstream.import.name] = options.mapImports ? options.mapImports(rules, key, upstream) : rules;
796
+ }
797
+ const rules = upstream.export.rules;
798
+ result[upstream.export.name] = options.mapExports ? options.mapExports(rules, key, upstream) : rules;
817
799
  }
818
800
  return result;
819
801
  };
820
802
  const untrusted = () => [
821
- ...all.filter(([, upstream]) => upstream.handoff).map(([key, upstream]) => vlan(key, upstream)),
803
+ ...all.flatMap(([, upstream]) => upstream.handoff ? [upstream.handoff.vlan.name] : []),
822
804
  ...natives()
823
805
  ];
824
- const natives = () => all.filter(([, upstream]) => upstream.handoff?.native !== undefined).map(([key, upstream]) => native(key, upstream));
825
- const privateDrops = () => all.filter(([, upstream]) => upstream.handoff && upstream.drop_private).map(([key, upstream]) => vlan(key, upstream));
806
+ const natives = () => all.flatMap(([, upstream]) => upstream.handoff?.native ? [upstream.handoff.native.name] : []);
807
+ const privateDrops = () => all.flatMap(([, upstream]) => upstream.handoff && upstream.drop_private ? [upstream.handoff.vlan.name] : []);
826
808
  const tunnelRules = () => all.flatMap(([key, upstream]) => upstream.tunnel ? [
827
809
  {
828
810
  description: `GRE from ${nameOf(key, upstream)}`,
@@ -2,6 +2,8 @@ import type { Finding, Network } from "./define.js";
2
2
  export declare class Validator {
3
3
  static run(network: Network): Finding[];
4
4
  private static checkDevice;
5
+ /** A management VDOM: FortiOS only, not `root`, with declared interfaces, and routes that leave through its own. */
6
+ private static checkVdoms;
5
7
  /** What the model's hardware holds, from the catalog. A LAG with too many members is an error; too many switch rules is a warning, since the limit is only roughly known. */
6
8
  private static checkLimits;
7
9
  private static checkVlans;
@@ -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: a community to add, or a rule, such as `communityScheme(...).tag(...)`. */
32
- tag?: StandardCommunity | LargeCommunity | PolicyRule<never, never>;
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 import policy that takes a neighbor's routes: `before` first, then the sanity rules, then each source address and the local preference, then the tag, then accept.
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
- * `sanity` names a policy holding `Bgp.sanity()` to call, so several imports share one; without it, the sanity rules are written here. `tag` is a community to add, or a rule such as `communityScheme(...).tag(...)`. Each of `sources` is the source address for routes of its own family.
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, `0.0.0.0/0` and `::/0`, from a transit. Any other route reaches the next rule, or is rejected when there is none. Each of `sources` is the source address for the default of its own family.
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 })`: a rule for an import policy. It removes your communities a neighbor set, then adds where the route was learned. A `trusted` neighbor keeps its requests: do not announce, prepend, blackhole. `site` is needed when the scheme has `learned_at`.
120
- * - `actions(neighborAs, { blackhole })`: rules for an export policy toward one neighbor. They act on a customer's action communities, then remove all of yours. Place them before the rules that accept. A blackholed route goes no further, unless `blackhole` names the neighbor's own blackhole community: then it is passed on with it, so the neighbor drops the traffic too.
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
  };
@@ -16,6 +16,10 @@ export declare class Checks {
16
16
  * Every BGP export ends in a reject that matches everything, so nothing leaves by accident. An export to a neighbor whose `role` is `customer` is left out: sending a customer everything you know is what it pays for, and cannot leak a route to the rest of the internet.
17
17
  */
18
18
  static readonly exportsEndInReject: Check;
19
+ /**
20
+ * 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.
21
+ */
22
+ static readonly importsEndInAcceptOrReject: Check;
19
23
  /** 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. */
20
24
  static readonly tunnelsOutsideOffered: Check;
21
25
  /**
@@ -1,5 +1,5 @@
1
1
  export { Bgp } from "./bgp.js";
2
- export type { Announced, CommunityScheme, DefaultOnlyOptions, LearnOptions, SanityLengths } from "./bgp.js";
2
+ export type { AnnounceOptions, Announced, CommunityScheme, DefaultOnlyOptions, LearnOptions, SanityLengths } from "./bgp.js";
3
3
  export { Cables } from "./cables.js";
4
4
  export type { FarEnd } from "./cables.js";
5
5
  export { Checks } from "./checks.js";
@@ -9,6 +9,6 @@ export type { ExchangeMember, InternetExchange } from "./exchange.js";
9
9
  export { Filters } from "./filters.js";
10
10
  export { RouteServerConfig } from "./route-server.js";
11
11
  export { Upstreams } from "./upstreams.js";
12
- export type { Upstream, UpstreamCatalog, UpstreamPolicies } from "./upstreams.js";
12
+ export type { NamedVlan, Upstream, UpstreamCatalog, UpstreamOptions, UpstreamPolicy } from "./upstreams.js";
13
13
  export type { RouteServer } from "./route-server.js";
14
14
  export type { AntiAmplificationOptions } from "./filters.js";