1
0
mirror of https://github.com/systemd/systemd.git synced 2025-01-11 09:18:07 +03:00

nspawn: add greater control over how /etc/resolv.conf is handled

Fixes: #8014 #1781
This commit is contained in:
Lennart Poettering 2018-05-12 12:50:57 -07:00
parent 8904ab86b0
commit 09d423e921
6 changed files with 155 additions and 20 deletions

View File

@ -858,6 +858,35 @@
<option>--link-journal=try-guest</option>.</para></listitem> <option>--link-journal=try-guest</option>.</para></listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><option>--resolv-conf=</option></term>
<listitem><para>Configures how <filename>/etc/resolv.conf</filename> inside of the container (i.e. DNS
configuration synchronization from host to container) shall be handled. Takes one of <literal>off</literal>,
<literal>copy-host</literal>, <literal>copy-static</literal>, <literal>bind-host</literal>,
<literal>bind-static</literal>, <literal>delete</literal> or <literal>auto</literal>. If set to
<literal>off</literal> the <filename>/etc/resolv.conf</filename> file in the container is left as it is
included in the image, and neither modified nor bind mounted over. If set to <literal>copy-host</literal>, the
<filename>/etc/resolv.conf</filename> file from the host is copied into the container. Similar, if
<literal>bind-host</literal> is used, the file is bind mounted from the host into the container. If set to
<literal>copy-static</literal> the static <filename>resolv.conf</filename> file supplied with
<citerefentry><refentrytitle>systemd-resolved.service</refentrytitle><manvolnum>8</manvolnum></citerefentry> is
copied into the container, and correspondingly <literal>bind-static</literal> bind mounts it there. If set to
<literal>delete</literal> the <filename>/etc/resolv.conf</filename> file in the container is deleted if it
exists. Finally, if set to <literal>auto</literal> the file is left as it is if private networking is turned on
(see <option>--private-network</option>). Otherwise, if <filename>systemd-resolved.service</filename> is
connectible its static <filename>resolv.conf</filename> file is used, and if not the host's
<filename>/etc/resolv.conf</filename> 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 <literal>copy</literal> if the container shall be
able to make changes to the DNS configuration on its own, deviating from the host's settings. Otherwise
<literal>bind</literal> is preferable, as it means direct changes to <filename>/etc/resolv.conf</filename> 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
<literal>auto</literal>.</para></listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term><option>--read-only</option></term> <term><option>--read-only</option></term>

View File

@ -340,6 +340,15 @@
details.</para></listitem> details.</para></listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><varname>ResolvConf=</varname></term>
<listitem><para>Configures how <filename>/etc/resolv.conf</filename> in the container shall be handled. This is
equivalent to the <option>--resolv-conf=</option> command line switch, and takes the same argument. See
<citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry> for
details.</para></listitem>
</varlistentry>
</variablelist> </variablelist>
</refsect1> </refsect1>

View File

@ -53,6 +53,7 @@ Exec.Hostname, config_parse_hostname, 0, of
Exec.NoNewPrivileges, config_parse_tristate, 0, offsetof(Settings, no_new_privileges) Exec.NoNewPrivileges, config_parse_tristate, 0, offsetof(Settings, no_new_privileges)
Exec.OOMScoreAdjust, config_parse_oom_score_adjust, 0, 0 Exec.OOMScoreAdjust, config_parse_oom_score_adjust, 0, 0
Exec.CPUAffinity, config_parse_cpu_affinity, 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.ReadOnly, config_parse_tristate, 0, offsetof(Settings, read_only)
Files.Volatile, config_parse_volatile_mode, 0, offsetof(Settings, volatile_mode) Files.Volatile, config_parse_volatile_mode, 0, offsetof(Settings, volatile_mode)
Files.Bind, config_parse_bind, 0, 0 Files.Bind, config_parse_bind, 0, 0

View File

@ -16,6 +16,7 @@
#include "process-util.h" #include "process-util.h"
#include "rlimit-util.h" #include "rlimit-util.h"
#include "socket-util.h" #include "socket-util.h"
#include "string-table.h"
#include "string-util.h" #include "string-util.h"
#include "strv.h" #include "strv.h"
#include "user-util.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->start_mode = _START_MODE_INVALID;
s->personality = PERSONALITY_INVALID; s->personality = PERSONALITY_INVALID;
s->userns_mode = _USER_NAMESPACE_MODE_INVALID; s->userns_mode = _USER_NAMESPACE_MODE_INVALID;
s->resolv_conf = _RESOLV_CONF_MODE_INVALID;
s->uid_shift = UID_INVALID; s->uid_shift = UID_INVALID;
s->uid_range = UID_INVALID; s->uid_range = UID_INVALID;
s->no_new_privileges = -1; s->no_new_privileges = -1;
@ -724,3 +726,17 @@ int config_parse_cpu_affinity(
return 0; 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);

View File

