From 09d423e9219883e5cb45adc249d07845fb6d4cb9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 12 May 2018 12:50:57 -0700 Subject: nspawn: add greater control over how /etc/resolv.conf is handled Fixes: #8014 #1781 --- man/systemd-nspawn.xml | 29 +++++++++++++ man/systemd.nspawn.xml | 9 ++++ src/nspawn/nspawn-gperf.gperf | 1 + src/nspawn/nspawn-settings.c | 16 ++++++++ src/nspawn/nspawn-settings.h | 24 +++++++++-- src/nspawn/nspawn.c | 96 +++++++++++++++++++++++++++++++++++-------- 6 files changed, 155 insertions(+), 20 deletions(-) diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index 9a0e02187f..03e79683bc 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -858,6 +858,35 @@ . + + + + Configures how /etc/resolv.conf inside of the container (i.e. DNS + configuration synchronization from host to container) shall be handled. Takes one of off, + copy-host, copy-static, bind-host, + bind-static, delete or auto. If set to + off the /etc/resolv.conf file in the container is left as it is + included in the image, and neither modified nor bind mounted over. If set to copy-host, the + /etc/resolv.conf file from the host is copied into the container. Similar, if + bind-host is used, the file is bind mounted from the host into the container. If set to + copy-static the static resolv.conf file supplied with + systemd-resolved.service8 is + copied into the container, and correspondingly bind-static bind mounts it there. If set to + delete the /etc/resolv.conf file in the container is deleted if it + exists. Finally, if set to auto the file is left as it is if private networking is turned on + (see ). Otherwise, if systemd-resolved.service is + connectible its static resolv.conf file is used, and if not the host's + /etc/resolv.conf file is used. In the latter cases the file is copied if the image is + writable, and bind mounted otherwise. It's recommended to use copy if the container shall be + able to make changes to the DNS configuration on its own, deviating from the host's settings. Otherwise + bind is preferable, as it means direct changes to /etc/resolv.conf in + the container are not allowed, as it is a read-only bind mount (but note that if the container has enough + privileges, it might simply go ahead and unmount the bind mount anyway). Note that both if the file is bind + mounted and if it is copied no further propagation of configuration is generally done after the one-time early + initialization (this is because the file is usually updated through copying and renaming). Defaults to + auto. + + diff --git a/man/systemd.nspawn.xml b/man/systemd.nspawn.xml index 1780bfd79a..679052ae78 100644 --- a/man/systemd.nspawn.xml +++ b/man/systemd.nspawn.xml @@ -340,6 +340,15 @@ details. + + ResolvConf= + + Configures how /etc/resolv.conf in the container shall be handled. This is + equivalent to the command line switch, and takes the same argument. See + systemd-nspawn1 for + details. + + diff --git a/src/nspawn/nspawn-gperf.gperf b/src/nspawn/nspawn-gperf.gperf index f8234e75d4..0f31aa2ec4 100644 --- a/src/nspawn/nspawn-gperf.gperf +++ b/src/nspawn/nspawn-gperf.gperf @@ -53,6 +53,7 @@ Exec.Hostname, config_parse_hostname, 0, of Exec.NoNewPrivileges, config_parse_tristate, 0, offsetof(Settings, no_new_privileges) Exec.OOMScoreAdjust, config_parse_oom_score_adjust, 0, 0 Exec.CPUAffinity, config_parse_cpu_affinity, 0, 0 +Exec.ResolvConf, config_parse_resolv_conf, 0, offsetof(Settings, resolv_conf) Files.ReadOnly, config_parse_tristate, 0, offsetof(Settings, read_only) Files.Volatile, config_parse_volatile_mode, 0, offsetof(Settings, volatile_mode) Files.Bind, config_parse_bind, 0, 0 diff --git a/src/nspawn/nspawn-settings.c b/src/nspawn/nspawn-settings.c index 0acf718456..367f18c420 100644 --- a/src/nspawn/nspawn-settings.c +++ b/src/nspawn/nspawn-settings.c @@ -16,6 +16,7 @@ #include "process-util.h" #include "rlimit-util.h" #include "socket-util.h" +#include "string-table.h" #include "string-util.h" #include "strv.h" #include "user-util.h" @@ -35,6 +36,7 @@ int settings_load(FILE *f, const char *path, Settings **ret) { s->start_mode = _START_MODE_INVALID; s->personality = PERSONALITY_INVALID; s->userns_mode = _USER_NAMESPACE_MODE_INVALID; + s->resolv_conf = _RESOLV_CONF_MODE_INVALID; s->uid_shift = UID_INVALID; s->uid_range = UID_INVALID; s->no_new_privileges = -1; @@ -724,3 +726,17 @@ int config_parse_cpu_affinity( return 0; } + +DEFINE_CONFIG_PARSE_ENUM(config_parse_resolv_conf, resolv_conf_mode, ResolvConfMode, "Failed to parse resolv.conf mode"); + +static const char *const resolv_conf_mode_table[_RESOLV_CONF_MODE_MAX] = { + [RESOLV_CONF_OFF] = "off", + [RESOLV_CONF_COPY_HOST] = "copy-host", + [RESOLV_CONF_COPY_STATIC] = "copy-static", + [RESOLV_CONF_BIND_HOST] = "bind-host", + [RESOLV_CONF_BIND_STATIC] = "bind-static", + [RESOLV_CONF_DELETE] = "delete", + [RESOLV_CONF_AUTO] = "auto", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(resolv_conf_mode, ResolvConfMode, RESOLV_CONF_AUTO); diff --git a/src/nspawn/nspawn-settings.h b/src/nspawn/nspawn-settings.h index 8dc310d569..8b4b897fa6 100644 --- a/src/nspawn/nspawn-settings.h +++ b/src/nspawn/nspawn-settings.h @@ -33,6 +33,18 @@ typedef enum UserNamespaceMode { _USER_NAMESPACE_MODE_INVALID = -1, } UserNamespaceMode; +typedef enum ResolvConfMode { + RESOLV_CONF_OFF, + RESOLV_CONF_COPY_HOST, + RESOLV_CONF_COPY_STATIC, + RESOLV_CONF_BIND_HOST, + RESOLV_CONF_BIND_STATIC, + RESOLV_CONF_DELETE, + RESOLV_CONF_AUTO, + _RESOLV_CONF_MODE_MAX, + _RESOLV_CONF_MODE_INVALID = -1 +} ResolvConfMode; + typedef enum SettingsMask { SETTING_START_MODE = UINT64_C(1) << 0, SETTING_ENVIRONMENT = UINT64_C(1) << 1, @@ -55,9 +67,10 @@ typedef enum SettingsMask { SETTING_NO_NEW_PRIVILEGES = UINT64_C(1) << 18, SETTING_OOM_SCORE_ADJUST = UINT64_C(1) << 19, SETTING_CPU_AFFINITY = UINT64_C(1) << 20, - SETTING_RLIMIT_FIRST = UINT64_C(1) << 21, /* we define one bit per resource limit here */ - SETTING_RLIMIT_LAST = UINT64_C(1) << (21 + _RLIMIT_MAX - 1), - _SETTINGS_MASK_ALL = (UINT64_C(1) << (21 + _RLIMIT_MAX)) - 1, + SETTING_RESOLV_CONF = UINT64_C(1) << 21, + SETTING_RLIMIT_FIRST = UINT64_C(1) << 22, /* we define one bit per resource limit here */ + SETTING_RLIMIT_LAST = UINT64_C(1) << (22 + _RLIMIT_MAX - 1), + _SETTINGS_MASK_ALL = (UINT64_C(1) << (22 + _RLIMIT_MAX)) - 1, _FORCE_ENUM_WIDTH = UINT64_MAX } SettingsMask; @@ -96,6 +109,7 @@ typedef struct Settings { bool oom_score_adjust_set; cpu_set_t *cpuset; unsigned cpuset_ncpus; + ResolvConfMode resolv_conf; /* [Image] */ int read_only; @@ -143,3 +157,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_syscall_filter); CONFIG_PARSER_PROTOTYPE(config_parse_hostname); CONFIG_PARSER_PROTOTYPE(config_parse_oom_score_adjust); CONFIG_PARSER_PROTOTYPE(config_parse_cpu_affinity); +CONFIG_PARSER_PROTOTYPE(config_parse_resolv_conf); + +const char *resolv_conf_mode_to_string(ResolvConfMode a) _const_; +ResolvConfMode resolv_conf_mode_from_string(const char *s) _pure_; diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 9cfbb1171e..3dbca99ef8 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -211,6 +211,7 @@ static int arg_oom_score_adjust = 0; static bool arg_oom_score_adjust_set = false; static cpu_set_t *arg_cpuset = NULL; static unsigned arg_cpuset_ncpus = 0; +static ResolvConfMode arg_resolv_conf = RESOLV_CONF_AUTO; static void help(void) { @@ -287,6 +288,7 @@ static void help(void) { " --link-journal=MODE Link up guest journal, one of no, auto, guest, \n" " host, try-guest, try-host\n" " -j Equivalent to --link-journal=try-guest\n" + " --resolv-conf=MODE Select mode of /etc/resolv.conf initialization\n" " --read-only Mount the root directory read-only\n" " --bind=PATH[:PATH[:OPTIONS]]\n" " Bind mount a file or directory from the host into\n" @@ -463,6 +465,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_NO_NEW_PRIVILEGES, ARG_OOM_SCORE_ADJUST, ARG_CPU_AFFINITY, + ARG_RESOLV_CONF, }; static const struct option options[] = { @@ -521,6 +524,7 @@ static int parse_argv(int argc, char *argv[]) { { "rlimit", required_argument, NULL, ARG_RLIMIT }, { "oom-score-adjust", required_argument, NULL, ARG_OOM_SCORE_ADJUST }, { "cpu-affinity", required_argument, NULL, ARG_CPU_AFFINITY }, + { "resolv-conf", required_argument, NULL, ARG_RESOLV_CONF }, {} }; @@ -1222,6 +1226,21 @@ static int parse_argv(int argc, char *argv[]) { break; } + case ARG_RESOLV_CONF: + if (streq(optarg, "help")) { + DUMP_STRING_TABLE(resolv_conf_mode, ResolvConfMode, _RESOLV_CONF_MODE_MAX); + return 0; + } + + arg_resolv_conf = resolv_conf_mode_from_string(optarg); + if (arg_resolv_conf < 0) { + log_error("Failed to parse /etc/resolv.conf mode: %s", optarg); + return -EINVAL; + } + + arg_settings_mask |= SETTING_RESOLV_CONF; + break; + case '?': return -EINVAL; @@ -1507,6 +1526,19 @@ static int setup_timezone(const char *dest) { return 0; } +static int have_resolv_conf(const char *path) { + assert(path); + + if (access(path, F_OK) < 0) { + if (errno == ENOENT) + return 0; + + return log_debug_errno(errno, "Failed to determine whether '%s' is available: %m", path); + } + + return 1; +} + static int resolved_listening(void) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; _cleanup_free_ char *dns_stub_listener_mode = NULL; @@ -1536,13 +1568,31 @@ static int resolved_listening(void) { } static int setup_resolv_conf(const char *dest) { - _cleanup_free_ char *resolved = NULL, *etc = NULL; - const char *where; - int r, found; + _cleanup_free_ char *etc = NULL; + const char *where, *what; + ResolvConfMode m; + int r; assert(dest); - if (arg_private_network) + if (arg_resolv_conf == RESOLV_CONF_AUTO) { + if (arg_private_network) + m = RESOLV_CONF_OFF; + else if (have_resolv_conf(STATIC_RESOLV_CONF) > 0 && resolved_listening() > 0) + /* resolved is enabled on the host. In this, case bind mount its static resolv.conf file into the + * container, so that the container can use the host's resolver. Given that network namespacing is + * disabled it's only natural of the container also uses the host's resolver. It also has the big + * advantage that the container will be able to follow the host's DNS server configuration changes + * transparently. */ + m = RESOLV_CONF_BIND_STATIC; + else if (have_resolv_conf("/etc/resolv.conf") > 0) + m = arg_read_only && arg_volatile_mode != VOLATILE_YES ? RESOLV_CONF_BIND_HOST : RESOLV_CONF_COPY_HOST; + else + m = arg_read_only && arg_volatile_mode != VOLATILE_YES ? RESOLV_CONF_OFF : RESOLV_CONF_DELETE; + } else + m = arg_resolv_conf; + + if (m == RESOLV_CONF_OFF) return 0; r = chase_symlinks("/etc", dest, CHASE_PREFIX_ROOT, &etc); @@ -1552,38 +1602,46 @@ static int setup_resolv_conf(const char *dest) { } where = strjoina(etc, "/resolv.conf"); - found = chase_symlinks(where, dest, CHASE_NONEXISTENT, &resolved); - if (found < 0) { - log_warning_errno(found, "Failed to resolve /etc/resolv.conf path in container, ignoring: %m"); + + if (m == RESOLV_CONF_DELETE) { + if (unlink(where) < 0) + log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno, "Failed to remove '%s', ignoring: %m", where); + return 0; } - if (access(STATIC_RESOLV_CONF, F_OK) >= 0 && - resolved_listening() > 0) { + if (IN_SET(m, RESOLV_CONF_BIND_STATIC, RESOLV_CONF_COPY_STATIC)) + what = STATIC_RESOLV_CONF; + else + what = "/etc/resolv.conf"; - /* resolved is enabled on the host. In this, case bind mount its static resolv.conf file into the - * container, so that the container can use the host's resolver. Given that network namespacing is - * disabled it's only natural of the container also uses the host's resolver. It also has the big - * advantage that the container will be able to follow the host's DNS server configuration changes - * transparently. */ + if (IN_SET(m, RESOLV_CONF_BIND_HOST, RESOLV_CONF_BIND_STATIC)) { + _cleanup_free_ char *resolved = NULL; + int found; + + found = chase_symlinks(where, dest, CHASE_NONEXISTENT, &resolved); + if (found < 0) { + log_warning_errno(found, "Failed to resolve /etc/resolv.conf path in container, ignoring: %m"); + return 0; + } if (found == 0) /* missing? */ (void) touch(resolved); - r = mount_verbose(LOG_DEBUG, STATIC_RESOLV_CONF, resolved, NULL, MS_BIND, NULL); + r = mount_verbose(LOG_WARNING, what, resolved, NULL, MS_BIND, NULL); if (r >= 0) return mount_verbose(LOG_ERR, NULL, resolved, NULL, MS_BIND|MS_REMOUNT|MS_RDONLY|MS_NOSUID|MS_NODEV, NULL); } /* If that didn't work, let's copy the file */ - r = copy_file("/etc/resolv.conf", where, O_TRUNC|O_NOFOLLOW, 0644, 0, COPY_REFLINK); + r = copy_file(what, where, O_TRUNC|O_NOFOLLOW, 0644, 0, COPY_REFLINK); if (r < 0) { /* If the file already exists as symlink, let's suppress the warning, under the assumption that * resolved or something similar runs inside and the symlink points there. * * If the disk image is read-only, there's also no point in complaining. */ - log_full_errno(IN_SET(r, -ELOOP, -EROFS, -EACCES, -EPERM) ? LOG_DEBUG : LOG_WARNING, r, + log_full_errno(!IN_SET(RESOLV_CONF_COPY_HOST, RESOLV_CONF_COPY_STATIC) && IN_SET(r, -ELOOP, -EROFS, -EACCES, -EPERM) ? LOG_DEBUG : LOG_WARNING, r, "Failed to copy /etc/resolv.conf to %s, ignoring: %m", where); return 0; } @@ -3385,6 +3443,10 @@ static int merge_settings(Settings *settings, const char *path) { } } + if ((arg_settings_mask & SETTING_RESOLV_CONF) == 0 && + settings->resolv_conf != _RESOLV_CONF_MODE_INVALID) + arg_resolv_conf = settings->resolv_conf; + return 0; } -- cgit v1.2.3 From b8ea7a6e1232128da3ab99882658e0909e4ed8d2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 12 May 2018 12:51:20 -0700 Subject: nspawn: add a bit of debug logging to resolved_listening() --- src/nspawn/nspawn.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 3dbca99ef8..0ed90edb53 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -38,6 +38,7 @@ #include "base-filesystem.h" #include "blkid-util.h" #include "btrfs-util.h" +#include "bus-error.h" #include "bus-util.h" #include "cap-list.h" #include "capability-util.h" @@ -1540,6 +1541,7 @@ static int have_resolv_conf(const char *path) { } static int resolved_listening(void) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; _cleanup_free_ char *dns_stub_listener_mode = NULL; int r; @@ -1548,21 +1550,23 @@ static int resolved_listening(void) { r = sd_bus_open_system(&bus); if (r < 0) - return r; + return log_debug_errno(r, "Failed to open system bus: %m"); r = bus_name_has_owner(bus, "org.freedesktop.resolve1", NULL); - if (r <= 0) - return r; + if (r < 0) + return log_debug_errno(r, "Failed to check whether the 'org.freedesktop.resolve1' bus name is taken: %m"); + if (r == 0) + return 0; r = sd_bus_get_property_string(bus, "org.freedesktop.resolve1", "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager", "DNSStubListener", - NULL, + &error, &dns_stub_listener_mode); if (r < 0) - return r; + return log_debug_errno(r, "Failed to query DNSStubListener property: %s", bus_error_message(&error, r)); return STR_IN_SET(dns_stub_listener_mode, "udp", "yes"); } -- cgit v1.2.3 From 4e1d6aa983b33ab9bc5a25d011452976d636f726 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 12 May 2018 13:17:16 -0700 Subject: nspawn: make --link-journal= configurable through .nspawn files, too --- man/systemd.nspawn.xml | 9 +++++++ src/nspawn/nspawn-gperf.gperf | 1 + src/nspawn/nspawn-settings.c | 57 +++++++++++++++++++++++++++++++++++++++++++ src/nspawn/nspawn-settings.h | 21 +++++++++++++--- src/nspawn/nspawn.c | 43 ++++++++++++-------------------- 5 files changed, 101 insertions(+), 30 deletions(-) diff --git a/man/systemd.nspawn.xml b/man/systemd.nspawn.xml index 679052ae78..3484d5cac6 100644 --- a/man/systemd.nspawn.xml +++ b/man/systemd.nspawn.xml @@ -349,6 +349,15 @@ details. + + LinkJournal= + + Configures how to link host and container journal setups. This is equivalent to the + command line switch, and takes the same parameter. See + systemd-nspawn1 for + details. + + diff --git a/src/nspawn/nspawn-gperf.gperf b/src/nspawn/nspawn-gperf.gperf index 0f31aa2ec4..485ae201b8 100644 --- a/src/nspawn/nspawn-gperf.gperf +++ b/src/nspawn/nspawn-gperf.gperf @@ -54,6 +54,7 @@ Exec.NoNewPrivileges, config_parse_tristate, 0, of Exec.OOMScoreAdjust, config_parse_oom_score_adjust, 0, 0 Exec.CPUAffinity, config_parse_cpu_affinity, 0, 0 Exec.ResolvConf, config_parse_resolv_conf, 0, offsetof(Settings, resolv_conf) +Exec.LinkJournal, config_parse_link_journal, 0, 0 Files.ReadOnly, config_parse_tristate, 0, offsetof(Settings, read_only) Files.Volatile, config_parse_volatile_mode, 0, offsetof(Settings, volatile_mode) Files.Bind, config_parse_bind, 0, 0 diff --git a/src/nspawn/nspawn-settings.c b/src/nspawn/nspawn-settings.c index 367f18c420..e63a14cbac 100644 --- a/src/nspawn/nspawn-settings.c +++ b/src/nspawn/nspawn-settings.c @@ -37,6 +37,7 @@ int settings_load(FILE *f, const char *path, Settings **ret) { s->personality = PERSONALITY_INVALID; s->userns_mode = _USER_NAMESPACE_MODE_INVALID; s->resolv_conf = _RESOLV_CONF_MODE_INVALID; + s->link_journal = _LINK_JOURNAL_INVALID; s->uid_shift = UID_INVALID; s->uid_range = UID_INVALID; s->no_new_privileges = -1; @@ -740,3 +741,59 @@ static const char *const resolv_conf_mode_table[_RESOLV_CONF_MODE_MAX] = { }; DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(resolv_conf_mode, ResolvConfMode, RESOLV_CONF_AUTO); + +int parse_link_journal(const char *s, LinkJournal *ret_mode, bool *ret_try) { + assert(s); + assert(ret_mode); + assert(ret_try); + + if (streq(s, "auto")) { + *ret_mode = LINK_AUTO; + *ret_try = false; + } else if (streq(s, "no")) { + *ret_mode = LINK_NO; + *ret_try = false; + } else if (streq(s, "guest")) { + *ret_mode = LINK_GUEST; + *ret_try = false; + } else if (streq(s, "host")) { + *ret_mode = LINK_HOST; + *ret_try = false; + } else if (streq(s, "try-guest")) { + *ret_mode = LINK_GUEST; + *ret_try = true; + } else if (streq(s, "try-host")) { + *ret_mode = LINK_HOST; + *ret_try = true; + } else + return -EINVAL; + + return 0; +} + +int config_parse_link_journal( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Settings *settings = data; + int r; + + assert(rvalue); + assert(settings); + + r = parse_link_journal(rvalue, &settings->link_journal, &settings->link_journal_try); + if (r < 0) { + log_syntax(unit, LOG_ERR, filename, line, r, "Failed to parse link journal mode, ignoring: %s", rvalue); + return 0; + } + + return 0; +} diff --git a/src/nspawn/nspawn-settings.h b/src/nspawn/nspawn-settings.h index 8b4b897fa6..69fce584a9 100644 --- a/src/nspawn/nspawn-settings.h +++ b/src/nspawn/nspawn-settings.h @@ -45,6 +45,15 @@ typedef enum ResolvConfMode { _RESOLV_CONF_MODE_INVALID = -1 } ResolvConfMode; +typedef enum LinkJournal { + LINK_NO, + LINK_AUTO, + LINK_HOST, + LINK_GUEST, + _LINK_JOURNAL_MAX, + _LINK_JOURNAL_INVALID = -1 +} LinkJournal; + typedef enum SettingsMask { SETTING_START_MODE = UINT64_C(1) << 0, SETTING_ENVIRONMENT = UINT64_C(1) << 1, @@ -68,9 +77,10 @@ typedef enum SettingsMask { SETTING_OOM_SCORE_ADJUST = UINT64_C(1) << 19, SETTING_CPU_AFFINITY = UINT64_C(1) << 20, SETTING_RESOLV_CONF = UINT64_C(1) << 21, - SETTING_RLIMIT_FIRST = UINT64_C(1) << 22, /* we define one bit per resource limit here */ - SETTING_RLIMIT_LAST = UINT64_C(1) << (22 + _RLIMIT_MAX - 1), - _SETTINGS_MASK_ALL = (UINT64_C(1) << (22 + _RLIMIT_MAX)) - 1, + SETTING_LINK_JOURNAL = UINT64_C(1) << 22, + SETTING_RLIMIT_FIRST = UINT64_C(1) << 23, /* we define one bit per resource limit here */ + SETTING_RLIMIT_LAST = UINT64_C(1) << (23 + _RLIMIT_MAX - 1), + _SETTINGS_MASK_ALL = (UINT64_C(1) << (23 + _RLIMIT_MAX)) - 1, _FORCE_ENUM_WIDTH = UINT64_MAX } SettingsMask; @@ -110,6 +120,8 @@ typedef struct Settings { cpu_set_t *cpuset; unsigned cpuset_ncpus; ResolvConfMode resolv_conf; + LinkJournal link_journal; + bool link_journal_try; /* [Image] */ int read_only; @@ -158,6 +170,9 @@ CONFIG_PARSER_PROTOTYPE(config_parse_hostname); CONFIG_PARSER_PROTOTYPE(config_parse_oom_score_adjust); CONFIG_PARSER_PROTOTYPE(config_parse_cpu_affinity); CONFIG_PARSER_PROTOTYPE(config_parse_resolv_conf); +CONFIG_PARSER_PROTOTYPE(config_parse_link_journal); const char *resolv_conf_mode_to_string(ResolvConfMode a) _const_; ResolvConfMode resolv_conf_mode_from_string(const char *s) _pure_; + +int parse_link_journal(const char *s, LinkJournal *ret_mode, bool *ret_try); diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 0ed90edb53..15d43774a4 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -118,13 +118,6 @@ typedef enum ContainerStatus { CONTAINER_REBOOTED } ContainerStatus; -typedef enum LinkJournal { - LINK_NO, - LINK_AUTO, - LINK_HOST, - LINK_GUEST -} LinkJournal; - static char *arg_directory = NULL; static char *arg_template = NULL; static char *arg_chdir = NULL; @@ -810,32 +803,17 @@ static int parse_argv(int argc, char *argv[]) { case 'j': arg_link_journal = LINK_GUEST; arg_link_journal_try = true; + arg_settings_mask |= SETTING_LINK_JOURNAL; break; case ARG_LINK_JOURNAL: - if (streq(optarg, "auto")) { - arg_link_journal = LINK_AUTO; - arg_link_journal_try = false; - } else if (streq(optarg, "no")) { - arg_link_journal = LINK_NO; - arg_link_journal_try = false; - } else if (streq(optarg, "guest")) { - arg_link_journal = LINK_GUEST; - arg_link_journal_try = false; - } else if (streq(optarg, "host")) { - arg_link_journal = LINK_HOST; - arg_link_journal_try = false; - } else if (streq(optarg, "try-guest")) { - arg_link_journal = LINK_GUEST; - arg_link_journal_try = true; - } else if (streq(optarg, "try-host")) { - arg_link_journal = LINK_HOST; - arg_link_journal_try = true; - } else { - log_error("Failed to parse link journal mode %s", optarg); + r = parse_link_journal(optarg, &arg_link_journal, &arg_link_journal_try); + if (r < 0) { + log_error_errno(r, "Failed to parse link journal mode %s", optarg); return -EINVAL; } + arg_settings_mask |= SETTING_LINK_JOURNAL; break; case ARG_BIND: @@ -3451,6 +3429,17 @@ static int merge_settings(Settings *settings, const char *path) { settings->resolv_conf != _RESOLV_CONF_MODE_INVALID) arg_resolv_conf = settings->resolv_conf; + if ((arg_settings_mask & SETTING_LINK_JOURNAL) == 0 && + settings->link_journal != _LINK_JOURNAL_INVALID) { + + if (!arg_settings_trusted) + log_warning("Ignoring journal link setting, file '%s' is not trusted.", path); + else { + arg_link_journal = settings->link_journal; + arg_link_journal_try = settings->link_journal_try; + } + } + return 0; } -- cgit v1.2.3 From 63d1c29ffabd737ec6eecc3a5552d279044cd0a1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 12 May 2018 13:17:38 -0700 Subject: nspawn: complain if people still use --share-system --- src/nspawn/nspawn.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 15d43774a4..8a234b8f88 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -868,6 +868,7 @@ static int parse_argv(int argc, char *argv[]) { case ARG_SHARE_SYSTEM: /* We don't officially support this anymore, except for compat reasons. People should use the * $SYSTEMD_NSPAWN_SHARE_* environment variables instead. */ + log_warning("Please do not use --share-system anymore, use $SYSTEMD_NSPAWN_SHARE_* instead."); arg_clone_ns_flags = 0; break; -- cgit v1.2.3 From 1688841f4628f4e55ad31e769da3a712474bf995 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 16 May 2018 23:43:03 -0400 Subject: nspawn: similar to the previous patches, also make /etc/localtime handling more configurable Fixes: #9009 --- man/systemd-nspawn.xml | 19 +++++ man/systemd.nspawn.xml | 9 ++ src/nspawn/nspawn-gperf.gperf | 1 + src/nspawn/nspawn-settings.c | 14 ++++ src/nspawn/nspawn-settings.h | 23 +++++- src/nspawn/nspawn.c | 186 ++++++++++++++++++++++++++++++++---------- 6 files changed, 205 insertions(+), 47 deletions(-) diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index 03e79683bc..1c8c6c8e60 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -887,6 +887,25 @@ auto. + + + + Configures how /etc/localtime inside of the container (i.e. local timezone + synchronization from host to container) shall be handled. Takes one of off, + copy, bind, symlink, delete or + auto. If set to off the /etc/localtime file in the + container is left as it is included in the image, and neither modified nor bind mounted over. If set to + copy the /etc/localtime file of the host is copied into the + container. Similar, if bind is used, it is bind mounted from the host into the container. If + set to symlink a symlink from /etc/localtime in the container is + created pointing to the matching the timezone file of the container that matches the timezone setting on the + host. If set to delete the file in the container is deleted, should it exist. If set to + auto and the /etc/localtime file of the host is a symlink, then + symlink mode is used, and copy otherwise, except if the image is + read-only in which case bind is used instead. Defaults to + auto. + + diff --git a/man/systemd.nspawn.xml b/man/systemd.nspawn.xml index 3484d5cac6..275f96ca13 100644 --- a/man/systemd.nspawn.xml +++ b/man/systemd.nspawn.xml @@ -349,6 +349,15 @@ details. + + Timezone= + + Configures how /etc/localtime in the container shall be handled. This is + equivalent to the command line switch, and takes the same argument. See + systemd-nspawn1 for + details. + + LinkJournal= diff --git a/src/nspawn/nspawn-gperf.gperf b/src/nspawn/nspawn-gperf.gperf index 485ae201b8..6029686ee9 100644 --- a/src/nspawn/nspawn-gperf.gperf +++ b/src/nspawn/nspawn-gperf.gperf @@ -55,6 +55,7 @@ Exec.OOMScoreAdjust, config_parse_oom_score_adjust, 0, 0 Exec.CPUAffinity, config_parse_cpu_affinity, 0, 0 Exec.ResolvConf, config_parse_resolv_conf, 0, offsetof(Settings, resolv_conf) Exec.LinkJournal, config_parse_link_journal, 0, 0 +Exec.Timezone, config_parse_timezone, 0, offsetof(Settings, timezone) Files.ReadOnly, config_parse_tristate, 0, offsetof(Settings, read_only) Files.Volatile, config_parse_volatile_mode, 0, offsetof(Settings, volatile_mode) Files.Bind, config_parse_bind, 0, 0 diff --git a/src/nspawn/nspawn-settings.c b/src/nspawn/nspawn-settings.c index e63a14cbac..126335da58 100644 --- a/src/nspawn/nspawn-settings.c +++ b/src/nspawn/nspawn-settings.c @@ -38,6 +38,7 @@ int settings_load(FILE *f, const char *path, Settings **ret) { s->userns_mode = _USER_NAMESPACE_MODE_INVALID; s->resolv_conf = _RESOLV_CONF_MODE_INVALID; s->link_journal = _LINK_JOURNAL_INVALID; + s->timezone = _TIMEZONE_MODE_INVALID; s->uid_shift = UID_INVALID; s->uid_range = UID_INVALID; s->no_new_privileges = -1; @@ -797,3 +798,16 @@ int config_parse_link_journal( return 0; } + +DEFINE_CONFIG_PARSE_ENUM(config_parse_timezone, timezone_mode, TimezoneMode, "Failed to parse timezone mode"); + +static const char *const timezone_mode_table[_TIMEZONE_MODE_MAX] = { + [TIMEZONE_OFF] = "off", + [TIMEZONE_COPY] = "copy", + [TIMEZONE_BIND] = "bind", + [TIMEZONE_SYMLINK] = "symlink", + [TIMEZONE_DELETE] = "delete", + [TIMEZONE_AUTO] = "auto", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(timezone_mode, TimezoneMode, TIMEZONE_AUTO); diff --git a/src/nspawn/nspawn-settings.h b/src/nspawn/nspawn-settings.h index 69fce584a9..9be7679027 100644 --- a/src/nspawn/nspawn-settings.h +++ b/src/nspawn/nspawn-settings.h @@ -54,6 +54,17 @@ typedef enum LinkJournal { _LINK_JOURNAL_INVALID = -1 } LinkJournal; +typedef enum TimezoneMode { + TIMEZONE_OFF, + TIMEZONE_COPY, + TIMEZONE_BIND, + TIMEZONE_SYMLINK, + TIMEZONE_DELETE, + TIMEZONE_AUTO, + _TIMEZONE_MODE_MAX, + _TIMEZONE_MODE_INVALID = -1 +} TimezoneMode; + typedef enum SettingsMask { SETTING_START_MODE = UINT64_C(1) << 0, SETTING_ENVIRONMENT = UINT64_C(1) << 1, @@ -78,9 +89,10 @@ typedef enum SettingsMask { SETTING_CPU_AFFINITY = UINT64_C(1) << 20, SETTING_RESOLV_CONF = UINT64_C(1) << 21, SETTING_LINK_JOURNAL = UINT64_C(1) << 22, - SETTING_RLIMIT_FIRST = UINT64_C(1) << 23, /* we define one bit per resource limit here */ - SETTING_RLIMIT_LAST = UINT64_C(1) << (23 + _RLIMIT_MAX - 1), - _SETTINGS_MASK_ALL = (UINT64_C(1) << (23 + _RLIMIT_MAX)) - 1, + SETTING_TIMEZONE = UINT64_C(1) << 23, + SETTING_RLIMIT_FIRST = UINT64_C(1) << 24, /* we define one bit per resource limit here */ + SETTING_RLIMIT_LAST = UINT64_C(1) << (24 + _RLIMIT_MAX - 1), + _SETTINGS_MASK_ALL = (UINT64_C(1) << (24 + _RLIMIT_MAX)) -1, _FORCE_ENUM_WIDTH = UINT64_MAX } SettingsMask; @@ -122,6 +134,7 @@ typedef struct Settings { ResolvConfMode resolv_conf; LinkJournal link_journal; bool link_journal_try; + TimezoneMode timezone; /* [Image] */ int read_only; @@ -171,8 +184,12 @@ CONFIG_PARSER_PROTOTYPE(config_parse_oom_score_adjust); CONFIG_PARSER_PROTOTYPE(config_parse_cpu_affinity); CONFIG_PARSER_PROTOTYPE(config_parse_resolv_conf); CONFIG_PARSER_PROTOTYPE(config_parse_link_journal); +CONFIG_PARSER_PROTOTYPE(config_parse_timezone); const char *resolv_conf_mode_to_string(ResolvConfMode a) _const_; ResolvConfMode resolv_conf_mode_from_string(const char *s) _pure_; +const char *timezone_mode_to_string(TimezoneMode a) _const_; +TimezoneMode timezone_mode_from_string(const char *s) _pure_; + int parse_link_journal(const char *s, LinkJournal *ret_mode, bool *ret_try); diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 8a234b8f88..5a0a389175 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -206,6 +206,7 @@ static bool arg_oom_score_adjust_set = false; static cpu_set_t *arg_cpuset = NULL; static unsigned arg_cpuset_ncpus = 0; static ResolvConfMode arg_resolv_conf = RESOLV_CONF_AUTO; +static TimezoneMode arg_timezone = TIMEZONE_AUTO; static void help(void) { @@ -283,6 +284,7 @@ static void help(void) { " host, try-guest, try-host\n" " -j Equivalent to --link-journal=try-guest\n" " --resolv-conf=MODE Select mode of /etc/resolv.conf initialization\n" + " --timezone=MODE Select mode of /etc/localtime initialization\n" " --read-only Mount the root directory read-only\n" " --bind=PATH[:PATH[:OPTIONS]]\n" " Bind mount a file or directory from the host into\n" @@ -460,6 +462,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_OOM_SCORE_ADJUST, ARG_CPU_AFFINITY, ARG_RESOLV_CONF, + ARG_TIMEZONE, }; static const struct option options[] = { @@ -519,6 +522,7 @@ static int parse_argv(int argc, char *argv[]) { { "oom-score-adjust", required_argument, NULL, ARG_OOM_SCORE_ADJUST }, { "cpu-affinity", required_argument, NULL, ARG_CPU_AFFINITY }, { "resolv-conf", required_argument, NULL, ARG_RESOLV_CONF }, + { "timezone", required_argument, NULL, ARG_TIMEZONE }, {} }; @@ -1221,6 +1225,21 @@ static int parse_argv(int argc, char *argv[]) { arg_settings_mask |= SETTING_RESOLV_CONF; break; + case ARG_TIMEZONE: + if (streq(optarg, "help")) { + DUMP_STRING_TABLE(timezone_mode, TimezoneMode, _TIMEZONE_MODE_MAX); + return 0; + } + + arg_timezone = timezone_mode_from_string(optarg); + if (arg_timezone < 0) { + log_error("Failed to parse /etc/localtime mode: %s", optarg); + return -EINVAL; + } + + arg_settings_mask |= SETTING_TIMEZONE; + break; + case '?': return -EINVAL; @@ -1436,72 +1455,147 @@ static int userns_mkdir(const char *root, const char *path, mode_t mode, uid_t u return userns_lchown(q, uid, gid); } +static const char *timezone_from_path(const char *path) { + const char *z; + + z = path_startswith(path, "../usr/share/zoneinfo/"); + if (z) + return z; + + z = path_startswith(path, "/usr/share/zoneinfo/"); + if (z) + return z; + + return NULL; +} + static int setup_timezone(const char *dest) { - _cleanup_free_ char *p = NULL, *q = NULL; - const char *where, *check, *what; - char *z, *y; + _cleanup_free_ char *p = NULL, *etc = NULL; + const char *where, *check; + TimezoneMode m; int r; assert(dest); - /* Fix the timezone, if possible */ - r = readlink_malloc("/etc/localtime", &p); + if (IN_SET(arg_timezone, TIMEZONE_AUTO, TIMEZONE_SYMLINK)) { + + r = readlink_malloc("/etc/localtime", &p); + if (r == -ENOENT && arg_timezone == TIMEZONE_AUTO) + m = arg_read_only && arg_volatile_mode != VOLATILE_YES ? TIMEZONE_OFF : TIMEZONE_DELETE; + else if (r == -EINVAL && arg_timezone == TIMEZONE_AUTO) /* regular file? */ + m = arg_read_only && arg_volatile_mode != VOLATILE_YES ? TIMEZONE_BIND : TIMEZONE_COPY; + else if (r < 0) { + log_warning_errno(r, "Failed to read host's /etc/localtime symlink, not updating container timezone: %m"); + /* To handle warning, delete /etc/localtime and replace it with a symbolic link to a time zone data + * file. + * + * Example: + * ln -s /usr/share/zoneinfo/UTC /etc/localtime + */ + return 0; + } else if (arg_timezone == TIMEZONE_AUTO) + m = arg_read_only && arg_volatile_mode != VOLATILE_YES ? TIMEZONE_BIND : TIMEZONE_SYMLINK; + else + m = arg_timezone; + } else + m = arg_timezone; + + if (m == TIMEZONE_OFF) + return 0; + + r = chase_symlinks("/etc", dest, CHASE_PREFIX_ROOT, &etc); if (r < 0) { - log_warning("host's /etc/localtime is not a symlink, not updating container timezone."); - /* to handle warning, delete /etc/localtime and replace it - * with a symbolic link to a time zone data file. - * - * Example: - * ln -s /usr/share/zoneinfo/UTC /etc/localtime - */ + log_warning_errno(r, "Failed to resolve /etc path in container, ignoring: %m"); return 0; } - z = path_startswith(p, "../usr/share/zoneinfo/"); - if (!z) - z = path_startswith(p, "/usr/share/zoneinfo/"); - if (!z) { - log_warning("/etc/localtime does not point into /usr/share/zoneinfo/, not updating container timezone."); + where = strjoina(etc, "/localtime"); + + switch (m) { + + case TIMEZONE_DELETE: + if (unlink(where) < 0) + log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno, "Failed to remove '%s', ignoring: %m", where); + return 0; - } - where = prefix_roota(dest, "/etc/localtime"); - r = readlink_malloc(where, &q); - if (r >= 0) { - y = path_startswith(q, "../usr/share/zoneinfo/"); - if (!y) - y = path_startswith(q, "/usr/share/zoneinfo/"); + case TIMEZONE_SYMLINK: { + _cleanup_free_ char *q = NULL; + const char *z, *what; - /* Already pointing to the right place? Then do nothing .. */ - if (y && streq(y, z)) + z = timezone_from_path(p); + if (!z) { + log_warning("/etc/localtime does not point into /usr/share/zoneinfo/, not updating container timezone."); return 0; - } + } - check = strjoina("/usr/share/zoneinfo/", z); - check = prefix_roota(dest, check); - if (laccess(check, F_OK) < 0) { - log_warning("Timezone %s does not exist in container, not updating container timezone.", z); - return 0; + r = readlink_malloc(where, &q); + if (r >= 0 && streq_ptr(timezone_from_path(q), z)) + return 0; /* Already pointing to the right place? Then do nothing .. */ + + check = strjoina(dest, "/usr/share/zoneinfo/", z); + r = chase_symlinks(check, dest, 0, NULL); + if (r < 0) + log_debug_errno(r, "Timezone %s does not exist (or is not accessible) in container, not creating symlink: %m", z); + else { + if (unlink(where) < 0 && errno != ENOENT) { + log_full_errno(IN_SET(errno, EROFS, EACCES, EPERM) ? LOG_DEBUG : LOG_WARNING, /* Don't complain on read-only images */ + errno, "Failed to remove existing timezone info %s in container, ignoring: %m", where); + return 0; + } + + what = strjoina("../usr/share/zoneinfo/", z); + if (symlink(what, where) < 0) { + log_full_errno(IN_SET(errno, EROFS, EACCES, EPERM) ? LOG_DEBUG : LOG_WARNING, + errno, "Failed to correct timezone of container, ignoring: %m"); + return 0; + } + + break; + } + + _fallthrough_; } - if (unlink(where) < 0 && errno != ENOENT) { - log_full_errno(IN_SET(errno, EROFS, EACCES, EPERM) ? LOG_DEBUG : LOG_WARNING, /* Don't complain on read-only images */ - errno, - "Failed to remove existing timezone info %s in container, ignoring: %m", where); - return 0; + case TIMEZONE_BIND: { + _cleanup_free_ char *resolved = NULL; + int found; + + found = chase_symlinks(where, dest, CHASE_NONEXISTENT, &resolved); + if (found < 0) { + log_warning_errno(found, "Failed to resolve /etc/localtime path in container, ignoring: %m"); + return 0; + } + + if (found == 0) /* missing? */ + (void) touch(resolved); + + r = mount_verbose(LOG_WARNING, "/etc/localtime", resolved, NULL, MS_BIND, NULL); + if (r >= 0) + return mount_verbose(LOG_ERR, NULL, resolved, NULL, MS_BIND|MS_REMOUNT|MS_RDONLY|MS_NOSUID|MS_NODEV, NULL); + + _fallthrough_; } - what = strjoina("../usr/share/zoneinfo/", z); - if (symlink(what, where) < 0) { - log_full_errno(IN_SET(errno, EROFS, EACCES, EPERM) ? LOG_DEBUG : LOG_WARNING, - errno, - "Failed to correct timezone of container, ignoring: %m"); - return 0; + case TIMEZONE_COPY: + /* If mounting failed, try to copy */ + r = copy_file_atomic("/etc/localtime", where, 0644, 0, COPY_REFLINK|COPY_REPLACE); + if (r < 0) { + log_full_errno(IN_SET(r, -EROFS, -EACCES, -EPERM) ? LOG_DEBUG : LOG_WARNING, r, + "Failed to copy /etc/localtime to %s, ignoring: %m", where); + return 0; + } + + break; + + default: + assert_not_reached("unexpected mode"); } + /* Fix permissions of the symlink or file copy we just created */ r = userns_lchown(where, 0, 0); if (r < 0) - return log_warning_errno(r, "Failed to chown /etc/localtime: %m"); + log_warning_errno(r, "Failed to chown /etc/localtime, ignoring: %m"); return 0; } @@ -3441,6 +3535,10 @@ static int merge_settings(Settings *settings, const char *path) { } } + if ((arg_settings_mask & SETTING_TIMEZONE) == 0 && + settings->timezone != _TIMEZONE_MODE_INVALID) + arg_timezone = settings->timezone; + return 0; } -- cgit v1.2.3 From f728ab1724bd2dde9e7c2e6841d7c826b2a9ec16 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 22 May 2018 14:39:50 +0200 Subject: =?UTF-8?q?nspawn:=20let's=20rename=20=5FFORCE=5FENUM=5FWIDTH=20?= =?UTF-8?q?=E2=86=92=20=5FSETTING=5FFORCE=5FENUM=5FWIDTH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Just some preparation in case we need a similar hack in another enum one day. --- src/nspawn/nspawn-settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nspawn/nspawn-settings.h b/src/nspawn/nspawn-settings.h index 9be7679027..28a0d8b8e1 100644 --- a/src/nspawn/nspawn-settings.h +++ b/src/nspawn/nspawn-settings.h @@ -93,7 +93,7 @@ typedef enum SettingsMask { SETTING_RLIMIT_FIRST = UINT64_C(1) << 24, /* we define one bit per resource limit here */ SETTING_RLIMIT_LAST = UINT64_C(1) << (24 + _RLIMIT_MAX - 1), _SETTINGS_MASK_ALL = (UINT64_C(1) << (24 + _RLIMIT_MAX)) -1, - _FORCE_ENUM_WIDTH = UINT64_MAX + _SETTING_FORCE_ENUM_WIDTH = UINT64_MAX } SettingsMask; /* We want to use SETTING_RLIMIT_FIRST in shifts, so make sure it is really 64 bits -- cgit v1.2.3 From 72d711efa3376f26727d099524651b70c31860b8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 7 May 2018 17:50:31 +0200 Subject: update TODO --- TODO | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TODO b/TODO index 5a0117260e..aec2f8c669 100644 --- a/TODO +++ b/TODO @@ -24,9 +24,9 @@ Janitorial Clean-ups: Features: -* nspawn: greater control over hostname, resolv.conf, timezone, rlim +* add O_TMPFILE support to copy_file_atomic() -* nspawn: when operating in a scope, also create /payload subcrgoup +* nspawn: greater control over selinux label? * the error paths in usbffs_dispatch_ep() leak memory @@ -561,7 +561,7 @@ Features: - document chaining of signal handler for SIGCHLD and child handlers - define more intervals where we will shift wakeup intervals around in, 1h, 6h, 24h, ... - generate a failure of a default event loop is executed out-of-thread - - maybe add support for inotify events + - maybe add support for inotify events (which we can do safely now, with O_PATH) * investigate endianness issues of UUID vs. GUID -- cgit v1.2.3