@takodotid/circuit 0.13.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 +141 -1
- package/dist/presets/index.js +20 -12
- package/dist/types/adapters/registries/librenms.d.ts +15 -0
- package/dist/types/adapters/registries/prometheus.d.ts +16 -0
- package/dist/types/core/define.d.ts +7 -0
- package/dist/types/presets/index.d.ts +19 -11
- package/package.json +1 -1
- package/src/adapters/registries/librenms.ts +90 -0
- package/src/adapters/registries/prometheus.ts +71 -0
- package/src/cli/index.ts +43 -1
- package/src/core/define.ts +7 -0
- package/src/presets/index.ts +37 -22
package/dist/cli/index.js
CHANGED
|
@@ -3347,6 +3347,67 @@ async function ripeStat(query, family) {
|
|
|
3347
3347
|
}
|
|
3348
3348
|
var registries = { "ripe-stat": ripeStat };
|
|
3349
3349
|
|
|
3350
|
+
// src/adapters/registries/librenms.ts
|
|
3351
|
+
var AUTH = { sha1: "SHA", sha256: "SHA-256" };
|
|
3352
|
+
var PRIVACY = { aes128: "AES", des: "DES" };
|
|
3353
|
+
function settingsOf2(network2) {
|
|
3354
|
+
if (!network2.librenms)
|
|
3355
|
+
throw new Error("the network has no librenms settings");
|
|
3356
|
+
return {
|
|
3357
|
+
base: `${network2.librenms.url.replace(/\/+$/, "")}/api/v0`,
|
|
3358
|
+
headers: { "X-Auth-Token": resolve(network2.librenms.api_token), "Content-Type": "application/json" }
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
function credentials(device) {
|
|
3362
|
+
const snmp3 = device.management?.snmp;
|
|
3363
|
+
const [name, user] = Object.entries(snmp3?.users ?? {})[0] ?? [];
|
|
3364
|
+
if (name && user) {
|
|
3365
|
+
return {
|
|
3366
|
+
snmpver: "v3",
|
|
3367
|
+
authlevel: "authPriv",
|
|
3368
|
+
authname: name,
|
|
3369
|
+
authpass: resolve(user.auth_password),
|
|
3370
|
+
authalgo: AUTH[user.auth],
|
|
3371
|
+
cryptopass: resolve(user.privacy_password),
|
|
3372
|
+
cryptoalgo: PRIVACY[user.privacy]
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
if (snmp3?.community)
|
|
3376
|
+
return { snmpver: "v2c", community: resolve(snmp3.community) };
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
async function planLibreNms(network2) {
|
|
3380
|
+
const { base, headers } = settingsOf2(network2);
|
|
3381
|
+
const response = await fetch(`${base}/devices?type=all`, { headers, signal: AbortSignal.timeout(30000) });
|
|
3382
|
+
if (!response.ok)
|
|
3383
|
+
throw new Error(`LibreNMS answered ${response.status} to the device list`);
|
|
3384
|
+
const monitored = new Set((await response.json()).devices.map((device) => device.hostname));
|
|
3385
|
+
const ours = new Set(network2.devices.map((device) => device.connection.host));
|
|
3386
|
+
const missing = network2.devices.filter((device) => !monitored.has(device.connection.host));
|
|
3387
|
+
return {
|
|
3388
|
+
additions: missing.filter((device) => device.management?.snmp),
|
|
3389
|
+
withoutSnmp: missing.filter((device) => !device.management?.snmp),
|
|
3390
|
+
unknown: [...monitored].filter((hostname) => !ours.has(hostname)).sort()
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
async function sendLibreNms(network2, additions) {
|
|
3394
|
+
const { base, headers } = settingsOf2(network2);
|
|
3395
|
+
for (const device of additions) {
|
|
3396
|
+
const body = { hostname: device.connection.host, display_template: device.name, ...credentials(device) };
|
|
3397
|
+
const response = await fetch(`${base}/devices`, {
|
|
3398
|
+
method: "POST",
|
|
3399
|
+
headers,
|
|
3400
|
+
body: JSON.stringify(body),
|
|
3401
|
+
signal: AbortSignal.timeout(60000)
|
|
3402
|
+
});
|
|
3403
|
+
const answer = await response.json().catch(() => ({}));
|
|
3404
|
+
if (!response.ok || answer.status !== "ok") {
|
|
3405
|
+
throw new Error(`${device.name}: LibreNMS refused it: ${answer.message ?? response.status}`);
|
|
3406
|
+
}
|
|
3407
|
+
console.log(` ${device.name}: ${answer.message}`);
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3350
3411
|
// src/adapters/registries/peeringdb.ts
|
|
3351
3412
|
var API = "https://www.peeringdb.com/api";
|
|
3352
3413
|
var addressesOf = (iface) => (iface.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
|
|
@@ -3440,6 +3501,48 @@ async function sendPeeringDb(network2, changes2) {
|
|
|
3440
3501
|
}
|
|
3441
3502
|
}
|
|
3442
3503
|
|
|
3504
|
+
// src/adapters/registries/prometheus.ts
|
|
3505
|
+
var AUTH2 = { sha1: "SHA", sha256: "SHA256" };
|
|
3506
|
+
var PRIVACY2 = { aes128: "AES", des: "DES" };
|
|
3507
|
+
function prometheusTargets(network2) {
|
|
3508
|
+
return {
|
|
3509
|
+
targets: network2.devices.filter((device) => device.management?.snmp).map((device) => ({
|
|
3510
|
+
targets: [device.connection.host],
|
|
3511
|
+
labels: { device: device.name, platform: device.platform, model: device.model, __param_auth: device.name }
|
|
3512
|
+
})),
|
|
3513
|
+
withoutSnmp: network2.devices.filter((device) => !device.management?.snmp)
|
|
3514
|
+
};
|
|
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
|
+
}
|
|
3545
|
+
|
|
3443
3546
|
// src/core/validate.ts
|
|
3444
3547
|
import { X509Certificate as X509Certificate2 } from "node:crypto";
|
|
3445
3548
|
function validate(network2) {
|
|
@@ -3796,6 +3899,9 @@ var HELP = `usage: circuit <command> [device...] [options]
|
|
|
3796
3899
|
a client config for one peer, its private key left for the peer to fill
|
|
3797
3900
|
communities the network's BGP communities, one per line, for a looking glass or bgp.tools
|
|
3798
3901
|
peeringdb bring PeeringDB's exchange records in line with the config, with --confirm
|
|
3902
|
+
librenms add every device LibreNMS does not monitor yet, with --confirm
|
|
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}
|
|
3799
3905
|
secrets every secret the config uses, and whether each can be read
|
|
3800
3906
|
|
|
3801
3907
|
--config <path> the network's config file, circuit.config.ts in the working directory by default`;
|
|
@@ -4030,6 +4136,38 @@ Nothing sent. Re-run with --confirm.`);
|
|
|
4030
4136
|
}
|
|
4031
4137
|
await sendPeeringDb(network2, plan4.changes);
|
|
4032
4138
|
}
|
|
4139
|
+
async function librenmsCommand() {
|
|
4140
|
+
const plan4 = await planLibreNms(network2);
|
|
4141
|
+
for (const hostname of plan4.unknown)
|
|
4142
|
+
console.log(` LibreNMS monitors ${hostname}, which the config does not. Remove it by hand if it is gone.`);
|
|
4143
|
+
for (const device of plan4.withoutSnmp)
|
|
4144
|
+
console.log(` ${device.name} has no SNMP in its config, so LibreNMS cannot poll it. Add management.snmp first.`);
|
|
4145
|
+
for (const device of plan4.additions)
|
|
4146
|
+
console.log(` add ${device.name}, ${device.connection.host}`);
|
|
4147
|
+
if (!plan4.additions.length) {
|
|
4148
|
+
console.log("LibreNMS monitors every device it can.");
|
|
4149
|
+
return;
|
|
4150
|
+
}
|
|
4151
|
+
if (!confirmed) {
|
|
4152
|
+
console.log(`
|
|
4153
|
+
Nothing sent. Re-run with --confirm.`);
|
|
4154
|
+
return;
|
|
4155
|
+
}
|
|
4156
|
+
await sendLibreNms(network2, plan4.additions);
|
|
4157
|
+
}
|
|
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
|
+
}
|
|
4166
|
+
const { targets, withoutSnmp } = prometheusTargets(network2);
|
|
4167
|
+
for (const device of withoutSnmp)
|
|
4168
|
+
console.error(`${device.name} has no SNMP in its config; left out`);
|
|
4169
|
+
console.log(JSON.stringify(targets, null, 4));
|
|
4170
|
+
}
|
|
4033
4171
|
function secretsIn(value2, found = new Set) {
|
|
4034
4172
|
if (isSecret(value2))
|
|
4035
4173
|
found.add(referenceOf(value2));
|
|
@@ -4042,7 +4180,7 @@ function secretsIn(value2, found = new Set) {
|
|
|
4042
4180
|
return found;
|
|
4043
4181
|
}
|
|
4044
4182
|
function secretsCommand() {
|
|
4045
|
-
const references = [...secretsIn([network2.devices, network2.peeringdb])].sort();
|
|
4183
|
+
const references = [...secretsIn([network2.devices, network2.peeringdb, network2.librenms])].sort();
|
|
4046
4184
|
let missing = 0;
|
|
4047
4185
|
for (const reference of references) {
|
|
4048
4186
|
const name = reference.replace(/^file:/, "");
|
|
@@ -4068,6 +4206,8 @@ var commands = {
|
|
|
4068
4206
|
wireguard: wireguardCommand,
|
|
4069
4207
|
communities: communitiesCommand,
|
|
4070
4208
|
peeringdb: peeringdbCommand,
|
|
4209
|
+
librenms: librenmsCommand,
|
|
4210
|
+
prometheus: prometheusCommand,
|
|
4071
4211
|
secrets: secretsCommand
|
|
4072
4212
|
};
|
|
4073
4213
|
var run = commands[command2];
|
package/dist/presets/index.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
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
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Network } from "../../core/define";
|
|
2
|
+
import type { Device } from "../../schema";
|
|
3
|
+
/** What `librenms` would do. */
|
|
4
|
+
export type LibreNmsPlan = {
|
|
5
|
+
/** Devices to add. */
|
|
6
|
+
additions: Device[];
|
|
7
|
+
/** Devices that cannot be added: their config turns SNMP off. */
|
|
8
|
+
withoutSnmp: Device[];
|
|
9
|
+
/** Hostnames LibreNMS holds that no device in the config has. */
|
|
10
|
+
unknown: string[];
|
|
11
|
+
};
|
|
12
|
+
/** Compare the config with what LibreNMS monitors. A device is known to LibreNMS by its management address. */
|
|
13
|
+
export declare function planLibreNms(network: Network): Promise<LibreNmsPlan>;
|
|
14
|
+
/** Add each device, named after its `name` and polled with the credentials from its config. */
|
|
15
|
+
export declare function sendLibreNms(network: Network, additions: readonly Device[]): Promise<void>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Network } from "../../core/define";
|
|
2
|
+
import type { Device } from "../../schema";
|
|
3
|
+
export type PrometheusTarget = {
|
|
4
|
+
targets: string[];
|
|
5
|
+
labels: Record<string, string>;
|
|
6
|
+
};
|
|
7
|
+
/** The targets, and the devices left out because their config turns SNMP off. */
|
|
8
|
+
export declare function prometheusTargets(network: Network): {
|
|
9
|
+
targets: PrometheusTarget[];
|
|
10
|
+
withoutSnmp: Device[];
|
|
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
|
+
};
|
|
@@ -45,6 +45,13 @@ export type Network = {
|
|
|
45
45
|
/** An API key with write access to the network's record. */
|
|
46
46
|
api_key: Secret;
|
|
47
47
|
};
|
|
48
|
+
/** Keeps LibreNMS monitoring every device: `circuit librenms`. */
|
|
49
|
+
librenms?: {
|
|
50
|
+
/** Where LibreNMS is, such as `https://librenms.example.com`. */
|
|
51
|
+
url: string;
|
|
52
|
+
/** An API token that can add devices. */
|
|
53
|
+
api_token: Secret;
|
|
54
|
+
};
|
|
48
55
|
};
|
|
49
56
|
/** Declare the network, in `circuit.config.ts`. */
|
|
50
57
|
export declare const defineNetwork: (network: Network) => Network;
|
|
@@ -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
|
-
/**
|
|
77
|
-
do_not_announce?:
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
/**
|
|
85
|
-
blackhole?:
|
|
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
|
|
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
|
|
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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Keeps LibreNMS in step with the config: every device Circuit manages is monitored there, polled with the SNMP credentials its config gives it. A device LibreNMS holds that the config does not is reported, never deleted.
|
|
2
|
+
|
|
3
|
+
import type { Network } from "../../core/define";
|
|
4
|
+
import { resolve } from "../../core/secrets";
|
|
5
|
+
import type { Device } from "../../schema";
|
|
6
|
+
|
|
7
|
+
/** What `librenms` would do. */
|
|
8
|
+
export type LibreNmsPlan = {
|
|
9
|
+
/** Devices to add. */
|
|
10
|
+
additions: Device[];
|
|
11
|
+
/** Devices that cannot be added: their config turns SNMP off. */
|
|
12
|
+
withoutSnmp: Device[];
|
|
13
|
+
/** Hostnames LibreNMS holds that no device in the config has. */
|
|
14
|
+
unknown: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** LibreNMS's names for the algorithms. */
|
|
18
|
+
const AUTH = { sha1: "SHA", sha256: "SHA-256" } as const;
|
|
19
|
+
const PRIVACY = { aes128: "AES", des: "DES" } as const;
|
|
20
|
+
|
|
21
|
+
function settingsOf(network: Network) {
|
|
22
|
+
if (!network.librenms) throw new Error("the network has no librenms settings");
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
base: `${network.librenms.url.replace(/\/+$/, "")}/api/v0`,
|
|
26
|
+
// The header every version of API v0 accepts.
|
|
27
|
+
headers: { "X-Auth-Token": resolve(network.librenms.api_token), "Content-Type": "application/json" },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** How LibreNMS polls a device: its first SNMP version 3 user, or else its version 2c community. */
|
|
32
|
+
function credentials(device: Device): Record<string, string> | undefined {
|
|
33
|
+
const snmp = device.management?.snmp;
|
|
34
|
+
const [name, user] = Object.entries(snmp?.users ?? {})[0] ?? [];
|
|
35
|
+
|
|
36
|
+
if (name && user) {
|
|
37
|
+
return {
|
|
38
|
+
snmpver: "v3",
|
|
39
|
+
authlevel: "authPriv",
|
|
40
|
+
authname: name,
|
|
41
|
+
authpass: resolve(user.auth_password),
|
|
42
|
+
authalgo: AUTH[user.auth],
|
|
43
|
+
cryptopass: resolve(user.privacy_password),
|
|
44
|
+
cryptoalgo: PRIVACY[user.privacy],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (snmp?.community) return { snmpver: "v2c", community: resolve(snmp.community) };
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Compare the config with what LibreNMS monitors. A device is known to LibreNMS by its management address. */
|
|
53
|
+
export async function planLibreNms(network: Network): Promise<LibreNmsPlan> {
|
|
54
|
+
const { base, headers } = settingsOf(network);
|
|
55
|
+
|
|
56
|
+
const response = await fetch(`${base}/devices?type=all`, { headers, signal: AbortSignal.timeout(30_000) });
|
|
57
|
+
if (!response.ok) throw new Error(`LibreNMS answered ${response.status} to the device list`);
|
|
58
|
+
|
|
59
|
+
const monitored = new Set(((await response.json()) as { devices: { hostname: string }[] }).devices.map((device) => device.hostname));
|
|
60
|
+
const ours = new Set(network.devices.map((device) => device.connection.host));
|
|
61
|
+
const missing = network.devices.filter((device) => !monitored.has(device.connection.host));
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
additions: missing.filter((device) => device.management?.snmp),
|
|
65
|
+
withoutSnmp: missing.filter((device) => !device.management?.snmp),
|
|
66
|
+
unknown: [...monitored].filter((hostname) => !ours.has(hostname)).sort(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Add each device, named after its `name` and polled with the credentials from its config. */
|
|
71
|
+
export async function sendLibreNms(network: Network, additions: readonly Device[]): Promise<void> {
|
|
72
|
+
const { base, headers } = settingsOf(network);
|
|
73
|
+
|
|
74
|
+
for (const device of additions) {
|
|
75
|
+
const body = { hostname: device.connection.host, display_template: device.name, ...credentials(device) };
|
|
76
|
+
const response = await fetch(`${base}/devices`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers,
|
|
79
|
+
body: JSON.stringify(body),
|
|
80
|
+
signal: AbortSignal.timeout(60_000),
|
|
81
|
+
});
|
|
82
|
+
const answer = (await response.json().catch(() => ({}))) as { status?: string; message?: string };
|
|
83
|
+
|
|
84
|
+
if (!response.ok || answer.status !== "ok") {
|
|
85
|
+
throw new Error(`${device.name}: LibreNMS refused it: ${answer.message ?? response.status}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(` ${device.name}: ${answer.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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`.
|
|
4
|
+
|
|
5
|
+
import type { Network } from "../../core/define";
|
|
6
|
+
import type { Device, Secret } from "../../schema";
|
|
7
|
+
|
|
8
|
+
export type PrometheusTarget = { targets: string[]; labels: Record<string, string> };
|
|
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
|
+
|
|
14
|
+
/** The targets, and the devices left out because their config turns SNMP off. */
|
|
15
|
+
export function prometheusTargets(network: Network): { targets: PrometheusTarget[]; withoutSnmp: Device[] } {
|
|
16
|
+
return {
|
|
17
|
+
targets: network.devices
|
|
18
|
+
.filter((device) => device.management?.snmp)
|
|
19
|
+
.map((device) => ({
|
|
20
|
+
targets: [device.connection.host],
|
|
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 },
|
|
23
|
+
})),
|
|
24
|
+
withoutSnmp: network.devices.filter((device) => !device.management?.snmp),
|
|
25
|
+
};
|
|
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
|
@@ -7,7 +7,9 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
7
7
|
import { adapters } from "../adapters/devices";
|
|
8
8
|
import type { ApplyOptions, DeviceAdapter, Plan } from "../adapters/devices/types";
|
|
9
9
|
import { registries } from "../adapters/registries";
|
|
10
|
+
import { planLibreNms, sendLibreNms } from "../adapters/registries/librenms";
|
|
10
11
|
import { planPeeringDb, sendPeeringDb } from "../adapters/registries/peeringdb";
|
|
12
|
+
import { prometheusTargets, snmpExporterAuths } from "../adapters/registries/prometheus";
|
|
11
13
|
import { network as prefixOf } from "../core/addr";
|
|
12
14
|
import type { Network } from "../core/define";
|
|
13
15
|
import { isSecret, referenceOf, resolve as reveal, sourceOf, useRoot } from "../core/secrets";
|
|
@@ -29,6 +31,9 @@ const HELP = `usage: circuit <command> [device...] [options]
|
|
|
29
31
|
a client config for one peer, its private key left for the peer to fill
|
|
30
32
|
communities the network's BGP communities, one per line, for a looking glass or bgp.tools
|
|
31
33
|
peeringdb bring PeeringDB's exchange records in line with the config, with --confirm
|
|
34
|
+
librenms add every device LibreNMS does not monitor yet, with --confirm
|
|
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}
|
|
32
37
|
secrets every secret the config uses, and whether each can be read
|
|
33
38
|
|
|
34
39
|
--config <path> the network's config file, circuit.config.ts in the working directory by default`;
|
|
@@ -325,6 +330,41 @@ async function peeringdbCommand(): Promise<void> {
|
|
|
325
330
|
await sendPeeringDb(network, plan.changes);
|
|
326
331
|
}
|
|
327
332
|
|
|
333
|
+
async function librenmsCommand(): Promise<void> {
|
|
334
|
+
const plan = await planLibreNms(network);
|
|
335
|
+
|
|
336
|
+
for (const hostname of plan.unknown)
|
|
337
|
+
console.log(` LibreNMS monitors ${hostname}, which the config does not. Remove it by hand if it is gone.`);
|
|
338
|
+
for (const device of plan.withoutSnmp)
|
|
339
|
+
console.log(` ${device.name} has no SNMP in its config, so LibreNMS cannot poll it. Add management.snmp first.`);
|
|
340
|
+
for (const device of plan.additions) console.log(` add ${device.name}, ${device.connection.host}`);
|
|
341
|
+
|
|
342
|
+
if (!plan.additions.length) {
|
|
343
|
+
console.log("LibreNMS monitors every device it can.");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!confirmed) {
|
|
348
|
+
console.log("\nNothing sent. Re-run with --confirm.");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
await sendLibreNms(network, plan.additions);
|
|
353
|
+
}
|
|
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
|
+
|
|
363
|
+
const { targets, withoutSnmp } = prometheusTargets(network);
|
|
364
|
+
for (const device of withoutSnmp) console.error(`${device.name} has no SNMP in its config; left out`);
|
|
365
|
+
console.log(JSON.stringify(targets, null, 4));
|
|
366
|
+
}
|
|
367
|
+
|
|
328
368
|
/** Every secret reference in a value, however deep. */
|
|
329
369
|
function secretsIn(value: unknown, found = new Set<string>()): Set<string> {
|
|
330
370
|
if (isSecret(value)) found.add(referenceOf(value));
|
|
@@ -335,7 +375,7 @@ function secretsIn(value: unknown, found = new Set<string>()): Set<string> {
|
|
|
335
375
|
}
|
|
336
376
|
|
|
337
377
|
function secretsCommand(): void {
|
|
338
|
-
const references = [...secretsIn([network.devices, network.peeringdb])].sort();
|
|
378
|
+
const references = [...secretsIn([network.devices, network.peeringdb, network.librenms])].sort();
|
|
339
379
|
let missing = 0;
|
|
340
380
|
|
|
341
381
|
for (const reference of references) {
|
|
@@ -365,6 +405,8 @@ const commands: Record<string, () => void | Promise<void>> = {
|
|
|
365
405
|
wireguard: wireguardCommand,
|
|
366
406
|
communities: communitiesCommand,
|
|
367
407
|
peeringdb: peeringdbCommand,
|
|
408
|
+
librenms: librenmsCommand,
|
|
409
|
+
prometheus: prometheusCommand,
|
|
368
410
|
secrets: secretsCommand,
|
|
369
411
|
};
|
|
370
412
|
|
package/src/core/define.ts
CHANGED
|
@@ -86,6 +86,13 @@ export type Network = {
|
|
|
86
86
|
/** An API key with write access to the network's record. */
|
|
87
87
|
api_key: Secret;
|
|
88
88
|
};
|
|
89
|
+
/** Keeps LibreNMS monitoring every device: `circuit librenms`. */
|
|
90
|
+
librenms?: {
|
|
91
|
+
/** Where LibreNMS is, such as `https://librenms.example.com`. */
|
|
92
|
+
url: string;
|
|
93
|
+
/** An API token that can add devices. */
|
|
94
|
+
api_token: Secret;
|
|
95
|
+
};
|
|
89
96
|
};
|
|
90
97
|
|
|
91
98
|
/** Declare the network, in `circuit.config.ts`. */
|
package/src/presets/index.ts
CHANGED
|
@@ -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
|
-
/**
|
|
236
|
-
do_not_announce?: number;
|
|
237
|
-
/**
|
|
238
|
-
prepend?: { once?: number; twice?: number; three_times?: number };
|
|
239
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|