@takodotid/circuit 0.14.0 → 0.15.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 CHANGED
@@ -3502,15 +3502,46 @@ async function sendPeeringDb(network2, changes2) {
3502
3502
  }
3503
3503
 
3504
3504
  // src/adapters/registries/prometheus.ts
3505
+ var AUTH2 = { sha1: "SHA", sha256: "SHA256" };
3506
+ var PRIVACY2 = { aes128: "AES", des: "DES" };
3505
3507
  function prometheusTargets(network2) {
3506
3508
  return {
3507
3509
  targets: network2.devices.filter((device) => device.management?.snmp).map((device) => ({
3508
3510
  targets: [device.connection.host],
3509
- labels: { device: device.name, platform: device.platform, model: device.model }
3511
+ labels: { device: device.name, platform: device.platform, model: device.model, __param_auth: device.name }
3510
3512
  })),
3511
3513
  withoutSnmp: network2.devices.filter((device) => !device.management?.snmp)
3512
3514
  };
3513
3515
  }
3516
+ function fromEnvironment(secret, notes, where) {
3517
+ if ("secret" in secret)
3518
+ return `\${${secret.secret}}`;
3519
+ notes.push(`${where} is read from a file; write its value in place of FILL_IN`);
3520
+ return "FILL_IN";
3521
+ }
3522
+ function snmpExporterAuths(network2) {
3523
+ const notes = [];
3524
+ const lines = [
3525
+ "# Written by `circuit prometheus --auths`. Load it beside snmp.yml, with --config.expand-environment-variables.",
3526
+ "auths:"
3527
+ ];
3528
+ for (const device of network2.devices) {
3529
+ const snmp3 = device.management?.snmp;
3530
+ if (!snmp3)
3531
+ continue;
3532
+ const [name, user] = Object.entries(snmp3.users ?? {})[0] ?? [];
3533
+ lines.push(` ${device.name}:`);
3534
+ if (name && user) {
3535
+ lines.push(" version: 3", ` username: ${name}`, " security_level: authPriv", ` password: ${fromEnvironment(user.auth_password, notes, `${device.name}'s SNMP password`)}`, ` auth_protocol: ${AUTH2[user.auth]}`, ` priv_protocol: ${PRIVACY2[user.privacy]}`, ` priv_password: ${fromEnvironment(user.privacy_password, notes, `${device.name}'s SNMP privacy password`)}`);
3536
+ } else {
3537
+ lines.push(" version: 2", " community: FILL_IN");
3538
+ notes.push(`${device.name} has only an SNMP version 2c community, which snmp_exporter cannot read from its environment; write it in place of FILL_IN, or give the device a version 3 user`);
3539
+ }
3540
+ }
3541
+ return { yaml: lines.join(`
3542
+ `) + `
3543
+ `, notes };
3544
+ }
3514
3545
 
3515
3546
  // src/core/validate.ts
3516
3547
  import { X509Certificate as X509Certificate2 } from "node:crypto";
@@ -3870,6 +3901,7 @@ var HELP = `usage: circuit <command> [device...] [options]
3870
3901
  peeringdb bring PeeringDB's exchange records in line with the config, with --confirm
3871
3902
  librenms add every device LibreNMS does not monitor yet, with --confirm
3872
3903
  prometheus every device with SNMP, as Prometheus targets for snmp_exporter
3904
+ --auths snmp_exporter's auths for them instead, with secrets as \${NAME}
3873
3905
  secrets every secret the config uses, and whether each can be read
3874
3906
 
3875
3907
  --config <path> the network's config file, circuit.config.ts in the working directory by default`;
