@takodotid/circuit 0.14.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.
- package/dist/cli/index.js +40 -1
- package/dist/presets/index.js +64 -56
- package/dist/types/adapters/registries/prometheus.d.ts +5 -0
- package/dist/types/presets/index.d.ts +16 -12
- package/package.json +1 -1
- package/src/adapters/registries/prometheus.ts +55 -3
- package/src/cli/index.ts +9 -1
- package/src/presets/index.ts +92 -73
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`);
|
package/dist/presets/index.js
CHANGED
|
@@ -161,15 +161,14 @@ function communityScheme(scheme) {
|
|
|
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
163
|
const tag = (neighborClass, neighborAs, options = {}) => {
|
|
164
|
-
const { site } = options;
|
|
165
|
-
const isCustomer = scheme.customers?.includes(neighborClass) ?? false;
|
|
164
|
+
const { site, trusted = false } = options;
|
|
166
165
|
if (scheme.learned_at && site === undefined)
|
|
167
166
|
throw new Error(`communityScheme: tag for AS${neighborAs} needs a site, for learned_at`);
|
|
168
167
|
return {
|
|
169
168
|
description: `Learned from AS${neighborAs}`,
|
|
170
169
|
set: {
|
|
171
170
|
remove_communities: ours.standard,
|
|
172
|
-
remove_large_communities:
|
|
171
|
+
remove_large_communities: trusted ? information.map((fn) => community(fn, "*")) : ours.large,
|
|
173
172
|
add_large_communities: [
|
|
174
173
|
...scheme.learned_from ? [community(scheme.learned_from.function, scheme.learned_from.classes[neighborClass])] : [],
|
|
175
174
|
...scheme.learned_at ? [community(scheme.learned_at.function, site)] : [],
|
|
@@ -178,65 +177,69 @@ function communityScheme(scheme) {
|
|
|
178
177
|
}
|
|
179
178
|
};
|
|
180
179
|
};
|
|
181
|
-
const blackhole = (
|
|
182
|
-
|
|
180
|
+
const blackhole = (prefixSets) => {
|
|
181
|
+
const fn = scheme.blackhole?.function;
|
|
182
|
+
if (fn === undefined)
|
|
183
183
|
throw new Error("communityScheme: blackhole needs a function number in the scheme");
|
|
184
|
-
return {
|
|
185
|
-
description: "Blackhole, asked by the
|
|
186
|
-
match: { large_community: community(
|
|
184
|
+
return prefixSets.map((prefixSet) => ({
|
|
185
|
+
description: "Blackhole, asked by the neighbor",
|
|
186
|
+
match: { large_community: community(fn, 0), prefix_set: prefixSet },
|
|
187
187
|
set: { blackhole: true },
|
|
188
188
|
action: "accept"
|
|
189
|
-
};
|
|
189
|
+
}));
|
|
190
190
|
};
|
|
191
|
-
const actions = (neighborAs
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
description: `Asked not to be announced to AS${neighborAs}`,
|
|
207
|
-
match: { large_community: community(scheme.do_not_announce, neighborAs) },
|
|
208
|
-
action: "reject"
|
|
209
|
-
}
|
|
210
|
-
] : [],
|
|
211
|
-
...scheme.blackhole !== undefined && options.blackhole ? [
|
|
212
|
-
{
|
|
213
|
-
description: `Blackhole passed on to AS${neighborAs}`,
|
|
214
|
-
match: { large_community: community(scheme.blackhole, 0) },
|
|
215
|
-
set: {
|
|
216
|
-
remove_communities: ours.standard,
|
|
217
|
-
remove_large_communities: ours.large,
|
|
218
|
-
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"
|
|
219
206
|
},
|
|
220
|
-
action: "accept"
|
|
221
|
-
}
|
|
222
|
-
] : [],
|
|
223
|
-
...PREPENDS.flatMap(([name, times]) => {
|
|
224
|
-
const fn = scheme.prepend?.[name];
|
|
225
|
-
if (fn === undefined)
|
|
226
|
-
return [];
|
|
227
|
-
return [
|
|
228
207
|
{
|
|
229
|
-
description: `Asked to
|
|
230
|
-
match: { large_community: community(
|
|
231
|
-
|
|
208
|
+
description: `Asked not to be announced to AS${neighborAs}`,
|
|
209
|
+
match: { large_community: community(scheme.do_not_announce, neighborAs) },
|
|
210
|
+
action: "reject"
|
|
232
211
|
}
|
|
233
|
-
]
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
};
|
|
240
243
|
const catalogue = [
|
|
241
244
|
...Object.entries(scheme.learned_from?.classes ?? {}).map(([name, value]) => ({
|
|
242
245
|
community: community(scheme.learned_from.function, value),
|
|
@@ -258,7 +261,12 @@ function communityScheme(scheme) {
|
|
|
258
261
|
const fn = scheme.prepend?.[name];
|
|
259
262
|
return fn === undefined ? [] : [{ community: community(fn, "nnn"), description: `Prepend ${name.replace("_", " ")} to AS$0` }];
|
|
260
263
|
}),
|
|
261
|
-
...scheme.blackhole !== undefined ? [
|
|
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
|
+
] : []
|
|
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,34 +71,40 @@ 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
|
-
/**
|
|
74
|
+
/** A neighbor asks: do not announce the route to an AS, `asn:function:asn`, or to anyone, `asn:function:0`. */
|
|
77
75
|
do_not_announce?: number;
|
|
78
|
-
/**
|
|
76
|
+
/** A neighbor asks: prepend toward an AS once, twice or three times, `asn:function:asn`. */
|
|
79
77
|
prepend?: {
|
|
80
78
|
once?: number;
|
|
81
79
|
twice?: number;
|
|
82
80
|
three_times?: number;
|
|
83
81
|
};
|
|
84
|
-
/**
|
|
85
|
-
|
|
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?: {
|
|
88
|
+
function: number;
|
|
89
|
+
upstreams?: Readonly<Record<number, string>>;
|
|
90
|
+
};
|
|
86
91
|
};
|
|
87
92
|
/**
|
|
88
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.
|
|
89
94
|
*
|
|
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
|
|
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`.
|
|
91
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.
|
|
92
|
-
* - `blackhole(
|
|
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.
|
|
93
98
|
* - `catalogue`: every community, for `communities` in `defineNetwork`.
|
|
94
99
|
*/
|
|
95
100
|
export declare function communityScheme<const Class extends string>(scheme: CommunityScheme<Class>): {
|
|
96
101
|
community: (fn: number, parameter: number | string) => string;
|
|
97
102
|
tag: (neighborClass: Class, neighborAs: number, options?: {
|
|
98
103
|
site?: number;
|
|
104
|
+
trusted?: boolean;
|
|
99
105
|
}) => PolicyRule<never, never>;
|
|
100
|
-
actions: (neighborAs: number,
|
|
101
|
-
|
|
102
|
-
}) => PolicyRule<never, never>[];
|
|
103
|
-
blackhole: <S extends string>(prefixSet: S) => PolicyRule<never, S>;
|
|
106
|
+
actions: (neighborAs: number) => PolicyRule<never, never>[];
|
|
107
|
+
blackhole: <S extends string>(prefixSets: readonly S[]) => PolicyRule<never, S>[];
|
|
104
108
|
catalogue: Community[];
|
|
105
109
|
};
|
|
106
110
|
/** A member of an internet exchange: a network with a port on the fabric and an address on the peering LAN. */
|
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
// Prometheus targets for snmp_exporter, in its file-based discovery format: one
|
|
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
|
-
|
|
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));
|
package/src/presets/index.ts
CHANGED
|
@@ -226,18 +226,20 @@ 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
|
-
/**
|
|
233
|
+
/** A neighbor asks: do not announce the route to an AS, `asn:function:asn`, or to anyone, `asn:function:0`. */
|
|
236
234
|
do_not_announce?: number;
|
|
237
|
-
/**
|
|
235
|
+
/** A neighbor asks: prepend toward an AS once, twice or three times, `asn:function:asn`. */
|
|
238
236
|
prepend?: { once?: number; twice?: number; three_times?: number };
|
|
239
|
-
/**
|
|
240
|
-
|
|
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>> };
|
|
241
243
|
};
|
|
242
244
|
|
|
243
245
|
const PREPENDS = [
|
|
@@ -251,9 +253,9 @@ const article = (word: string) => (/^[aeiou]/i.test(word) ? `an ${word}` : `a ${
|
|
|
251
253
|
/**
|
|
252
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.
|
|
253
255
|
*
|
|
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
|
|
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`.
|
|
255
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.
|
|
256
|
-
* - `blackhole(
|
|
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.
|
|
257
259
|
* - `catalogue`: every community, for `communities` in `defineNetwork`.
|
|
258
260
|
*/
|
|
259
261
|
export function communityScheme<const Class extends string>(scheme: CommunityScheme<Class>) {
|
|
@@ -265,9 +267,12 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
|
|
|
265
267
|
(fn): fn is number => fn !== undefined
|
|
266
268
|
);
|
|
267
269
|
|
|
268
|
-
const tag = (
|
|
269
|
-
|
|
270
|
-
|
|
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;
|
|
271
276
|
if (scheme.learned_at && site === undefined)
|
|
272
277
|
throw new Error(`communityScheme: tag for AS${neighborAs} needs a site, for learned_at`);
|
|
273
278
|
|
|
@@ -275,7 +280,8 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
|
|
|
275
280
|
description: `Learned from AS${neighborAs}`,
|
|
276
281
|
set: {
|
|
277
282
|
remove_communities: ours.standard,
|
|
278
|
-
|
|
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,
|
|
279
285
|
add_large_communities: [
|
|
280
286
|
...(scheme.learned_from ? [community(scheme.learned_from.function, scheme.learned_from.classes[neighborClass])] : []),
|
|
281
287
|
...(scheme.learned_at ? [community(scheme.learned_at.function, site!)] : []),
|
|
@@ -285,72 +291,78 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
|
|
|
285
291
|
};
|
|
286
292
|
};
|
|
287
293
|
|
|
288
|
-
const blackhole = <S extends string>(
|
|
289
|
-
|
|
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");
|
|
290
297
|
|
|
291
|
-
return {
|
|
292
|
-
description: "Blackhole, asked by the
|
|
293
|
-
match: { large_community: community(
|
|
298
|
+
return prefixSets.map((prefixSet) => ({
|
|
299
|
+
description: "Blackhole, asked by the neighbor",
|
|
300
|
+
match: { large_community: community(fn, 0), prefix_set: prefixSet },
|
|
294
301
|
set: { blackhole: true },
|
|
295
302
|
action: "accept",
|
|
296
|
-
};
|
|
303
|
+
}));
|
|
297
304
|
};
|
|
298
305
|
|
|
299
|
-
const actions = (neighborAs: number
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
? [
|
|
311
|
-
{
|
|
312
|
-
description: "Asked not to be announced anywhere",
|
|
313
|
-
match: { large_community: community(scheme.do_not_announce, 0) },
|
|
314
|
-
action: "reject" as const,
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
description: `Asked not to be announced to AS${neighborAs}`,
|
|
318
|
-
match: { large_community: community(scheme.do_not_announce, neighborAs) },
|
|
319
|
-
action: "reject" as const,
|
|
320
|
-
},
|
|
321
|
-
]
|
|
322
|
-
: []),
|
|
323
|
-
...(scheme.blackhole !== undefined && options.blackhole
|
|
324
|
-
? [
|
|
325
|
-
{
|
|
326
|
-
description: `Blackhole passed on to AS${neighborAs}`,
|
|
327
|
-
match: { large_community: community(scheme.blackhole, 0) },
|
|
328
|
-
set: {
|
|
329
|
-
remove_communities: ours.standard,
|
|
330
|
-
remove_large_communities: ours.large,
|
|
331
|
-
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,
|
|
332
317
|
},
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
+
};
|
|
354
366
|
|
|
355
367
|
const catalogue: Community[] = [
|
|
356
368
|
...Object.entries<number>(scheme.learned_from?.classes ?? {}).map(([name, value]) => ({
|
|
@@ -380,7 +392,14 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
|
|
|
380
392
|
return fn === undefined ? [] : [{ community: community(fn, "nnn"), description: `Prepend ${name.replace("_", " ")} to AS$0` }];
|
|
381
393
|
}),
|
|
382
394
|
...(scheme.blackhole !== undefined
|
|
383
|
-
? [
|
|
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
|
+
]
|
|
384
403
|
: []),
|
|
385
404
|
];
|
|
386
405
|
|