From 09d423e9219883e5cb45adc249d07845fb6d4cb9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 12 May 2018 12:50:57 -0700 Subject: [PATCH] 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 9a0e02187f7..03e79683bcd 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 1780bfd79a2..679052ae78b 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 f8234e75d48..0f31aa2ec4f 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 0acf718456b..367f18c4200 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 8dc310d5693..8b4b897fa6d 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 9cfbb1171e6..3dbca99ef8b 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; } -- 2.39.2