@@ -4124,6 +4156,13 @@ Nothing sent. Re-run with --confirm.`);
4124
4156
  await sendLibreNms(network2, plan4.additions);
4125
4157
  }
4126
4158
  function prometheusCommand() {
4159
+ if (flags.includes("--auths")) {
4160
+ const { yaml, notes } = snmpExporterAuths(network2);
4161
+ for (const note of notes)
4162
+ console.error(note);
4163
+ process.stdout.write(yaml);
4164
+ return;
4165
+ }
4127
4166
  const { targets, withoutSnmp } = prometheusTargets(network2);
4128
4167
  for (const device of withoutSnmp)
4129
4168
  console.error(`${device.name} has no SNMP in its config; left out`);
@@ -160,16 +160,22 @@ 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 ?? [] }));
163
168
  const tag = (neighborClass, neighborAs, options = {}) => {
164
169
  const { site } = options;
165
- const isCustomer = scheme.customers?.includes(neighborClass) ?? false;
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));
166
172
  if (scheme.learned_at && site === undefined)
167
173
  throw new Error(`communityScheme: tag for AS${neighborAs} needs a site, for learned_at`);
168
174
  return {
169
175
  description: `Learned from AS${neighborAs}`,
170
176
  set: {
171
177
  remove_communities: ours.standard,
172
- remove_large_communities: isCustomer ? information.map((fn) => community(fn, "*")) : ours.large,
178
+ remove_large_communities: trustedForAny ? [...information, ...untrusted].map((fn) => community(fn, "*")) : ours.large,
173
179
  add_large_communities: [
174
180
  ...scheme.learned_from ? [community(scheme.learned_from.function, scheme.learned_from.classes[neighborClass])] : [],
175
181
  ...scheme.learned_at ? [community(scheme.learned_at.function, site)] : [],
@@ -178,12 +184,14 @@ function communityScheme(scheme) {
178
184
  }
179
185
  };
180
186
  };
181
- const blackhole = (prefixSet) => {
187
+ const blackhole = (neighborClass, prefixSet) => {
182
188
  if (scheme.blackhole === undefined)
183
189
  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`);
184
192
  return {
185
- description: "Blackhole, asked by the customer",
186
- match: { large_community: community(scheme.blackhole, 0), prefix_set: prefixSet },
193
+ description: "Blackhole, asked by the neighbor",
194
+ match: { large_community: community(scheme.blackhole.function, 0), prefix_set: prefixSet },
187
195
  set: { blackhole: true },
188
196
  action: "accept"
189
197
  };
