@takodotid/circuit 0.15.0 → 0.16.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.
@@ -160,22 +160,15 @@ function communityScheme(scheme) {
160
160
  const community = (fn, parameter) => `${asn}:${fn}:${parameter}`;
161
161
  const ours = { standard: [`${asn}:*`], large: [`${asn}:*:*`] };
162
162
  const information = [scheme.learned_from?.function, scheme.learned_at?.function, scheme.learned_from_as].filter((fn) => fn !== undefined);
163
- const requests = [
164
- { functions: [scheme.do_not_announce?.function], trusted: scheme.do_not_announce?.trusted },
165
- { functions: PREPENDS.map(([name]) => scheme.prepend?.[name]), trusted: scheme.prepend?.trusted },
166
- { functions: [scheme.blackhole?.function], trusted: scheme.blackhole?.trusted }
167
- ].map(({ functions, trusted }) => ({ functions: functions.filter((fn) => fn !== undefined), trusted: trusted ?? [] }));
168
163
  const tag = (neighborClass, neighborAs, options = {}) => {
169
- const { site } = options;
170
- const untrusted = requests.filter((request) => !request.trusted.includes(neighborClass)).flatMap((request) => request.functions);
171
- const trustedForAny = requests.some((request) => request.functions.length && request.trusted.includes(neighborClass));
164
+ const { site, trusted = false } = options;
172
165
  if (scheme.learned_at && site === undefined)
173
166
  throw new Error(`communityScheme: tag for AS${neighborAs} needs a site, for learned_at`);
174
167
  return {
175
168
  description: `Learned from AS${neighborAs}`,
176
169
  set: {
177
170
  remove_communities: ours.standard,
178
- remove_large_communities: trustedForAny ? [...information, ...untrusted].map((fn) => community(fn, "*")) : ours.large,
171
+ remove_large_communities: trusted ? information.map((fn) => community(fn, "*")) : ours.large,
179
172
  add_large_communities: [
180
173
  ...scheme.learned_from ? [community(scheme.learned_from.function, scheme.learned_from.classes[neighborClass])] : [],
181
174
  ...scheme.learned_at ? [community(scheme.learned_at.function, site)] : [],
@@ -184,67 +177,69 @@ function communityScheme(scheme) {
184
177
  }
185
178
  };
186
179
  };
187
- const blackhole = (neighborClass, prefixSet) => {
188
- if (scheme.blackhole === undefined)
180
+ const blackhole = (prefixSets) => {
181
+ const fn = scheme.blackhole?.function;
182
+ if (fn === undefined)
189
183
  throw new Error("communityScheme: blackhole needs a function number in the scheme");
190
- if (!scheme.blackhole.trusted.includes(neighborClass))
191
- throw new Error(`communityScheme: ${neighborClass} is not trusted to ask for a blackhole`);
192
- return {
184
+ return prefixSets.map((prefixSet) => ({
193
185
  description: "Blackhole, asked by the neighbor",
194
- match: { large_community: community(scheme.blackhole.function, 0), prefix_set: prefixSet },
186
+ match: { large_community: community(fn, 0), prefix_set: prefixSet },
195
187
  set: { blackhole: true },
196
188
  action: "accept"
197
- };
189
+ }));
198
190
  };
199
- const actions = (neighborAs, options = {}) => [
200
- ...scheme.blackhole !== undefined && !options.blackhole ? [
201
- {
202
- description: "Blackholed here, never announced",
203
- match: { large_community: community(scheme.blackhole.function, 0) },
204
- action: "reject"
205
- }
206
- ] : [],
207
- ...scheme.do_not_announce !== undefined ? [
208
- {
209
- description: "Asked not to be announced anywhere",
210
- match: { large_community: community(scheme.do_not_announce.function, 0) },
211
- action: "reject"
212
- },
213
- {
214
- description: `Asked not to be announced to AS${neighborAs}`,
215
- match: { large_community: community(scheme.do_not_announce.function, neighborAs) },
216
- action: "reject"
217
- }
218
- ] : [],
219
- ...scheme.blackhole !== undefined && options.blackhole ? [
220
- {
221
- description: `Blackhole passed on to AS${neighborAs}`,
222
- match: { large_community: community(scheme.blackhole.function, 0) },
223
- set: {
224
- remove_communities: ours.standard,
225
- remove_large_communities: ours.large,
226
- add_communities: [options.blackhole]
191
+ const actions = (neighborAs) => {
192
+ const passOn = scheme.blackhole?.upstreams?.[neighborAs];
193
+ return [
194
+ ...scheme.blackhole !== undefined && !passOn ? [
195
+ {
196
+ description: "Blackholed here, never announced",
197
+ match: { large_community: community(scheme.blackhole.function, 0) },
198
+ action: "reject"
199
+ }
200
+ ] : [],
201
+ ...scheme.do_not_announce !== undefined ? [
202
+ {
203
+ description: "Asked not to be announced anywhere",
204
+ match: { large_community: community(scheme.do_not_announce, 0) },
205
+ action: "reject"
227
206
  },
228
- action: "accept"
229
- }
230
- ] : [],
231
- ...PREPENDS.flatMap(([name, times]) => {
232
- const fn = scheme.prepend?.[name];
233
- if (fn === undefined)
234
- return [];
235
- return [
236
207
  {
237
- description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
238
- match: { large_community: community(fn, neighborAs) },
239
- set: { prepend: times }
208
+ description: `Asked not to be announced to AS${neighborAs}`,
209
+ match: { large_community: community(scheme.do_not_announce, neighborAs) },
210
+ action: "reject"
240
211
  }
241
- ];
242
- }),
243
- {
244
- description: "Our communities stay inside our network",
245
- set: { remove_communities: ours.standard, remove_large_communities: ours.large }
246
- }
247
- ];
212
+ ] : [],
213
+ ...scheme.blackhole !== undefined && passOn ? [
214
+ {
215
+ description: `Blackhole passed on to AS${neighborAs}`,
216
+ match: { large_community: community(scheme.blackhole.function, 0) },
217
+ set: {
218
+ remove_communities: ours.standard,
219
+ remove_large_communities: ours.large,
220
+ add_communities: [passOn]
221
+ },
222
+ action: "accept"
223
+ }
224
+ ] : [],
225
+ ...PREPENDS.flatMap(([name, times]) => {
226
+ const fn = scheme.prepend?.[name];
227
+ if (fn === undefined)
228
+ return [];
229
+ return [
230
+ {
231
+ description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
232
+ match: { large_community: community(fn, neighborAs) },
233
+ set: { prepend: times }
234
+ }
235
+ ];
236
+ }),
237
+ {
238
+ description: "Our communities stay inside our network",
239
+ set: { remove_communities: ours.standard, remove_large_communities: ours.large }
240
+ }
241
+ ];
242
+ };
248
243
  const catalogue = [
249
244
  ...Object.entries(scheme.learned_from?.classes ?? {}).map(([name, value]) => ({
250
245
  community: community(scheme.learned_from.function, value),
@@ -259,14 +254,19 @@ function communityScheme(scheme) {
259
254
  ] : [],
260
255
  ...scheme.learned_from_as !== undefined ? [{ community: community(scheme.learned_from_as, "nnn"), description: "Learned from AS$0" }] : [],
261
256
  ...scheme.do_not_announce !== undefined ? [
262
- { community: community(scheme.do_not_announce.function, 0), description: "Do not announce to anyone" },
263
- { community: community(scheme.do_not_announce.function, "nnn"), description: "Do not announce to AS$0" }
257
+ { community: community(scheme.do_not_announce, 0), description: "Do not announce to anyone" },
258
+ { community: community(scheme.do_not_announce, "nnn"), description: "Do not announce to AS$0" }
264
259
  ] : [],
265
260
  ...PREPENDS.flatMap(([name]) => {
266
261
  const fn = scheme.prepend?.[name];
267
262
  return fn === undefined ? [] : [{ community: community(fn, "nnn"), description: `Prepend ${name.replace("_", " ")} to AS$0` }];
268
263
  }),
269
- ...scheme.blackhole !== undefined ? [{ community: community(scheme.blackhole.function, 0), description: `Blackhole: dropped in AS${asn}, not announced further` }] : []
264
+ ...scheme.blackhole !== undefined ? [
265
+ {
266
+ community: community(scheme.blackhole.function, 0),
267
+ description: Object.keys(scheme.blackhole.upstreams ?? {}).length ? `Blackhole: dropped in AS${asn} and by its upstreams that take it` : `Blackhole: dropped in AS${asn}, not announced further`
268
+ }
269
+ ] : []
270
270
  ];
271
271
  return { community, tag, actions, blackhole, catalogue };
272
272
  }
@@ -72,42 +72,39 @@ export type CommunityScheme<Class extends string = string> = {
72
72
  /** Set on import: the AS a route was learned from, `asn:function:asn`. */
73
73
  learned_from_as?: number;
74
74
  /** A neighbor asks: do not announce the route to an AS, `asn:function:asn`, or to anyone, `asn:function:0`. */
75
- do_not_announce?: Request<Class> & {
76
- function: number;
77
- };
75
+ do_not_announce?: number;
78
76
  /** A neighbor asks: prepend toward an AS once, twice or three times, `asn:function:asn`. */
79
- prepend?: Request<Class> & {
77
+ prepend?: {
80
78
  once?: number;
81
79
  twice?: number;
82
80
  three_times?: number;
83
81
  };
84
- /** A neighbor asks: drop traffic to the route in your network, before it reaches them, `asn:function:0`. */
85
- blackhole?: Request<Class> & {
82
+ /**
83
+ * A neighbor asks: drop all traffic to this route, `asn:function:0`, usually one address under attack. Your router drops it, and so does every upstream in `upstreams`.
84
+ *
85
+ * `upstreams` maps an upstream's AS to its own blackhole community, as its NOC gives it. The route is passed to that upstream with it, so the traffic stops before it reaches your links. An upstream not listed never receives the route.
86
+ */
87
+ blackhole?: {
86
88
  function: number;
89
+ upstreams?: Readonly<Record<number, string>>;
87
90
  };
88
91
  };
89
- /** Something a neighbor can ask you for, and who may. */
90
- type Request<Class extends string> = {
91
- /** The classes allowed to ask for it, from `learned_from.classes`. From anyone else, the request is removed when the route comes in. */
92
- trusted: readonly NoInfer<Class>[];
93
- };
94
92
  /**
95
93
  * 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.
96
94
  *
97
- * - `tag(neighborClass, neighborAs, { site })`: a rule for an import policy. It removes your communities a neighbor set, then adds where the route was learned. A neighbor keeps the requests its class is trusted with. `site` is needed when the scheme has `learned_at`.
95
+ * - `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`.
98
96
  * - `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.
99
- * - `blackhole(neighborClass, prefixSet)`: a rule for the import policy of a neighbor trusted with blackholes, before anything that rejects long prefixes. It takes a route the neighbor asked to blackhole, but only inside `prefixSet`, its own space, and drops traffic to it here.
97
+ * - `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.
100
98
  * - `catalogue`: every community, for `communities` in `defineNetwork`.
101
99
  */
102
100
  export declare function communityScheme<const Class extends string>(scheme: CommunityScheme<Class>): {
103
101
  community: (fn: number, parameter: number | string) => string;
104
102
  tag: (neighborClass: Class, neighborAs: number, options?: {
105
103
  site?: number;
104
+ trusted?: boolean;
106
105
  }) => PolicyRule<never, never>;
107
- actions: (neighborAs: number, options?: {
108
- blackhole?: string;
109
- }) => PolicyRule<never, never>[];
110
- blackhole: <S extends string>(neighborClass: Class, prefixSet: S) => PolicyRule<never, S>;
106
+ actions: (neighborAs: number) => PolicyRule<never, never>[];
107
+ blackhole: <S extends string>(prefixSets: readonly S[]) => PolicyRule<never, S>[];
111
108
  catalogue: Community[];
112
109
  };
113
110
  /** A member of an internet exchange: a network with a port on the fabric and an address on the peering LAN. */
@@ -145,4 +142,3 @@ export declare function internetExchange<const V extends string>(exchange: Inter
145
142
  ports: (device: string) => Record<string, Port<V, string, string, string>>;
146
143
  check: Check;
147
144
  };
148
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Declarative, vendor-neutral network configuration: describe each device in TypeScript, and Circuit makes the device match. Alpha.",
5
5
  "keywords": [
6
6
  "network",
@@ -231,17 +231,15 @@ export type CommunityScheme<Class extends string = string> = {
231
231
  /** Set on import: the AS a route was learned from, `asn:function:asn`. */
232
232
  learned_from_as?: number;
233
233
  /** A neighbor asks: do not announce the route to an AS, `asn:function:asn`, or to anyone, `asn:function:0`. */
234
- do_not_announce?: Request<Class> & { function: number };
234
+ do_not_announce?: number;
235
235
  /** A neighbor asks: prepend toward an AS once, twice or three times, `asn:function:asn`. */
236
- prepend?: Request<Class> & { once?: number; twice?: number; three_times?: number };
237
- /** A neighbor asks: drop traffic to the route in your network, before it reaches them, `asn:function:0`. */
238
- blackhole?: Request<Class> & { function: number };
239
- };
240
-
241
- /** Something a neighbor can ask you for, and who may. */
242
- type Request<Class extends string> = {
243
- /** The classes allowed to ask for it, from `learned_from.classes`. From anyone else, the request is removed when the route comes in. */
244
- trusted: readonly NoInfer<Class>[];
236
+ prepend?: { once?: number; twice?: number; three_times?: number };
237
+ /**
238
+ * A neighbor asks: drop all traffic to this route, `asn:function:0`, usually one address under attack. Your router drops it, and so does every upstream in `upstreams`.
239
+ *
240
+ * `upstreams` maps an upstream's AS to its own blackhole community, as its NOC gives it. The route is passed to that upstream with it, so the traffic stops before it reaches your links. An upstream not listed never receives the route.
241
+ */
242
+ blackhole?: { function: number; upstreams?: Readonly<Record<number, string>> };
245
243
  };
246
244
 
247
245
  const PREPENDS = [
@@ -255,9 +253,9 @@ const article = (word: string) => (/^[aeiou]/i.test(word) ? `an ${word}` : `a ${
255
253
  /**
256
254
  * 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.
257
255
  *
258
- * - `tag(neighborClass, neighborAs, { site })`: a rule for an import policy. It removes your communities a neighbor set, then adds where the route was learned. A neighbor keeps the requests its class is trusted with. `site` is needed when the scheme has `learned_at`.
256
+ * - `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`.
259
257
  * - `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.
260
- * - `blackhole(neighborClass, prefixSet)`: a rule for the import policy of a neighbor trusted with blackholes, before anything that rejects long prefixes. It takes a route the neighbor asked to blackhole, but only inside `prefixSet`, its own space, and drops traffic to it here.
258
+ * - `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.
261
259
  * - `catalogue`: every community, for `communities` in `defineNetwork`.
262
260
  */
263
261
  export function communityScheme<const Class extends string>(scheme: CommunityScheme<Class>) {
@@ -269,17 +267,12 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
269
267
  (fn): fn is number => fn !== undefined
270
268
  );
271
269
 
272
- // Each request's function numbers, and who may make it.
273
- const requests = [
274
- { functions: [scheme.do_not_announce?.function], trusted: scheme.do_not_announce?.trusted },
275
- { functions: PREPENDS.map(([name]) => scheme.prepend?.[name]), trusted: scheme.prepend?.trusted },
276
- { functions: [scheme.blackhole?.function], trusted: scheme.blackhole?.trusted },
277
- ].map(({ functions, trusted }) => ({ functions: functions.filter((fn): fn is number => fn !== undefined), trusted: trusted ?? [] }));
278
-
279
- const tag = (neighborClass: Class, neighborAs: number, options: { site?: number } = {}): PolicyRule<never, never> => {
280
- const { site } = options;
281
- const untrusted = requests.filter((request) => !request.trusted.includes(neighborClass)).flatMap((request) => request.functions);
282
- const trustedForAny = requests.some((request) => request.functions.length && request.trusted.includes(neighborClass));
270
+ const tag = (
271
+ neighborClass: Class,
272
+ neighborAs: number,
273
+ options: { site?: number; trusted?: boolean } = {}
274
+ ): PolicyRule<never, never> => {
275
+ const { site, trusted = false } = options;
283
276
  if (scheme.learned_at && site === undefined)
284
277
  throw new Error(`communityScheme: tag for AS${neighborAs} needs a site, for learned_at`);
285
278
 
@@ -287,8 +280,8 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
287
280
  description: `Learned from AS${neighborAs}`,
288
281
  set: {
289
282
  remove_communities: ours.standard,
290
- // A neighbor trusted with nothing loses every community of ours it set. One trusted with some requests keeps those, and loses the rest.
291
- remove_large_communities: trustedForAny ? [...information, ...untrusted].map((fn) => community(fn, "*")) : ours.large,
283
+ // A trusted neighbor keeps its requests and loses only the "where it came from" tags, which are ours to set. Anyone else loses every community of ours.
284
+ remove_large_communities: trusted ? information.map((fn) => community(fn, "*")) : ours.large,
292
285
  add_large_communities: [
293
286
  ...(scheme.learned_from ? [community(scheme.learned_from.function, scheme.learned_from.classes[neighborClass])] : []),
294
287
  ...(scheme.learned_at ? [community(scheme.learned_at.function, site!)] : []),
@@ -298,74 +291,78 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
298
291
  };
299
292
  };
300
293
 
301
- const blackhole = <S extends string>(neighborClass: Class, prefixSet: S): PolicyRule<never, S> => {
302
- if (scheme.blackhole === undefined) throw new Error("communityScheme: blackhole needs a function number in the scheme");
303
- if (!scheme.blackhole.trusted.includes(neighborClass))
304
- throw new Error(`communityScheme: ${neighborClass} is not trusted to ask for a blackhole`);
294
+ const blackhole = <S extends string>(prefixSets: readonly S[]): PolicyRule<never, S>[] => {
295
+ const fn = scheme.blackhole?.function;
296
+ if (fn === undefined) throw new Error("communityScheme: blackhole needs a function number in the scheme");
305
297
 
306
- return {
298
+ return prefixSets.map((prefixSet) => ({
307
299
  description: "Blackhole, asked by the neighbor",
308
- match: { large_community: community(scheme.blackhole.function, 0), prefix_set: prefixSet },
300
+ match: { large_community: community(fn, 0), prefix_set: prefixSet },
309
301
  set: { blackhole: true },
310
302
  action: "accept",
311
- };
303
+ }));
312
304
  };
313
305
 
314
- const actions = (neighborAs: number, options: { blackhole?: string } = {}): PolicyRule<never, never>[] => [
315
- ...(scheme.blackhole !== undefined && !options.blackhole
316
- ? [
317
- {
318
- description: "Blackholed here, never announced",
319
- match: { large_community: community(scheme.blackhole.function, 0) },
320
- action: "reject" as const,
321
- },
322
- ]
323
- : []),
324
- ...(scheme.do_not_announce !== undefined
325
- ? [
326
- {
327
- description: "Asked not to be announced anywhere",
328
- match: { large_community: community(scheme.do_not_announce.function, 0) },
329
- action: "reject" as const,
330
- },
331
- {
332
- description: `Asked not to be announced to AS${neighborAs}`,
333
- match: { large_community: community(scheme.do_not_announce.function, neighborAs) },
334
- action: "reject" as const,
335
- },
336
- ]
337
- : []),
338
- ...(scheme.blackhole !== undefined && options.blackhole
339
- ? [
340
- {
341
- description: `Blackhole passed on to AS${neighborAs}`,
342
- match: { large_community: community(scheme.blackhole.function, 0) },
343
- set: {
344
- remove_communities: ours.standard,
345
- remove_large_communities: ours.large,
346
- add_communities: [options.blackhole],
306
+ const actions = (neighborAs: number): PolicyRule<never, never>[] => {
307
+ // The upstream's own blackhole community, when it takes blackholes from us.
308
+ const passOn = scheme.blackhole?.upstreams?.[neighborAs];
309
+
310
+ return [
311
+ ...(scheme.blackhole !== undefined && !passOn
312
+ ? [
313
+ {
314
+ description: "Blackholed here, never announced",
315
+ match: { large_community: community(scheme.blackhole.function, 0) },
316
+ action: "reject" as const,
347
317
  },
348
- action: "accept" as const,
349
- },
350
- ]
351
- : []),
352
- ...PREPENDS.flatMap(([name, times]) => {
353
- const fn = scheme.prepend?.[name];
354
- if (fn === undefined) return [];
355
-
356
- return [
357
- {
358
- description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
359
- match: { large_community: community(fn, neighborAs) },
360
- set: { prepend: times },
361
- },
362
- ];
363
- }),
364
- {
365
- description: "Our communities stay inside our network",
366
- set: { remove_communities: ours.standard, remove_large_communities: ours.large },
367
- },
368
- ];
318
+ ]
319
+ : []),
320
+ ...(scheme.do_not_announce !== undefined
321
+ ? [
322
+ {
323
+ description: "Asked not to be announced anywhere",
324
+ match: { large_community: community(scheme.do_not_announce, 0) },
325
+ action: "reject" as const,
326
+ },
327
+ {
328
+ description: `Asked not to be announced to AS${neighborAs}`,
329
+ match: { large_community: community(scheme.do_not_announce, neighborAs) },
330
+ action: "reject" as const,
331
+ },
332
+ ]
333
+ : []),
334
+ ...(scheme.blackhole !== undefined && passOn
335
+ ? [
336
+ {
337
+ description: `Blackhole passed on to AS${neighborAs}`,
338
+ match: { large_community: community(scheme.blackhole.function, 0) },
339
+ set: {
340
+ remove_communities: ours.standard,
341
+ remove_large_communities: ours.large,
342
+ add_communities: [passOn],
343
+ },
344
+ action: "accept" as const,
345
+ },
346
+ ]
347
+ : []),
348
+ ...PREPENDS.flatMap(([name, times]) => {
349
+ const fn = scheme.prepend?.[name];
350
+ if (fn === undefined) return [];
351
+
352
+ return [
353
+ {
354
+ description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
355
+ match: { large_community: community(fn, neighborAs) },
356
+ set: { prepend: times },
357
+ },
358
+ ];
359
+ }),
360
+ {
361
+ description: "Our communities stay inside our network",
362
+ set: { remove_communities: ours.standard, remove_large_communities: ours.large },
363
+ },
364
+ ];
365
+ };
369
366
 
370
367
  const catalogue: Community[] = [
371
368
  ...Object.entries<number>(scheme.learned_from?.classes ?? {}).map(([name, value]) => ({
@@ -386,8 +383,8 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
386
383
  : []),
387
384
  ...(scheme.do_not_announce !== undefined
388
385
  ? [
389
- { community: community(scheme.do_not_announce.function, 0), description: "Do not announce to anyone" },
390
- { community: community(scheme.do_not_announce.function, "nnn"), description: "Do not announce to AS$0" },
386
+ { community: community(scheme.do_not_announce, 0), description: "Do not announce to anyone" },
387
+ { community: community(scheme.do_not_announce, "nnn"), description: "Do not announce to AS$0" },
391
388
  ]
392
389
  : []),
393
390
  ...PREPENDS.flatMap(([name]) => {
@@ -395,7 +392,14 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
395
392
  return fn === undefined ? [] : [{ community: community(fn, "nnn"), description: `Prepend ${name.replace("_", " ")} to AS$0` }];
396
393
  }),
397
394
  ...(scheme.blackhole !== undefined
398
- ? [{ community: community(scheme.blackhole.function, 0), description: `Blackhole: dropped in AS${asn}, not announced further` }]
395
+ ? [
396
+ {
397
+ community: community(scheme.blackhole.function, 0),
398
+ description: Object.keys(scheme.blackhole.upstreams ?? {}).length
399
+ ? `Blackhole: dropped in AS${asn} and by its upstreams that take it`
400
+ : `Blackhole: dropped in AS${asn}, not announced further`,
401
+ },
402
+ ]
399
403
  : []),
400
404
  ];
401
405