Repository navigation
Expand file tree
/
Copy pathmodule.nix
More file actions
360 lines (321 loc) · 14.2 KB
/
Copy pathmodule.nix
File metadata and controls
360 lines (321 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
{ std, inputs, ... }:
{ config, pkgs, ... }:
let
cfg = config.services.rfm;
toml = pkgs.formats.toml { };
configFile = toml.generate "rfm.toml" cfg.settings;
# a port the unit listens on or sends from, it holds no
# CAP_NET_BIND_SERVICE for one below 1024
boundPort = std.types.ints.between 1024 65535;
in
{
options.services.rfm = {
enable = std.mkEnableOption "RFM (Router Flow Monitor)";
package = std.mkPackageOption inputs.self.packages.${pkgs.stdenv.hostPlatform.system} "default" { };
settings = std.mkOption {
type = std.types.submodule {
options.agent = std.mkOption {
type = std.types.submodule {
options = {
interfaces = std.mkOption {
type = std.types.listOf std.types.str;
description = ''
Network interfaces to monitor. Each entry is a Go regular
expression matched against system interface names with
implicit full-string anchoring. Use `[".*"]` for all
interfaces, `["enp.*"]` for the enp prefix, or list
exact names like `["eth0"]`. Overlapping patterns are
deduplicated by interface index, so `["eth0", "eth.*"]`
attaches each interface at most once. Links without an
Ethernet header, such as WireGuard, tun or xfrm
interfaces, are skipped with a warning.
'';
example = [
"eth0"
"enp.*"
];
};
bpf = std.mkOption {
default = { };
type = std.types.submodule {
options = {
sample_rate = std.mkOption {
type = std.types.ints.positive;
default = 100;
description = "Sample 1 in N skbs for flow events, an event carries every wire packet of its skb.";
};
ring_buf_size = std.mkOption {
type = std.types.ints.positive;
default = 262144;
description = "Ring buffer size in bytes.";
};
wakeup_batch = std.mkOption {
type = std.types.ints.positive;
default = 64;
description = "Force a ring buffer wakeup every N flow events a CPU submits.";
};
iface_stats_size = std.mkOption {
type = std.types.ints.unsigned;
default = 0;
description = "Override the BPF iface stats map capacity, 0 keeps the default of 4096 counter keys. A pinned map of another capacity fails the start until the pin is removed or the host reboots.";
};
adaptive_sampling = std.mkOption {
type = std.types.bool;
default = false;
description = "Raise the sample rate while the ring buffer drops events and relax it afterwards.";
};
max_sample_rate = std.mkOption {
type = std.types.ints.positive;
default = 1000;
description = "Upper bound for the adaptive sample rate and for any rate set with `rfm set sample-rate`.";
};
# the unit opens the mount root to its user and creates
# the pin directory in it, nothing deeper
pin_path = std.mkOption {
type = std.types.strMatching "(/sys/fs/bpf/[^/]+)?";
default = "/sys/fs/bpf/rfm";
description = "bpffs directory directly under /sys/fs/bpf that keeps the interface counters across restarts, empty keeps them private to the process.";
};
};
};
};
collector = std.mkOption {
default = { };
type = std.types.submodule {
options = {
max_flows = std.mkOption {
type = std.types.ints.positive;
default = 65536;
description = "Maximum number of active flows. It also limits the label tuples of the flow metrics, a new tuple with enrichment labels finds room while fewer than max_flows tuples exist or takes the place of the longest idle one a scrape has shown, and otherwise its flow counts under empty labels.";
};
eviction_timeout = std.mkOption {
type = std.types.str;
default = "30s";
description = "Flow eviction timeout (Go duration).";
};
active_timeout = std.mkOption {
type = std.types.str;
default = "60s";
description = "Interval after which a live flow is exported as a delta record over IPFIX (Go duration), \"0s\" disables it.";
};
};
};
};
ipfix = std.mkOption {
default = { };
type = std.types.submodule {
options = {
host = std.mkOption {
type = std.types.str;
default = "";
description = "IPFIX collector host.";
};
port = std.mkOption {
type = std.types.ints.between 0 65535;
default = 0;
description = "IPFIX collector UDP port, 0 means 4739 when a host is set and no export otherwise.";
};
bind = std.mkOption {
default = { };
type = std.types.submodule {
options = {
host = std.mkOption {
type = std.types.str;
default = "";
description = "IPFIX exporter local source address.";
};
port = std.mkOption {
type = std.types.either (std.types.enum [ 0 ]) boundPort;
default = 0;
description = "IPFIX exporter local source port (0 = ephemeral).";
};
};
};
};
template_refresh = std.mkOption {
type = std.types.str;
default = "60s";
description = "How often UDP IPFIX templates are re-sent (Go duration).";
};
observation_domain_id = std.mkOption {
type = std.types.ints.positive;
default = 1;
description = "IPFIX observation domain id used in exported messages.";
};
queue_size = std.mkOption {
type = std.types.ints.unsigned;
default = 0;
description = "Records that may wait for the IPFIX sender before new ones are dropped, 0 means max_flows or 4096, whichever is larger.";
};
flush_interval = std.mkOption {
type = std.types.str;
default = "1s";
description = "How long the IPFIX sender gathers records before sending a partial message (Go duration).";
};
max_message_size = std.mkOption {
type = std.types.ints.between 171 65535;
default = 1200;
description = "Largest IPFIX message in bytes, the UDP payload, keep it at most the path MTU minus 28 bytes for IPv4 or 48 bytes for IPv6.";
};
};
};
};
prometheus = std.mkOption {
default = { };
type = std.types.submodule {
options = {
host = std.mkOption {
type = std.types.str;
default = "::1";
description = "Prometheus metrics listen address.";
};
port = std.mkOption {
type = boundPort;
default = 9669;
description = "Prometheus metrics listen port.";
};
};
};
};
control = std.mkOption {
default = { };
type = std.types.submodule {
options = {
# the agent cannot write outside its runtime directory
# and does not create directories for the socket
socket = std.mkOption {
type = std.types.strMatching "(/run/rfm/[^/]+)?";
default = "/run/rfm/rfm.sock";
description = "Unix socket the rfm command line talks to, a file directly in /run/rfm, empty disables it.";
};
};
};
};
enrich = std.mkOption {
default = { };
type = std.types.submodule {
options = {
mmdb = std.mkOption {
default = { };
type = std.types.submodule {
options = {
asn_db = std.mkOption {
type = std.types.str;
default = "";
description = "Path to the ASN MMDB database.";
};
city_db = std.mkOption {
type = std.types.str;
default = "";
description = "Path to the city MMDB database.";
};
};
};
};
rib = std.mkOption {
default = { };
type = std.types.submodule {
options = {
bmp = std.mkOption {
default = { };
type = std.types.submodule {
options = {
host = std.mkOption {
type = std.types.str;
default = "";
description = "BMP listen host for live RIB updates.";
};
port = std.mkOption {
type = std.types.either (std.types.enum [ 0 ]) boundPort;
default = 0;
description = "BMP listen port for live RIB updates, 0 means 11019 when a host is set and no listener otherwise.";
};
};
};
};
};
};
};
};
};
};
};
};
};
};
default = { };
description = "Settings for RFM, serialized to TOML.";
};
};
config = std.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
# the agent runs as its own user with just the three capabilities the
# bpf verifier, tcx attach and map access need, root is not required
users.users.rfm = {
isSystemUser = true;
group = "rfm";
description = "Router Flow Monitor agent";
};
users.groups.rfm = { };
systemd.services.rfm = {
description = "Router Flow Monitor agent";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
# a start fails while a listen address or an mmdb file is not there
# yet, so the unit retries every few seconds without a start limit
# instead of giving up within a second, a matched interface that is
# not there yet is attached once it appears
startLimitIntervalSec = 0;
serviceConfig = {
# systemd mounts bpffs with mode 0700, which the agent's user cannot
# traverse, so the mount root is opened to 0711 (traverse only) and
# the pin directory prepared as root before the agent drops to its
# own user, pinned objects keep their own 0600 mode
ExecStartPre = std.optionals (cfg.settings.agent.bpf.pin_path != "") [
"+${pkgs.coreutils}/bin/chmod 0711 ${std.dirOf cfg.settings.agent.bpf.pin_path}"
"+${pkgs.coreutils}/bin/install -d -m 0750 -o rfm -g rfm ${cfg.settings.agent.bpf.pin_path}"
];
ExecStart = "${cfg.package}/bin/rfm agent -c ${configFile}";
Restart = "on-failure";
RestartSec = "5s";
User = "rfm";
Group = "rfm";
RuntimeDirectory = "rfm";
RuntimeDirectoryMode = "0750";
AmbientCapabilities = [
"CAP_BPF"
"CAP_NET_ADMIN"
"CAP_PERFMON"
];
CapabilityBoundingSet = [
"CAP_BPF"
"CAP_NET_ADMIN"
"CAP_PERFMON"
];
NoNewPrivileges = true;
# the tree is read only except the runtime directory, the pin
# directory lives under /sys which strict mode leaves writable
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
ProtectControlGroups = true;
ProtectKernelModules = true;
ProtectClock = true;
ProtectHostname = true;
LockPersonality = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RestrictNamespaces = true;
SystemCallArchitectures = "native";
# unix for the control socket, inet for ipfix, bmp and metrics,
# netlink for interface discovery
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
};
};
};
}