@ -33,6 +33,18 @@ typedef enum UserNamespaceMode {
_USER_NAMESPACE_MODE_INVALID = -1, _USER_NAMESPACE_MODE_INVALID = -1,
} UserNamespaceMode; } 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 { typedef enum SettingsMask {
SETTING_START_MODE = UINT64_C(1) << 0, SETTING_START_MODE = UINT64_C(1) << 0,
SETTING_ENVIRONMENT = UINT64_C(1) << 1, SETTING_ENVIRONMENT = UINT64_C(1) << 1,
@ -55,9 +67,10 @@ typedef enum SettingsMask {
SETTING_NO_NEW_PRIVILEGES = UINT64_C(1) << 18, SETTING_NO_NEW_PRIVILEGES = UINT64_C(1) << 18,
SETTING_OOM_SCORE_ADJUST = UINT64_C(1) << 19, SETTING_OOM_SCORE_ADJUST = UINT64_C(1) << 19,
SETTING_CPU_AFFINITY = UINT64_C(1) << 20, SETTING_CPU_AFFINITY = UINT64_C(1) << 20,
SETTING_RLIMIT_FIRST = UINT64_C(1) << 21, /* we define one bit per resource limit here */ SETTING_RESOLV_CONF = UINT64_C(1) << 21,
SETTING_RLIMIT_LAST = UINT64_C(1) << (21 + _RLIMIT_MAX - 1), SETTING_RLIMIT_FIRST = UINT64_C(1) << 22, /* we define one bit per resource limit here */
_SETTINGS_MASK_ALL = (UINT64_C(1) << (21 + _RLIMIT_MAX)) - 1, 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 _FORCE_ENUM_WIDTH = UINT64_MAX
} SettingsMask; } SettingsMask;
@ -96,6 +109,7 @@ typedef struct Settings {
bool oom_score_adjust_set; bool oom_score_adjust_set;
cpu_set_t *cpuset; cpu_set_t *cpuset;
unsigned cpuset_ncpus; unsigned cpuset_ncpus;
ResolvConfMode resolv_conf;
/* [Image] */ /* [Image] */
int read_only; 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_hostname);
CONFIG_PARSER_PROTOTYPE(config_parse_oom_score_adjust); CONFIG_PARSER_PROTOTYPE(config_parse_oom_score_adjust);
CONFIG_PARSER_PROTOTYPE(config_parse_cpu_affinity); 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_;

View File

@ -211,6 +211,7 @@ static int arg_oom_score_adjust = 0;
static bool arg_oom_score_adjust_set = false; static bool arg_oom_score_adjust_set = false;
static cpu_set_t *arg_cpuset = NULL; static cpu_set_t *arg_cpuset = NULL;
static unsigned arg_cpuset_ncpus = 0; static unsigned arg_cpuset_ncpus = 0;
static ResolvConfMode arg_resolv_conf = RESOLV_CONF_AUTO;
static void help(void) { 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" " --link-journal=MODE Link up guest journal, one of no, auto, guest, \n"
" host, try-guest, try-host\n" " host, try-guest, try-host\n"
" -j Equivalent to --link-journal=try-guest\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" " --read-only Mount the root directory read-only\n"
" --bind=PATH[:PATH[:OPTIONS]]\n" " --bind=PATH[:PATH[:OPTIONS]]\n"
" Bind mount a file or directory from the host into\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_NO_NEW_PRIVILEGES,
ARG_OOM_SCORE_ADJUST, ARG_OOM_SCORE_ADJUST,
ARG_CPU_AFFINITY, ARG_CPU_AFFINITY,
ARG_RESOLV_CONF,
}; };
static const struct option options[] = { static const struct option options[] = {
@ -521,6 +524,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "rlimit", required_argument, NULL, ARG_RLIMIT }, { "rlimit", required_argument, NULL, ARG_RLIMIT },
{ "oom-score-adjust", required_argument, NULL, ARG_OOM_SCORE_ADJUST }, { "oom-score-adjust", required_argument, NULL, ARG_OOM_SCORE_ADJUST },
{ "cpu-affinity", required_argument, NULL, ARG_CPU_AFFINITY }, { "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; 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 '?': case '?':
return -EINVAL; return -EINVAL;
@ -1507,6 +1526,19 @@ static int setup_timezone(const char *dest) {
return 0; 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) { static int resolved_listening(void) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_free_ char *dns_stub_listener_mode = 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) { static int setup_resolv_conf(const char *dest) {
_cleanup_free_ char *resolved = NULL, *etc = NULL; _cleanup_free_ char *etc = NULL;
const char *where; const char *where, *what;
int r, found; ResolvConfMode m;
int r;
assert(dest); 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; return 0;
r = chase_symlinks("/etc", dest, CHASE_PREFIX_ROOT, &etc); 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"); where = strjoina(etc, "/resolv.conf");
found = chase_symlinks(where, dest, CHASE_NONEXISTENT, &resolved);
if (found < 0) { if (m == RESOLV_CONF_DELETE) {
log_warning_errno(found, "Failed to resolve /etc/resolv.conf path in container, ignoring: %m"); if (unlink(where) < 0)
log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno, "Failed to remove '%s', ignoring: %m", where);
return 0; return 0;
} }
if (access(STATIC_RESOLV_CONF, F_OK) >= 0 && if (IN_SET(m, RESOLV_CONF_BIND_STATIC, RESOLV_CONF_COPY_STATIC))
resolved_listening() > 0) { 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 if (IN_SET(m, RESOLV_CONF_BIND_HOST, RESOLV_CONF_BIND_STATIC)) {
* container, so that the container can use the host's resolver. Given that network namespacing is _cleanup_free_ char *resolved = NULL;
* disabled it's only natural of the container also uses the host's resolver. It also has the big int found;
* advantage that the container will be able to follow the host's DNS server configuration changes
* transparently. */ 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? */ if (found == 0) /* missing? */
(void) touch(resolved); (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) if (r >= 0)
return mount_verbose(LOG_ERR, NULL, resolved, NULL, MS_BIND|MS_REMOUNT|MS_RDONLY|MS_NOSUID|MS_NODEV, NULL); 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 */ /* 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 (r < 0) {
/* If the file already exists as symlink, let's suppress the warning, under the assumption that /* 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. * 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. * 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); "Failed to copy /etc/resolv.conf to %s, ignoring: %m", where);
return 0; 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; return 0;
} }