@@ -192,26 +200,26 @@ function communityScheme(scheme) {
192
200
  ...scheme.blackhole !== undefined && !options.blackhole ? [
193
201
  {
194
202
  description: "Blackholed here, never announced",
195
- match: { large_community: community(scheme.blackhole, 0) },
203
+ match: { large_community: community(scheme.blackhole.function, 0) },
196
204
  action: "reject"
197
205
  }
198
206
  ] : [],
199
207
  ...scheme.do_not_announce !== undefined ? [
200
208
  {
201
209
  description: "Asked not to be announced anywhere",
202
- match: { large_community: community(scheme.do_not_announce, 0) },
210
+ match: { large_community: community(scheme.do_not_announce.function, 0) },
203
211
  action: "reject"
204
212
  },
205
213
  {
206
214
  description: `Asked not to be announced to AS${neighborAs}`,
207
- match: { large_community: community(scheme.do_not_announce, neighborAs) },
215
+ match: { large_community: community(scheme.do_not_announce.function, neighborAs) },
208
216
  action: "reject"
209
217
  }
210
218
  ] : [],
211
219
  ...scheme.blackhole !== undefined && options.blackhole ? [
212
220
  {
213
221
  description: `Blackhole passed on to AS${neighborAs}`,
214
- match: { large_community: community(scheme.blackhole, 0) },
222
+ match: { large_community: community(scheme.blackhole.function, 0) },
215
223
  set: {
216
224
  remove_communities: ours.standard,
217
225
  remove_large_communities: ours.large,
@@ -251,14 +259,14 @@ function communityScheme(scheme) {
251
259
  ] : [],
252
260
  ...scheme.learned_from_as !== undefined ? [{ community: community(scheme.learned_from_as, "nnn"), description: "Learned from AS$0" }] : [],
253
261
  ...scheme.do_not_announce !== undefined ? [
254
- { community: community(scheme.do_not_announce, 0), description: "Do not announce to anyone" },
255
- { community: community(scheme.do_not_announce, "nnn"), description: "Do not announce to AS$0" }
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" }
256
264
  ] : [],
257
265
  ...PREPENDS.flatMap(([name]) => {
258
266
  const fn = scheme.prepend?.[name];
259
267
  return fn === undefined ? [] : [{ community: community(fn, "nnn"), description: `Prepend ${name.replace("_", " ")} to AS$0` }];
260
268
  }),
261
- ...scheme.blackhole !== undefined ? [{ community: community(scheme.blackhole, 0), description: `Blackhole: dropped in AS${asn}, not announced further` }] : []
269
+ ...scheme.blackhole !== undefined ? [{ community: community(scheme.blackhole.function, 0), description: `Blackhole: dropped in AS${asn}, not announced further` }] : []
262
270
  ];
263
271
  return { community, tag, actions, blackhole, catalogue };
264
272
  }
@@ -9,3 +9,8 @@ export declare function prometheusTargets(network: Network): {
9
9
  targets: PrometheusTarget[];
10
10
  withoutSnmp: Device[];
11
11
  };
12
+ /** The `auths` section, as YAML, and what the operator must still fill in by hand. */
13
+ export declare function snmpExporterAuths(network: Network): {
14
+ yaml: string;
15
+ notes: string[];
16
+ };
@@ -64,8 +64,6 @@ export type CommunityScheme<Class extends string = string> = {
64
64
  function: number;
65
65
  classes: Readonly<Record<Class, number>>;
66
66
  };
67
- /** Which of the classes above are your customers. Only a customer may ask you for something, such as not announcing its route somewhere; what anyone else asks is removed when the route comes in. */
68
- customers?: readonly NoInfer<Class>[];
69
67
  /** Set on import: the site a route was learned at, `asn:function:site`. `sites` names the sites, for the catalogue. */
70
68
  learned_at?: {
71
69
  function: number;
@@ -73,23 +71,32 @@ export type CommunityScheme<Class extends string = string> = {
73
71
  };
74
72
  /** Set on import: the AS a route was learned from, `asn:function:asn`. */
75
73
  learned_from_as?: number;
76
- /** Set by a customer: do not announce the route to an AS, `asn:function:asn`, or to anyone, `asn:function:0`. */
77
- do_not_announce?: number;
78
- /** Set by a customer: prepend toward an AS once, twice or three times, `asn:function:asn`. */
79
- prepend?: {
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
+ };
78
+ /** A neighbor asks: prepend toward an AS once, twice or three times, `asn:function:asn`. */
79
+ prepend?: Request<Class> & {
80
80
  once?: number;
81
81
  twice?: number;
82
82
  three_times?: number;
83
83
  };
84
- /** Set by a customer: drop traffic to the route in your network, before it reaches them, `asn:function:0`. The route is never announced further. */
85
- blackhole?: number;
84
+ /** A neighbor asks: drop traffic to the route in your network, before it reaches them, `asn:function:0`. */
85
+ blackhole?: Request<Class> & {
86
+ function: number;
87
+ };
88
+ };
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>[];
86
93
  };
87
94
  /**
88
95
  * 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.
89
96
  *
90
- * - `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 customer, a class in `customers`, keeps its requests. `site` is needed when the scheme has `learned_at`.
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`.
91
98
  * - `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.
92
- * - `blackhole(prefixSet)`: a rule for a customer's import policy, before anything that rejects long prefixes. It takes a route the customer asked to blackhole, but only inside `prefixSet`, its own space, and drops traffic to it here.
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.
93
100
  * - `catalogue`: every community, for `communities` in `defineNetwork`.
94
101
  */
95
102
  export declare function communityScheme<const Class extends string>(scheme: CommunityScheme<Class>): {
@@ -100,7 +107,7 @@ export declare function communityScheme<const Class extends string>(scheme: Comm
100
107
  actions: (neighborAs: number, options?: {
101
108
  blackhole?: string;
102
109
  }) => PolicyRule<never, never>[];
103
- blackhole: <S extends string>(prefixSet: S) => PolicyRule<never, S>;
110
+ blackhole: <S extends string>(neighborClass: Class, prefixSet: S) => PolicyRule<never, S>;
104
111
  catalogue: Community[];
105
112
  };
106
113
  /** A member of an internet exchange: a network with a port on the fabric and an address on the peering LAN. */
@@ -138,3 +145,4 @@ export declare function internetExchange<const V extends string>(exchange: Inter
138
145
  ports: (device: string) => Record<string, Port<V, string, string, string>>;
139
146
  check: Check;
140
147
  };
148
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "0.14.0",
3
+ "version": "0.15.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",
@@ -1,10 +1,16 @@
1
- // Prometheus targets for snmp_exporter, in its file-based discovery format: one entry per device that answers SNMP, labelled with what the config knows about it.
1
+ // Prometheus targets for snmp_exporter, in its file-based discovery format, and the `auths` section of its config: one auth per device, named after it, so each device is asked with its own credentials.
2
+ //
3
+ // The auths hold no secret. A password is written as `${NAME}`, the name of its secret, and snmp_exporter reads it from its environment when started with `--config.expand-environment-variables`.
2
4
 
3
5
  import type { Network } from "../../core/define";
4
- import type { Device } from "../../schema";
6
+ import type { Device, Secret } from "../../schema";
5
7
 
6
8
  export type PrometheusTarget = { targets: string[]; labels: Record<string, string> };
7
9
 
10
+ /** snmp_exporter's names for the algorithms. */
11
+ const AUTH = { sha1: "SHA", sha256: "SHA256" } as const;
12
+ const PRIVACY = { aes128: "AES", des: "DES" } as const;
13
+
8
14
  /** The targets, and the devices left out because their config turns SNMP off. */
9
15
  export function prometheusTargets(network: Network): { targets: PrometheusTarget[]; withoutSnmp: Device[] } {
10
16
  return {
@@ -12,8 +18,54 @@ export function prometheusTargets(network: Network): { targets: PrometheusTarget
12
18
  .filter((device) => device.management?.snmp)
13
19
  .map((device) => ({
14
20
  targets: [device.connection.host],
15
- labels: { device: device.name, platform: device.platform, model: device.model },
21
+ // `__param_auth` picks the device's own auth in snmp_exporter.
22
+ labels: { device: device.name, platform: device.platform, model: device.model, __param_auth: device.name },
16
23
  })),
17
24
  withoutSnmp: network.devices.filter((device) => !device.management?.snmp),
18
25
  };
19
26
  }
27
+
28
+ /** A password as snmp_exporter reads it from its environment, or a note when the secret is a file, which it cannot read. */
29
+ function fromEnvironment(secret: Secret, notes: string[], where: string): string {
30
+ if ("secret" in secret) return `\${${secret.secret}}`;
31
+
32
+ notes.push(`${where} is read from a file; write its value in place of FILL_IN`);
33
+ return "FILL_IN";
34
+ }
35
+
36
+ /** The `auths` section, as YAML, and what the operator must still fill in by hand. */
37
+ export function snmpExporterAuths(network: Network): { yaml: string; notes: string[] } {
38
+ const notes: string[] = [];
39
+ const lines = [
40
+ "# Written by `circuit prometheus --auths`. Load it beside snmp.yml, with --config.expand-environment-variables.",
41
+ "auths:",
42
+ ];
43
+
44
+ for (const device of network.devices) {
45
+ const snmp = device.management?.snmp;
46
+ if (!snmp) continue;
47
+
48
+ const [name, user] = Object.entries(snmp.users ?? {})[0] ?? [];
49
+ lines.push(` ${device.name}:`);
50
+
51
+ if (name && user) {
52
+ lines.push(
53
+ " version: 3",
54
+ ` username: ${name}`,
55
+ " security_level: authPriv",
56
+ ` password: ${fromEnvironment(user.auth_password, notes, `${device.name}'s SNMP password`)}`,
57
+ ` auth_protocol: ${AUTH[user.auth]}`,
58
+ ` priv_protocol: ${PRIVACY[user.privacy]}`,
59
+ ` priv_password: ${fromEnvironment(user.privacy_password, notes, `${device.name}'s SNMP privacy password`)}`
60
+ );
61
+ } else {
62
+ // snmp_exporter reads no community from the environment, and Circuit never writes a secret's value.
63
+ lines.push(" version: 2", " community: FILL_IN");
64
+ notes.push(
65
+ `${device.name} has only an SNMP version 2c community, which snmp_exporter cannot read from its environment; write it in place of FILL_IN, or give the device a version 3 user`
66
+ );
67
+ }
68
+ }
69
+
70
+ return { yaml: lines.join("\n") + "\n", notes };
71
+ }
package/src/cli/index.ts CHANGED
@@ -9,7 +9,7 @@ import type { ApplyOptions, DeviceAdapter, Plan } from "../adapters/devices/type
9
9
  import { registries } from "../adapters/registries";
10
10
  import { planLibreNms, sendLibreNms } from "../adapters/registries/librenms";
11
11
  import { planPeeringDb, sendPeeringDb } from "../adapters/registries/peeringdb";
12
- import { prometheusTargets } from "../adapters/registries/prometheus";
12
+ import { prometheusTargets, snmpExporterAuths } from "../adapters/registries/prometheus";
13
13
  import { network as prefixOf } from "../core/addr";
14
14
  import type { Network } from "../core/define";
15
15
  import { isSecret, referenceOf, resolve as reveal, sourceOf, useRoot } from "../core/secrets";
@@ -33,6 +33,7 @@ const HELP = `usage: circuit <command> [device...] [options]
33
33
  peeringdb bring PeeringDB's exchange records in line with the config, with --confirm
34
34
  librenms add every device LibreNMS does not monitor yet, with --confirm
35
35
  prometheus every device with SNMP, as Prometheus targets for snmp_exporter
36
+ --auths snmp_exporter's auths for them instead, with secrets as \${NAME}
36
37
  secrets every secret the config uses, and whether each can be read
37
38
 
38
39
  --config <path> the network's config file, circuit.config.ts in the working directory by default`;
@@ -352,6 +353,13 @@ async function librenmsCommand(): Promise<void> {
352
353
  }
353
354
 
354
355
  function prometheusCommand(): void {
356
+ if (flags.includes("--auths")) {
357
+ const { yaml, notes } = snmpExporterAuths(network);
358
+ for (const note of notes) console.error(note);
359
+ process.stdout.write(yaml);
360
+ return;
361
+ }
362
+
355
363
  const { targets, withoutSnmp } = prometheusTargets(network);
356
364
  for (const device of withoutSnmp) console.error(`${device.name} has no SNMP in its config; left out`);
357
365
  console.log(JSON.stringify(targets, null, 4));
@@ -226,18 +226,22 @@ export type CommunityScheme<Class extends string = string> = {
226
226
  asn: number;
227
227
  /** Set on import: the kind of neighbor a route came from, `asn:function:class`. */
228
228
  learned_from?: { function: number; classes: Readonly<Record<Class, number>> };
229
- /** Which of the classes above are your customers. Only a customer may ask you for something, such as not announcing its route somewhere; what anyone else asks is removed when the route comes in. */
230
- customers?: readonly NoInfer<Class>[];
231
229
  /** Set on import: the site a route was learned at, `asn:function:site`. `sites` names the sites, for the catalogue. */
232
230
  learned_at?: { function: number; sites?: Readonly<Record<number, string>> };
233
231
  /** Set on import: the AS a route was learned from, `asn:function:asn`. */
234
232
  learned_from_as?: number;
235
- /** Set by a customer: do not announce the route to an AS, `asn:function:asn`, or to anyone, `asn:function:0`. */
236
- do_not_announce?: number;
237
- /** Set by a customer: prepend toward an AS once, twice or three times, `asn:function:asn`. */
238
- prepend?: { once?: number; twice?: number; three_times?: number };
239
- /** Set by a customer: drop traffic to the route in your network, before it reaches them, `asn:function:0`. The route is never announced further. */
240
- blackhole?: number;
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 };
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>[];
241
245
  };
242
246
 
243
247
  const PREPENDS = [
@@ -251,9 +255,9 @@ const article = (word: string) => (/^[aeiou]/i.test(word) ? `an ${word}` : `a ${
251
255
  /**
252
256
  * 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.
253
257
  *
254
- * - `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 customer, a class in `customers`, keeps its requests. `site` is needed when the scheme has `learned_at`.
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`.
255
259
  * - `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.
256
- * - `blackhole(prefixSet)`: a rule for a customer's import policy, before anything that rejects long prefixes. It takes a route the customer asked to blackhole, but only inside `prefixSet`, its own space, and drops traffic to it here.
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.
257
261
  * - `catalogue`: every community, for `communities` in `defineNetwork`.
258
262
  */
259
263
  export function communityScheme<const Class extends string>(scheme: CommunityScheme<Class>) {
@@ -265,9 +269,17 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
265
269
  (fn): fn is number => fn !== undefined
266
270
  );
267
271
 
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
+
268
279
  const tag = (neighborClass: Class, neighborAs: number, options: { site?: number } = {}): PolicyRule<never, never> => {
269
280
  const { site } = options;
270
- const isCustomer = scheme.customers?.includes(neighborClass) ?? false;
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));
271
283
  if (scheme.learned_at && site === undefined)
272
284
  throw new Error(`communityScheme: tag for AS${neighborAs} needs a site, for learned_at`);
273
285
 
@@ -275,7 +287,8 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
275
287
  description: `Learned from AS${neighborAs}`,
276
288
  set: {
277
289
  remove_communities: ours.standard,
278
- remove_large_communities: isCustomer ? information.map((fn) => community(fn, "*")) : ours.large,
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,
279
292
  add_large_communities: [
280
293
  ...(scheme.learned_from ? [community(scheme.learned_from.function, scheme.learned_from.classes[neighborClass])] : []),
281
294
  ...(scheme.learned_at ? [community(scheme.learned_at.function, site!)] : []),
@@ -285,12 +298,14 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
285
298
  };
286
299
  };
287
300
 
288
- const blackhole = <S extends string>(prefixSet: S): PolicyRule<never, S> => {
301
+ const blackhole = <S extends string>(neighborClass: Class, prefixSet: S): PolicyRule<never, S> => {
289
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`);
290
305
 
291
306
  return {
292
- description: "Blackhole, asked by the customer",
293
- match: { large_community: community(scheme.blackhole, 0), prefix_set: prefixSet },
307
+ description: "Blackhole, asked by the neighbor",
308
+ match: { large_community: community(scheme.blackhole.function, 0), prefix_set: prefixSet },
294
309
  set: { blackhole: true },
295
310
  action: "accept",
296
311
  };
@@ -301,7 +316,7 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
301
316
  ? [
302
317
  {
303
318
  description: "Blackholed here, never announced",
304
- match: { large_community: community(scheme.blackhole, 0) },
319
+ match: { large_community: community(scheme.blackhole.function, 0) },
305
320
  action: "reject" as const,
306
321
  },
307
322
  ]
@@ -310,12 +325,12 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
310
325
  ? [
311
326
  {
312
327
  description: "Asked not to be announced anywhere",
313
- match: { large_community: community(scheme.do_not_announce, 0) },
328
+ match: { large_community: community(scheme.do_not_announce.function, 0) },
314
329
  action: "reject" as const,
315
330
  },
316
331
  {
317
332
  description: `Asked not to be announced to AS${neighborAs}`,
318
- match: { large_community: community(scheme.do_not_announce, neighborAs) },
333
+ match: { large_community: community(scheme.do_not_announce.function, neighborAs) },
319
334
  action: "reject" as const,
320
335
  },
321
336
  ]
@@ -324,7 +339,7 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
324
339
  ? [
325
340
  {
326
341
  description: `Blackhole passed on to AS${neighborAs}`,
327
- match: { large_community: community(scheme.blackhole, 0) },
342
+ match: { large_community: community(scheme.blackhole.function, 0) },
328
343
  set: {
329
344
  remove_communities: ours.standard,
330
345
  remove_large_communities: ours.large,
@@ -371,8 +386,8 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
371
386
  : []),
372
387
  ...(scheme.do_not_announce !== undefined
373
388
  ? [
374
- { community: community(scheme.do_not_announce, 0), description: "Do not announce to anyone" },
375
- { community: community(scheme.do_not_announce, "nnn"), description: "Do not announce to AS$0" },
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" },
376
391
  ]
377
392
  : []),
378
393
  ...PREPENDS.flatMap(([name]) => {
@@ -380,7 +395,7 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
380
395
  return fn === undefined ? [] : [{ community: community(fn, "nnn"), description: `Prepend ${name.replace("_", " ")} to AS$0` }];
381
396
  }),
382
397
  ...(scheme.blackhole !== undefined
383
- ? [{ community: community(scheme.blackhole, 0), description: `Blackhole: dropped in AS${asn}, not announced further` }]
398
+ ? [{ community: community(scheme.blackhole.function, 0), description: `Blackhole: dropped in AS${asn}, not announced further` }]
384
399
  : []),
385
400
  ];
386
401