]>
Commit | Line | Data |
---|---|---|
1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ | |
2 | ||
3 | #include <getopt.h> | |
4 | #include <stdio.h> | |
5 | #include <sys/stat.h> | |
6 | ||
7 | #include "alloc-util.h" | |
8 | #include "build.h" | |
9 | #include "conf-files.h" | |
10 | #include "constants.h" | |
11 | #include "creds-util.h" | |
12 | #include "errno-util.h" | |
13 | #include "fd-util.h" | |
14 | #include "fileio.h" | |
15 | #include "glob-util.h" | |
16 | #include "hashmap.h" | |
17 | #include "log.h" | |
18 | #include "main-func.h" | |
19 | #include "pager.h" | |
20 | #include "path-util.h" | |
21 | #include "pretty-print.h" | |
22 | #include "string-util.h" | |
23 | #include "strv.h" | |
24 | #include "sysctl-util.h" | |
25 | ||
26 | static char **arg_prefixes = NULL; | |
27 | static CatFlags arg_cat_flags = CAT_CONFIG_OFF; | |
28 | static bool arg_strict = false; | |
29 | static PagerFlags arg_pager_flags = 0; | |
30 | ||
31 | STATIC_DESTRUCTOR_REGISTER(arg_prefixes, strv_freep); | |
32 | ||
33 | typedef struct Option { | |
34 | char *key; | |
35 | char *value; | |
36 | bool ignore_failure; | |
37 | } Option; | |
38 | ||
39 | static Option *option_free(Option *o) { | |
40 | if (!o) | |
41 | return NULL; | |
42 | ||
43 | free(o->key); | |
44 | free(o->value); | |
45 | ||
46 | return mfree(o); | |
47 | } | |
48 | ||
49 | DEFINE_TRIVIAL_CLEANUP_FUNC(Option*, option_free); | |
50 | DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(option_hash_ops, char, string_hash_func, string_compare_func, Option, option_free); | |
51 | ||
52 | static bool test_prefix(const char *p) { | |
53 | if (strv_isempty(arg_prefixes)) | |
54 | return true; | |
55 | ||
56 | return path_startswith_strv(p, arg_prefixes); | |
57 | } | |
58 | ||
59 | static Option *option_new( | |
60 | const char *key, | |
61 | const char *value, | |
62 | bool ignore_failure) { | |
63 | ||
64 | _cleanup_(option_freep) Option *o = NULL; | |
65 | ||
66 | assert(key); | |
67 | ||
68 | o = new(Option, 1); | |
69 | if (!o) | |
70 | return NULL; | |
71 | ||
72 | *o = (Option) { | |
73 | .key = strdup(key), | |
74 | .value = value ? strdup(value) : NULL, | |
75 | .ignore_failure = ignore_failure, | |
76 | }; | |
77 | ||
78 | if (!o->key) | |
79 | return NULL; | |
80 | if (value && !o->value) | |
81 | return NULL; | |
82 | ||
83 | return TAKE_PTR(o); | |
84 | } | |
85 | ||
86 | static int sysctl_write_or_warn(const char *key, const char *value, bool ignore_failure, bool ignore_enoent) { | |
87 | int r; | |
88 | ||
89 | r = sysctl_write(key, value); | |
90 | if (r < 0) { | |
91 | /* Proceed without failing if ignore_failure is true. | |
92 | * If the sysctl is not available in the kernel or we are running with reduced privileges and | |
93 | * cannot write it, then log about the issue, and proceed without failing. Unless strict mode | |
94 | * (arg_strict = true) is enabled, in which case we should fail. (EROFS is treated as a | |
95 | * permission problem here, since that's how container managers usually protected their | |
96 | * sysctls.) | |
97 | * In all other cases log an error and make the tool fail. */ | |
98 | if (ignore_failure || (!arg_strict && (r == -EROFS || ERRNO_IS_PRIVILEGE(r)))) | |
99 | log_debug_errno(r, "Couldn't write '%s' to '%s', ignoring: %m", value, key); | |
100 | else if (ignore_enoent && r == -ENOENT) | |
101 | log_warning_errno(r, "Couldn't write '%s' to '%s', ignoring: %m", value, key); | |
102 | else | |
103 | return log_error_errno(r, "Couldn't write '%s' to '%s': %m", value, key); | |
104 | } | |
105 | ||
106 | return 0; | |
107 | } | |
108 | ||
109 | static int apply_glob_option_with_prefix(OrderedHashmap *sysctl_options, Option *option, const char *prefix) { | |
110 | _cleanup_strv_free_ char **paths = NULL; | |
111 | _cleanup_free_ char *pattern = NULL; | |
112 | int r; | |
113 | ||
114 | assert(sysctl_options); | |
115 | assert(option); | |
116 | ||
117 | if (prefix) { | |
118 | _cleanup_free_ char *key = NULL; | |
119 | ||
120 | r = path_glob_can_match(option->key, prefix, &key); | |
121 | if (r < 0) | |
122 | return log_error_errno(r, "Failed to check if the glob '%s' matches prefix '%s': %m", | |
123 | option->key, prefix); | |
124 | if (r == 0) { | |
125 | log_debug("The glob '%s' does not match prefix '%s'.", option->key, prefix); | |
126 | return 0; | |
127 | } | |
128 | ||
129 | log_debug("The glob '%s' is prefixed with '%s': '%s'", option->key, prefix, key); | |
130 | ||
131 | if (!string_is_glob(key)) { | |
132 | /* The prefixed pattern is not glob anymore. Let's skip to call glob(). */ | |
133 | if (ordered_hashmap_contains(sysctl_options, key)) { | |
134 | log_debug("Not setting %s (explicit setting exists).", key); | |
135 | return 0; | |
136 | } | |
137 | ||
138 | return sysctl_write_or_warn(key, option->value, | |
139 | /* ignore_failure = */ option->ignore_failure, | |
140 | /* ignore_enoent = */ true); | |
141 | } | |
142 | ||
143 | pattern = path_join("/proc/sys", key); | |
144 | } else | |
145 | pattern = path_join("/proc/sys", option->key); | |
146 | if (!pattern) | |
147 | return log_oom(); | |
148 | ||
149 | r = glob_extend(&paths, pattern, GLOB_NOCHECK); | |
150 | if (r < 0) { | |
151 | if (r == -ENOENT) { | |
152 | log_debug("No match for glob: %s", option->key); | |
153 | return 0; | |
154 | } | |
155 | if (option->ignore_failure || ERRNO_IS_PRIVILEGE(r)) { | |
156 | log_debug_errno(r, "Failed to resolve glob '%s', ignoring: %m", option->key); | |
157 | return 0; | |
158 | } else | |
159 | return log_error_errno(r, "Couldn't resolve glob '%s': %m", option->key); | |
160 | } | |
161 | ||
162 | STRV_FOREACH(s, paths) { | |
163 | const char *key; | |
164 | ||
165 | assert_se(key = path_startswith(*s, "/proc/sys")); | |
166 | ||
167 | if (ordered_hashmap_contains(sysctl_options, key)) { | |
168 | log_debug("Not setting %s (explicit setting exists).", key); | |
169 | continue; | |
170 | } | |
171 | ||
172 | RET_GATHER(r, | |
173 | sysctl_write_or_warn(key, option->value, | |
174 | /* ignore_failure = */ option->ignore_failure, | |
175 | /* ignore_enoent = */ !arg_strict)); | |
176 | } | |
177 | ||
178 | return r; | |
179 | } | |
180 | ||
181 | static int apply_glob_option(OrderedHashmap *sysctl_options, Option *option) { | |
182 | int r = 0; | |
183 | ||
184 | if (strv_isempty(arg_prefixes)) | |
185 | return apply_glob_option_with_prefix(sysctl_options, option, NULL); | |
186 | ||
187 | STRV_FOREACH(i, arg_prefixes) | |
188 | RET_GATHER(r, apply_glob_option_with_prefix(sysctl_options, option, *i)); | |
189 | return r; | |
190 | } | |
191 | ||
192 | static int apply_all(OrderedHashmap *sysctl_options) { | |
193 | Option *option; | |
194 | int r = 0; | |
195 | ||
196 | ORDERED_HASHMAP_FOREACH(option, sysctl_options) { | |
197 | int k; | |
198 | ||
199 | /* Ignore "negative match" options, they are there only to exclude stuff from globs. */ | |
200 | if (!option->value) | |
201 | continue; | |
202 | ||
203 | if (string_is_glob(option->key)) | |
204 | k = apply_glob_option(sysctl_options, option); | |
205 | else | |
206 | k = sysctl_write_or_warn(option->key, option->value, | |
207 | /* ignore_failure = */ option->ignore_failure, | |
208 | /* ignore_enoent = */ !arg_strict); | |
209 | RET_GATHER(r, k); | |
210 | } | |
211 | ||
212 | return r; | |
213 | } | |
214 | ||
215 | static int parse_file(OrderedHashmap **sysctl_options, const char *path, bool ignore_enoent) { | |
216 | _cleanup_fclose_ FILE *f = NULL; | |
217 | _cleanup_free_ char *pp = NULL; | |
218 | unsigned c = 0; | |
219 | int r; | |
220 | ||
221 | assert(path); | |
222 | ||
223 | r = search_and_fopen(path, "re", NULL, (const char**) CONF_PATHS_STRV("sysctl.d"), &f, &pp); | |
224 | if (r < 0) { | |
225 | if (ignore_enoent && r == -ENOENT) | |
226 | return 0; | |
227 | ||
228 | return log_error_errno(r, "Failed to open file '%s', ignoring: %m", path); | |
229 | } | |
230 | ||
231 | log_debug("Parsing %s", pp); | |
232 | for (;;) { | |
233 | _cleanup_(option_freep) Option *new_option = NULL; | |
234 | _cleanup_free_ char *l = NULL; | |
235 | bool ignore_failure = false; | |
236 | Option *existing; | |
237 | char *value; | |
238 | int k; | |
239 | ||
240 | k = read_stripped_line(f, LONG_LINE_MAX, &l); | |
241 | if (k == 0) | |
242 | break; | |
243 | if (k < 0) | |
244 | return log_error_errno(k, "Failed to read file '%s', ignoring: %m", pp); | |
245 | ||
246 | c++; | |
247 | ||
248 | if (isempty(l)) | |
249 | continue; | |
250 | if (strchr(COMMENTS, l[0])) | |
251 | continue; | |
252 | ||
253 | char *p = l; | |
254 | value = strchr(p, '='); | |
255 | if (value) { | |
256 | if (p[0] == '-') { | |
257 | ignore_failure = true; | |
258 | p++; | |
259 | } | |
260 | ||
261 | *value = 0; | |
262 | value++; | |
263 | value = strstrip(value); | |
264 | ||
265 | } else { | |
266 | if (p[0] == '-') | |
267 | /* We have a "negative match" option. Let's continue with value==NULL. */ | |
268 | p++; | |
269 | else { | |
270 | log_syntax(NULL, LOG_WARNING, pp, c, 0, | |
271 | "Line is not an assignment, ignoring: %s", p); | |
272 | if (r == 0) | |
273 | r = -EINVAL; | |
274 | continue; | |
275 | } | |
276 | } | |
277 | ||
278 | p = strstrip(p); | |
279 | p = sysctl_normalize(p); | |
280 | ||
281 | /* We can't filter out globs at this point, we'll need to do that later. */ | |
282 | if (!string_is_glob(p) && | |
283 | !test_prefix(p)) | |
284 | continue; | |
285 | ||
286 | existing = ordered_hashmap_get(*sysctl_options, p); | |
287 | if (existing) { | |
288 | if (streq_ptr(value, existing->value)) { | |
289 | existing->ignore_failure = existing->ignore_failure || ignore_failure; | |
290 | continue; | |
291 | } | |
292 | ||
293 | log_debug("Overwriting earlier assignment of %s at '%s:%u'.", p, pp, c); | |
294 | option_free(ordered_hashmap_remove(*sysctl_options, p)); | |
295 | } | |
296 | ||
297 | new_option = option_new(p, value, ignore_failure); | |
298 | if (!new_option) | |
299 | return log_oom(); | |
300 | ||
301 | k = ordered_hashmap_ensure_put(sysctl_options, &option_hash_ops, new_option->key, new_option); | |
302 | if (k < 0) | |
303 | return log_error_errno(k, "Failed to add sysctl variable %s to hashmap: %m", p); | |
304 | ||
305 | TAKE_PTR(new_option); | |
306 | } | |
307 | ||
308 | return r; | |
309 | } | |
310 | ||
311 | static int read_credential_lines(OrderedHashmap **sysctl_options) { | |
312 | _cleanup_free_ char *j = NULL; | |
313 | const char *d; | |
314 | int r; | |
315 | ||
316 | r = get_credentials_dir(&d); | |
317 | if (r == -ENXIO) | |
318 | return 0; | |
319 | if (r < 0) | |
320 | return log_error_errno(r, "Failed to get credentials directory: %m"); | |
321 | ||
322 | j = path_join(d, "sysctl.extra"); | |
323 | if (!j) | |
324 | return log_oom(); | |
325 | ||
326 | (void) parse_file(sysctl_options, j, /* ignore_enoent= */ true); | |
327 | return 0; | |
328 | } | |
329 | ||
330 | static int cat_config(char **files) { | |
331 | pager_open(arg_pager_flags); | |
332 | ||
333 | return cat_files(NULL, files, arg_cat_flags); | |
334 | } | |
335 | ||
336 | static int help(void) { | |
337 | _cleanup_free_ char *link = NULL; | |
338 | int r; | |
339 | ||
340 | r = terminal_urlify_man("systemd-sysctl.service", "8", &link); | |
341 | if (r < 0) | |
342 | return log_oom(); | |
343 | ||
344 | printf("%s [OPTIONS...] [CONFIGURATION FILE...]\n\n" | |
345 | "Applies kernel sysctl settings.\n\n" | |
346 | " -h --help Show this help\n" | |
347 | " --version Show package version\n" | |
348 | " --cat-config Show configuration files\n" | |
349 | " --tldr Show non-comment parts of configuration\n" | |
350 | " --prefix=PATH Only apply rules with the specified prefix\n" | |
351 | " --no-pager Do not pipe output into a pager\n" | |
352 | "\nSee the %s for details.\n", | |
353 | program_invocation_short_name, | |
354 | link); | |
355 | ||
356 | return 0; | |
357 | } | |
358 | ||
359 | static int parse_argv(int argc, char *argv[]) { | |
360 | ||
361 | enum { | |
362 | ARG_VERSION = 0x100, | |
363 | ARG_CAT_CONFIG, | |
364 | ARG_TLDR, | |
365 | ARG_PREFIX, | |
366 | ARG_NO_PAGER, | |
367 | ARG_STRICT, | |
368 | }; | |
369 | ||
370 | static const struct option options[] = { | |
371 | { "help", no_argument, NULL, 'h' }, | |
372 | { "version", no_argument, NULL, ARG_VERSION }, | |
373 | { "cat-config", no_argument, NULL, ARG_CAT_CONFIG }, | |
374 | { "tldr", no_argument, NULL, ARG_TLDR }, | |
375 | { "prefix", required_argument, NULL, ARG_PREFIX }, | |
376 | { "no-pager", no_argument, NULL, ARG_NO_PAGER }, | |
377 | { "strict", no_argument, NULL, ARG_STRICT }, | |
378 | {} | |
379 | }; | |
380 | ||
381 | int c; | |
382 | ||
383 | assert(argc >= 0); | |
384 | assert(argv); | |
385 | ||
386 | while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) | |
387 | ||
388 | switch (c) { | |
389 | ||
390 | case 'h': | |
391 | return help(); | |
392 | ||
393 | case ARG_VERSION: | |
394 | return version(); | |
395 | ||
396 | case ARG_CAT_CONFIG: | |
397 | arg_cat_flags = CAT_CONFIG_ON; | |
398 | break; | |
399 | ||
400 | case ARG_TLDR: | |
401 | arg_cat_flags = CAT_TLDR; | |
402 | break; | |
403 | ||
404 | case ARG_PREFIX: { | |
405 | const char *s; | |
406 | char *p; | |
407 | ||
408 | /* We used to require people to specify absolute paths | |
409 | * in /proc/sys in the past. This is kinda useless, but | |
410 | * we need to keep compatibility. We now support any | |
411 | * sysctl name available. */ | |
412 | sysctl_normalize(optarg); | |
413 | ||
414 | s = path_startswith(optarg, "/proc/sys"); | |
415 | p = strdup(s ?: optarg); | |
416 | if (!p) | |
417 | return log_oom(); | |
418 | ||
419 | if (strv_consume(&arg_prefixes, p) < 0) | |
420 | return log_oom(); | |
421 | ||
422 | break; | |
423 | } | |
424 | ||
425 | case ARG_NO_PAGER: | |
426 | arg_pager_flags |= PAGER_DISABLE; | |
427 | break; | |
428 | ||
429 | case ARG_STRICT: | |
430 | arg_strict = true; | |
431 | break; | |
432 | ||
433 | case '?': | |
434 | return -EINVAL; | |
435 | ||
436 | default: | |
437 | assert_not_reached(); | |
438 | } | |
439 | ||
440 | if (arg_cat_flags != CAT_CONFIG_OFF && argc > optind) | |
441 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), | |
442 | "Positional arguments are not allowed with --cat-config/--tldr."); | |
443 | ||
444 | return 1; | |
445 | } | |
446 | ||
447 | static int run(int argc, char *argv[]) { | |
448 | _cleanup_ordered_hashmap_free_ OrderedHashmap *sysctl_options = NULL; | |
449 | int r; | |
450 | ||
451 | r = parse_argv(argc, argv); | |
452 | if (r <= 0) | |
453 | return r; | |
454 | ||
455 | log_setup(); | |
456 | ||
457 | umask(0022); | |
458 | ||
459 | if (argc > optind) { | |
460 | r = 0; | |
461 | ||
462 | for (int i = optind; i < argc; i++) | |
463 | RET_GATHER(r, parse_file(&sysctl_options, argv[i], false)); | |
464 | ||
465 | } else { | |
466 | _cleanup_strv_free_ char **files = NULL; | |
467 | ||
468 | r = conf_files_list_strv(&files, ".conf", NULL, 0, (const char**) CONF_PATHS_STRV("sysctl.d")); | |
469 | if (r < 0) | |
470 | return log_error_errno(r, "Failed to enumerate sysctl.d files: %m"); | |
471 | ||
472 | if (arg_cat_flags != CAT_CONFIG_OFF) | |
473 | return cat_config(files); | |
474 | ||
475 | STRV_FOREACH(f, files) | |
476 | RET_GATHER(r, parse_file(&sysctl_options, *f, true)); | |
477 | ||
478 | RET_GATHER(r, read_credential_lines(&sysctl_options)); | |
479 | } | |
480 | ||
481 | RET_GATHER(r, apply_all(sysctl_options)); | |
482 | ||
483 | return r; | |
484 | } | |
485 | ||
486 | DEFINE_MAIN_FUNCTION(run); |