diff --git a/TODO b/TODO
index 719bf29879..ad902f0736 100644
--- a/TODO
+++ b/TODO
@@ -253,9 +253,6 @@ Features:
processes with varlink communication over socketpair(). This all should be
useful as blueprint for graphical installers which should do the same.
-* make "systemd-vmspawn -n" work unprivileged properly, i.e. acquire tap netif
- from nsresourced.
-
* Make run0 forward various signals to the forked process so that sending
signals to a child process works roughly the same regardless of whether the
child process is spawned via run0 or not.
diff --git a/network/80-namespace-ns-tun.link b/network/80-namespace-ns-tun.link
new file mode 100644
index 0000000000..6eb37edb4b
--- /dev/null
+++ b/network/80-namespace-ns-tun.link
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: MIT-0
+#
+# This config file is installed as part of systemd.
+# It may be freely copied and edited (following the MIT No Attribution license).
+#
+# To make local modifications, use "networkctl edit". See networkctl(1) for details.
+# This file should not be edited in place, because it'll be overwritten on upgrades.
+
+# This link file matches the host-side of the Ethernet Tap device created by
+# systemd-nsresourced's network support. See systemd-nsresourced(1) for
+# details.
+
+[Match]
+Kind=tun
+OriginalName=ns-*
+
+[Link]
+NamePolicy=keep
+Property=ID_NET_MANAGED_BY=io.systemd.Network
diff --git a/network/80-namespace-ns-tun.network b/network/80-namespace-ns-tun.network
new file mode 100644
index 0000000000..fe084838a9
--- /dev/null
+++ b/network/80-namespace-ns-tun.network
@@ -0,0 +1,32 @@
+# SPDX-License-Identifier: MIT-0
+#
+# This config file is installed as part of systemd.
+# It may be freely copied and edited (following the MIT No Attribution license).
+#
+# To make local modifications, use "networkctl edit". See networkctl(1) for details.
+# This file should not be edited in place, because it'll be overwritten on upgrades.
+
+# This network file matches the host-side of the Ethernet Tap device created by
+# systemd-nsresourced's network support. See systemd-nsresourced(1) for
+# details.
+
+[Match]
+Kind=tun
+Name=ns-*
+
+[Link]
+RequiredForOnline=no
+
+[Network]
+# Default to using a /28 prefix, giving up to 13 addresses per namespace
+Address=0.0.0.0/28
+LinkLocalAddressing=yes
+DHCPServer=yes
+IPMasquerade=both
+LLDP=yes
+EmitLLDP=customer-bridge
+IPv6AcceptRA=no
+IPv6SendRA=yes
+
+[DHCPServer]
+PersistLeases=no
diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c
index 322cba4cf3..a026ae7d05 100644
--- a/src/basic/namespace-util.c
+++ b/src/basic/namespace-util.c
@@ -524,8 +524,8 @@ int userns_acquire_empty(void) {
return pidref_namespace_open_by_type(&pid, NAMESPACE_USER);
}
-int userns_acquire(const char *uid_map, const char *gid_map) {
- char path[STRLEN("/proc//uid_map") + DECIMAL_STR_MAX(pid_t) + 1];
+int userns_acquire(const char *uid_map, const char *gid_map, bool setgroups_deny) {
+ char path[STRLEN("/proc//setgroups") + DECIMAL_STR_MAX(pid_t) + 1];
_cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL;
int r;
@@ -546,6 +546,13 @@ int userns_acquire(const char *uid_map, const char *gid_map) {
if (r < 0)
return log_debug_errno(r, "Failed to write UID map: %m");
+ if (setgroups_deny) {
+ xsprintf(path, "/proc/" PID_FMT "/setgroups", pid.pid);
+ r = write_string_file(path, "deny", WRITE_STRING_FILE_DISABLE_BUFFER);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to write setgroups file: %m");
+ }
+
xsprintf(path, "/proc/" PID_FMT "/gid_map", pid.pid);
r = write_string_file(path, gid_map, WRITE_STRING_FILE_DISABLE_BUFFER);
if (r < 0)
@@ -554,6 +561,21 @@ int userns_acquire(const char *uid_map, const char *gid_map) {
return pidref_namespace_open_by_type(&pid, NAMESPACE_USER);
}
+int userns_acquire_self_root(void) {
+
+ /* Returns a user namespace with only our own uid/gid mapped to root, and everything else unmapped.
+ *
+ * Note: this can be acquired unprivileged! */
+
+ _cleanup_free_ char *uid_map = NULL, *gid_map = NULL;
+ if (asprintf(&uid_map, "0 " UID_FMT " 1", getuid()) < 0)
+ return -ENOMEM;
+ if (asprintf(&gid_map, "0 " GID_FMT " 1", getgid()) < 0)
+ return -ENOMEM;
+
+ return userns_acquire(uid_map, gid_map, /* setgroups_deny= */ true);
+}
+
int userns_enter_and_pin(int userns_fd, pid_t *ret_pid) {
_cleanup_close_pair_ int pfd[2] = EBADF_PAIR;
_cleanup_(sigkill_waitp) pid_t pid = 0;
@@ -697,7 +719,6 @@ int process_is_owned_by_uid(const PidRef *pidref, uid_t uid) {
int is_idmapping_supported(const char *path) {
_cleanup_close_ int mount_fd = -EBADF, userns_fd = -EBADF, dir_fd = -EBADF;
- _cleanup_free_ char *uid_map = NULL, *gid_map = NULL;
int r;
assert(path);
@@ -705,15 +726,7 @@ int is_idmapping_supported(const char *path) {
if (!mount_new_api_supported())
return false;
- r = strextendf(&uid_map, UID_FMT " " UID_FMT " " UID_FMT "\n", UID_NOBODY, UID_NOBODY, 1u);
- if (r < 0)
- return r;
-
- r = strextendf(&gid_map, GID_FMT " " GID_FMT " " GID_FMT "\n", GID_NOBODY, GID_NOBODY, 1u);
- if (r < 0)
- return r;
-
- userns_fd = r = userns_acquire(uid_map, gid_map);
+ userns_fd = r = userns_acquire_self_root();
if (ERRNO_IS_NEG_NOT_SUPPORTED(r) || ERRNO_IS_NEG_PRIVILEGE(r) || r == -EINVAL)
return false;
if (r == -ENOSPC) {
diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h
index 3d40a515e7..0c227e5adf 100644
--- a/src/basic/namespace-util.h
+++ b/src/basic/namespace-util.h
@@ -86,7 +86,8 @@ static inline bool userns_shift_range_valid(uid_t shift, uid_t range) {
int parse_userns_uid_range(const char *s, uid_t *ret_uid_shift, uid_t *ret_uid_range);
int userns_acquire_empty(void);
-int userns_acquire(const char *uid_map, const char *gid_map);
+int userns_acquire(const char *uid_map, const char *gid_map, bool setgroups_deny);
+int userns_acquire_self_root(void);
int userns_enter_and_pin(int userns_fd, pid_t *ret_pid);
int userns_get_base_uid(int userns_fd, uid_t *ret_uid, gid_t *ret_gid);
diff --git a/src/core/namespace.c b/src/core/namespace.c
index e801b6ea60..64f773b2c2 100644
--- a/src/core/namespace.c
+++ b/src/core/namespace.c
@@ -1899,25 +1899,23 @@ static int apply_one_mount(
log_debug("Setting an id-mapped mount on %s", mount_entry_path(m));
/* Do mapping from nobody (in setup_exec_directory()) -> this uid */
- if (strextendf(&uid_map, UID_FMT " " UID_FMT " " UID_FMT "\n", UID_NOBODY, (uid_t)m->idmap_uid, (uid_t)1u) < 0)
+ if (strextendf(&uid_map, UID_FMT " " UID_FMT " 1\n", UID_NOBODY, m->idmap_uid) < 0)
return log_oom();
/* Consider StateDirectory=xxx aaa xxx:aaa/222
* To allow for later symlink creation (by root) in create_symlinks_from_tuples(), map root as well. */
- if (m->idmap_uid != (uid_t)0) {
- if (strextendf(&uid_map, UID_FMT " " UID_FMT " " UID_FMT "\n", (uid_t)0, (uid_t)0, (uid_t)1u) < 0)
+ if (m->idmap_uid != 0)
+ if (!strextend(&uid_map, "0 0 1\n"))
return log_oom();
- }
- if (strextendf(&gid_map, GID_FMT " " GID_FMT " " GID_FMT "\n", GID_NOBODY, (gid_t)m->idmap_gid, (gid_t)1u) < 0)
+ if (strextendf(&gid_map, GID_FMT " " GID_FMT " 1\n", GID_NOBODY, m->idmap_gid) < 0)
return log_oom();
- if (m->idmap_gid != (gid_t)0) {
- if (strextendf(&gid_map, GID_FMT " " GID_FMT " " GID_FMT "\n", (gid_t)0, (gid_t)0, (gid_t)1u) < 0)
+ if (m->idmap_gid != 0)
+ if (!strextend(&gid_map, "0 0 1\n"))
return log_oom();
- }
- userns_fd = userns_acquire(uid_map, gid_map);
+ userns_fd = userns_acquire(uid_map, gid_map, /* setgroups_deny= */ true);
if (userns_fd < 0)
return log_error_errno(userns_fd, "Failed to allocate user namespace: %m");
diff --git a/src/home/homework-mount.c b/src/home/homework-mount.c
index 085ba09e81..52115d86be 100644
--- a/src/home/homework-mount.c
+++ b/src/home/homework-mount.c
@@ -232,7 +232,7 @@ static int make_home_userns(uid_t stored_uid, uid_t exposed_uid) {
log_debug("Creating userns with mapping:\n%s", text);
- userns_fd = userns_acquire(text, text); /* same uid + gid mapping */
+ userns_fd = userns_acquire(text, text, /* setgroups_deny= */ true); /* same uid + gid mapping */
if (userns_fd < 0)
return log_error_errno(userns_fd, "Failed to allocate user namespace: %m");
diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c
index eaf72893aa..41c1d65391 100644
--- a/src/mountfsd/mountwork.c
+++ b/src/mountfsd/mountwork.c
@@ -860,7 +860,7 @@ static int vl_method_mount_directory(
if (r < 0)
return r;
- _cleanup_close_ int idmap_userns_fd = userns_acquire(new_uid_map, new_uid_map);
+ _cleanup_close_ int idmap_userns_fd = userns_acquire(new_uid_map, new_uid_map, /* setgroups_deny= */ true);
if (idmap_userns_fd < 0)
return log_debug_errno(idmap_userns_fd, "Failed to acquire user namespace for id mapping: %m");
diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c
index e3c5d6b732..87cae525ca 100644
--- a/src/nspawn/nspawn.c
+++ b/src/nspawn/nspawn.c
@@ -5459,7 +5459,7 @@ static int run_container(
} else {
_cleanup_free_ char *host_ifname = NULL;
- r = nsresource_add_netif(userns_fd, child_netns_fd, /* namespace_ifname= */ NULL, &host_ifname, /* ret_namespace_ifname= */ NULL);
+ r = nsresource_add_netif_veth(userns_fd, child_netns_fd, /* namespace_ifname= */ NULL, &host_ifname, /* ret_namespace_ifname= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to add network interface to container: %m");
diff --git a/src/nsresourced/io.systemd.namespace-resource.policy b/src/nsresourced/io.systemd.namespace-resource.policy
new file mode 100644
index 0000000000..b71efb9fc2
--- /dev/null
+++ b/src/nsresourced/io.systemd.namespace-resource.policy
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+ The systemd Project
+ https://systemd.io
+
+
+
+ Allow user namespace allocation
+ Authentication is required for an application to allocate a user namespace '$(name)' with an automatically assigned transient UID range.
+
+ yes
+ yes
+ yes
+
+ io.systemd.namespace-resource.register-user-namespace
+
+
+
+
+ Allow user namespace registration
+ Authentication is required for an application to register a user namespace '$(name)'.
+
+ yes
+ yes
+ yes
+
+ io.systemd.namespace-resource.allocate-user-namespace
+
+
+
+
+ Allow adding a mount to a user namespace
+ Authentication is required for an application to add a mount to a user namespace.
+
+ yes
+ yes
+ yes
+
+
+
+
+
+ Allow adding a control group to a user namespace
+ Authentication is required for an application to add a control group to a user namespace.
+
+ yes
+ yes
+ yes
+
+
+
+
+
+ Allow adding a network interface to a user namespace
+ Authentication is required for an application to add a network interface of type $(type) to a user namespace.
+
+ yes
+ yes
+ yes
+
+
+
+
diff --git a/src/nsresourced/meson.build b/src/nsresourced/meson.build
index d8524d52bf..b80d6b2a34 100644
--- a/src/nsresourced/meson.build
+++ b/src/nsresourced/meson.build
@@ -48,3 +48,6 @@ executables += [
'include_directories' : [ includes, userns_restrict_include ],
},
]
+
+install_data('io.systemd.namespace-resource.policy',
+ install_dir : polkitpolicydir)
diff --git a/src/nsresourced/nsresourced-manager.c b/src/nsresourced/nsresourced-manager.c
index 7fac1828ca..99a6e1fede 100644
--- a/src/nsresourced/nsresourced-manager.c
+++ b/src/nsresourced/nsresourced-manager.c
@@ -344,10 +344,12 @@ static void manager_release_userns_by_inode(Manager *m, uint64_t inode) {
log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
"Failed to find userns for inode %" PRIu64 ", ignoring: %m", inode);
- if (userns_info && uid_is_valid(userns_info->start))
- log_debug("Removing user namespace mapping %" PRIu64 " for UID " UID_FMT ".", inode, userns_info->start);
- else
- log_debug("Removing user namespace mapping %" PRIu64 ".", inode);
+ if (DEBUG_LOGGING) {
+ if (userns_info && uid_is_valid(userns_info->start_uid))
+ log_debug("Removing user namespace mapping %" PRIu64 " for UID " UID_FMT ".", inode, userns_info->start_uid);
+ else
+ log_debug("Removing user namespace mapping %" PRIu64 ".", inode);
+ }
/* Remove the BPF rules */
manager_release_userns_bpf(m, inode);
@@ -360,7 +362,12 @@ static void manager_release_userns_by_inode(Manager *m, uint64_t inode) {
/* Remove the cgroups of this userns */
r = userns_info_remove_cgroups(userns_info);
if (r < 0)
- log_warning_errno(r, "Failed to remove cgroups of user namespace: %m");
+ log_warning_errno(r, "Failed to remove cgroups of user namespace, ignoring: %m");
+
+ /* Remove the netifs of this userns */
+ r = userns_info_remove_netifs(userns_info);
+ if (r < 0)
+ log_warning_errno(r, "Failed to remove netifs of user namespace, ignoring: %m");
r = userns_registry_remove(m->registry_fd, userns_info);
if (r < 0)
diff --git a/src/nsresourced/nsresourcework.c b/src/nsresourced/nsresourcework.c
index 26b382066f..671afc6997 100644
--- a/src/nsresourced/nsresourcework.c
+++ b/src/nsresourced/nsresourcework.c
@@ -1,9 +1,12 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* Make sure the net/if.h header is included before any linux/ one */
+#include
+
#include
+#include
#include
#include
-#include
#include
#include
#include
@@ -14,6 +17,7 @@
#include "sd-netlink.h"
#include "sd-varlink.h"
+#include "bus-polkit.h"
#include "env-util.h"
#include "fd-util.h"
#include "fileio.h"
@@ -50,10 +54,14 @@
#define ITERATIONS_MAX 64U
#define RUNTIME_MAX_USEC (5 * USEC_PER_MINUTE)
#define PRESSURE_SLEEP_TIME_USEC (50 * USEC_PER_MSEC)
-#define CONNECTION_IDLE_USEC (15 * USEC_PER_SEC)
#define LISTEN_IDLE_USEC (90 * USEC_PER_SEC)
#define USERNS_PER_UID 256
+typedef struct Context {
+ Hashmap *polkit_registry;
+ struct userns_restrict_bpf *bpf;
+} Context;
+
typedef struct LookupParameters {
const char *user_name;
const char *group_name;
@@ -88,7 +96,7 @@ static int build_user_json(UserNamespaceInfo *userns_info, uid_t offset, sd_json
return sd_json_buildo(
ret,
SD_JSON_BUILD_PAIR("userName", SD_JSON_BUILD_STRING(name)),
- SD_JSON_BUILD_PAIR("uid", SD_JSON_BUILD_UNSIGNED(userns_info->start + offset)),
+ SD_JSON_BUILD_PAIR("uid", SD_JSON_BUILD_UNSIGNED(userns_info->start_uid + offset)),
SD_JSON_BUILD_PAIR("gid", SD_JSON_BUILD_UNSIGNED(GID_NOBODY)),
SD_JSON_BUILD_PAIR("realName", SD_JSON_BUILD_STRING(realname)),
SD_JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_CONST_STRING("/")),
@@ -155,7 +163,7 @@ static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *paramete
if (offset >= userns_info->size) /* Outside of range? */
goto not_found;
- if (uid_is_valid(p.uid) && p.uid != userns_info->start + offset)
+ if (uid_is_valid(p.uid) && p.uid != userns_info->start_uid + offset)
return sd_varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
} else if (uid_is_valid(p.uid)) {
@@ -219,7 +227,7 @@ static int build_group_json(UserNamespaceInfo *userns_info, gid_t offset, sd_jso
return sd_json_buildo(
ret,
SD_JSON_BUILD_PAIR("groupName", SD_JSON_BUILD_STRING(name)),
- SD_JSON_BUILD_PAIR("gid", SD_JSON_BUILD_UNSIGNED(userns_info->start + offset)),
+ SD_JSON_BUILD_PAIR("gid", SD_JSON_BUILD_UNSIGNED(userns_info->start_gid + offset)),
SD_JSON_BUILD_PAIR("description", SD_JSON_BUILD_STRING(description)),
SD_JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NamespaceResource")),
SD_JSON_BUILD_PAIR("disposition", SD_JSON_BUILD_STRING(user_disposition_to_string(disposition))));
@@ -282,7 +290,7 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet
if (offset >= userns_info->size) /* Outside of range? */
goto not_found;
- if (gid_is_valid(p.gid) && p.gid != userns_info->start + offset)
+ if (gid_is_valid(p.gid) && p.gid != userns_info->start_gid + offset)
return sd_varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
} else if (gid_is_valid(p.gid)) {
@@ -298,9 +306,9 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet
start = p.gid & gidmask;
offset = p.gid - start;
- r = userns_registry_load_by_start_uid(
+ r = userns_registry_load_by_start_gid(
/* registry_fd= */ -EBADF,
- (uid_t) start,
+ start,
&userns_info);
if (r == -ENOENT)
goto not_found;
@@ -362,6 +370,12 @@ static int uid_is_available(
if (r > 0)
return false;
+ r = userns_registry_gid_exists(registry_dir_fd, (gid_t) candidate);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return false;
+
r = userdb_by_uid(candidate, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret_record= */ NULL);
if (r >= 0)
return false;
@@ -508,7 +522,8 @@ static int allocate_now(
if (r < 0)
return log_debug_errno(r, "Can't determine if UID range " UID_FMT " is available: %m", candidate);
if (r > 0) {
- info->start = candidate;
+ info->start_uid = candidate;
+ info->start_gid = (gid_t) candidate;
log_debug("Allocating UID range " UID_FMT "…" UID_FMT, candidate, candidate + info->size - 1);
@@ -530,10 +545,13 @@ static int write_userns(int usernsfd, const UserNamespaceInfo *userns_info) {
assert(usernsfd >= 0);
assert(userns_info);
- assert(uid_is_valid(userns_info->target));
- assert(uid_is_valid(userns_info->start));
+ assert(uid_is_valid(userns_info->target_uid));
+ assert(uid_is_valid(userns_info->start_uid));
+ assert(gid_is_valid(userns_info->target_gid));
+ assert(gid_is_valid(userns_info->start_gid));
assert(userns_info->size > 0);
- assert(userns_info->size <= UINT32_MAX - userns_info->start);
+ assert(userns_info->size <= UINT32_MAX - userns_info->start_uid);
+ assert(userns_info->size <= UINT32_MAX - userns_info->start_gid);
efd = eventfd(0, EFD_CLOEXEC);
if (efd < 0)
@@ -572,7 +590,7 @@ static int write_userns(int usernsfd, const UserNamespaceInfo *userns_info) {
if (asprintf(&pmap, "/proc/" PID_FMT "/uid_map", pid) < 0)
return log_oom();
- r = write_string_filef(pmap, 0, UID_FMT " " UID_FMT " " UID_FMT "\n", userns_info->target, userns_info->start, userns_info->size);
+ r = write_string_filef(pmap, 0, UID_FMT " " UID_FMT " %" PRIu32 "\n", userns_info->target_uid, userns_info->start_uid, userns_info->size);
if (r < 0)
return log_error_errno(r, "Failed to write 'uid_map' file of user namespace: %m");
@@ -580,7 +598,7 @@ static int write_userns(int usernsfd, const UserNamespaceInfo *userns_info) {
if (asprintf(&pmap, "/proc/" PID_FMT "/gid_map", pid) < 0)
return log_oom();
- r = write_string_filef(pmap, 0, GID_FMT " " GID_FMT " " GID_FMT "\n", (gid_t) userns_info->target, (gid_t) userns_info->start, (gid_t) userns_info->size);
+ r = write_string_filef(pmap, 0, GID_FMT " " GID_FMT " %" PRIu32 "\n", userns_info->target_gid, userns_info->start_gid, userns_info->size);
if (r < 0)
return log_error_errno(r, "Failed to write 'gid_map' file of user namespace: %m");
@@ -811,9 +829,9 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
{}
};
- struct userns_restrict_bpf **bpf = ASSERT_PTR(userdata);
_cleanup_close_ int userns_fd = -EBADF, registry_dir_fd = -EBADF, lock_fd = -EBADF;
_cleanup_free_ char *userns_name = NULL;
+ Context *c = ASSERT_PTR(userdata);
uid_t peer_uid;
struct stat userns_st;
AllocateParameters p = {
@@ -841,7 +859,7 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
if (r != 0)
return r;
- userns_fd = sd_varlink_take_fd(link, p.userns_fd_idx);
+ userns_fd = sd_varlink_peek_dup_fd(link, p.userns_fd_idx);
if (userns_fd < 0)
return log_debug_errno(userns_fd, "Failed to take user namespace fd from Varlink connection: %m");
@@ -860,8 +878,24 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
if (r < 0)
return r;
- if (!*bpf) {
- r = userns_restrict_install(/* pin= */ true, bpf);
+ const char *polkit_details[] = {
+ "name", userns_name,
+ NULL,
+ };
+
+ r = varlink_verify_polkit_async_full(
+ link,
+ /* bus= */ NULL,
+ "io.systemd.namespace-resource.allocate-user-namespace",
+ polkit_details,
+ /* good_user= */ UID_INVALID,
+ POLKIT_DEFAULT_ALLOW, /* If no polkit is installed, allow unpriv userns namespace allocation */
+ &c->polkit_registry);
+ if (r <= 0)
+ return r;
+
+ if (!c->bpf) {
+ r = userns_restrict_install(/* pin= */ true, &c->bpf);
if (r < 0)
return r;
}
@@ -881,7 +915,8 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
userns_info->owner = peer_uid;
userns_info->userns_inode = userns_st.st_ino;
userns_info->size = p.size;
- userns_info->target = p.target;
+ userns_info->target_uid = p.target;
+ userns_info->target_gid = (gid_t) p.target;
r = allocate_now(registry_dir_fd, userns_info, &lock_fd);
if (r == -EHOSTDOWN) /* The needed UID range is not delegated to us */
@@ -901,7 +936,7 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
/* Register the userns in the BPF map with an empty allowlist */
r = userns_restrict_put_by_fd(
- *bpf,
+ c->bpf,
userns_fd,
/* replace= */ true,
/* mount_fds= */ NULL,
@@ -1012,9 +1047,9 @@ static int vl_method_register_user_namespace(sd_varlink *link, sd_json_variant *
{}
};
- struct userns_restrict_bpf **bpf = ASSERT_PTR(userdata);
_cleanup_close_ int userns_fd = -EBADF, registry_dir_fd = -EBADF;
_cleanup_free_ char *userns_name = NULL;
+ Context *c = ASSERT_PTR(userdata);
uid_t peer_uid;
struct stat userns_st;
RegisterParameters p = {
@@ -1037,7 +1072,7 @@ static int vl_method_register_user_namespace(sd_varlink *link, sd_json_variant *
if (r != 0)
return r;
- userns_fd = sd_varlink_take_fd(link, p.userns_fd_idx);
+ userns_fd = sd_varlink_peek_dup_fd(link, p.userns_fd_idx);
if (userns_fd < 0)
return userns_fd;
@@ -1056,8 +1091,24 @@ static int vl_method_register_user_namespace(sd_varlink *link, sd_json_variant *
if (r < 0)
return r;
- if (!*bpf) {
- r = userns_restrict_install(/* pin= */ true, bpf);
+ const char *polkit_details[] = {
+ "name", userns_name,
+ NULL,
+ };
+
+ r = varlink_verify_polkit_async_full(
+ link,
+ /* bus= */ NULL,
+ "io.systemd.namespace-resource.register-user-namespace",
+ polkit_details,
+ /* good_user= */ UID_INVALID,
+ POLKIT_DEFAULT_ALLOW, /* If no polkit is installed, allow unpriv userns namespace registration */
+ &c->polkit_registry);
+ if (r <= 0)
+ return r;
+
+ if (!c->bpf) {
+ r = userns_restrict_install(/* pin= */ true, &c->bpf);
if (r < 0)
return r;
}
@@ -1100,7 +1151,7 @@ static int vl_method_register_user_namespace(sd_varlink *link, sd_json_variant *
/* Register the userns in the BPF map with an empty allowlist */
r = userns_restrict_put_by_fd(
- *bpf,
+ c->bpf,
userns_fd,
/* replace= */ true,
/* mount_fds= */ NULL,
@@ -1139,7 +1190,7 @@ static int vl_method_add_mount_to_user_namespace(sd_varlink *link, sd_json_varia
};
_cleanup_close_ int userns_fd = -EBADF, mount_fd = -EBADF, registry_dir_fd = -EBADF;
- struct userns_restrict_bpf **bpf = ASSERT_PTR(userdata);
+ Context *c = ASSERT_PTR(userdata);
AddMountParameters p = {
.userns_fd_idx = UINT_MAX,
.mount_fd_idx = UINT_MAX,
@@ -1166,7 +1217,7 @@ static int vl_method_add_mount_to_user_namespace(sd_varlink *link, sd_json_varia
if (r != 0)
return r;
- userns_fd = sd_varlink_take_fd(link, p.userns_fd_idx);
+ userns_fd = sd_varlink_peek_dup_fd(link, p.userns_fd_idx);
if (userns_fd < 0)
return userns_fd;
@@ -1177,7 +1228,7 @@ static int vl_method_add_mount_to_user_namespace(sd_varlink *link, sd_json_varia
if (fstat(userns_fd, &userns_st) < 0)
return -errno;
- mount_fd = sd_varlink_take_fd(link, p.mount_fd_idx);
+ mount_fd = sd_varlink_peek_dup_fd(link, p.mount_fd_idx);
if (mount_fd < 0)
return mount_fd;
@@ -1193,6 +1244,17 @@ static int vl_method_add_mount_to_user_namespace(sd_varlink *link, sd_json_varia
if (r < 0)
return r;
+ r = varlink_verify_polkit_async_full(
+ link,
+ /* bus= */ NULL,
+ "io.systemd.namespace-resource.delegate-mount",
+ /* polkit_details= */ NULL,
+ /* good_user= */ UID_INVALID,
+ POLKIT_DEFAULT_ALLOW, /* If no polkit is installed, allow delegation of mounts to registered userns */
+ &c->polkit_registry);
+ if (r <= 0)
+ return r;
+
registry_dir_fd = userns_registry_open_fd();
if (registry_dir_fd < 0)
return registry_dir_fd;
@@ -1212,8 +1274,8 @@ static int vl_method_add_mount_to_user_namespace(sd_varlink *link, sd_json_varia
if (r < 0)
return r;
- if (!*bpf) {
- r = userns_restrict_install(/* pin= */ true, bpf);
+ if (!c->bpf) {
+ r = userns_restrict_install(/* pin= */ true, &c->bpf);
if (r < 0)
return r;
}
@@ -1230,7 +1292,7 @@ static int vl_method_add_mount_to_user_namespace(sd_varlink *link, sd_json_varia
/* Add this mount to the user namespace's BPF map allowlist entry. */
r = userns_restrict_put_by_fd(
- *bpf,
+ c->bpf,
userns_fd,
/* replace= */ false,
&mount_fd,
@@ -1238,12 +1300,14 @@ static int vl_method_add_mount_to_user_namespace(sd_varlink *link, sd_json_varia
if (r < 0)
return r;
- if (userns_info->size > 0)
- log_debug("Granting access to mount %i to user namespace " INO_FMT " ('%s' @ UID " UID_FMT ")",
- mnt_id, userns_st.st_ino, userns_info->name, userns_info->start);
- else
- log_debug("Granting access to mount %i to user namespace " INO_FMT " ('%s')",
- mnt_id, userns_st.st_ino, userns_info->name);
+ if (DEBUG_LOGGING) {
+ if (userns_info->size > 0)
+ log_debug("Granting access to mount %i to user namespace " INO_FMT " ('%s' @ UID " UID_FMT ")",
+ mnt_id, userns_st.st_ino, userns_info->name, userns_info->start_uid);
+ else
+ log_debug("Granting access to mount %i to user namespace " INO_FMT " ('%s')",
+ mnt_id, userns_st.st_ino, userns_info->name);
+ }
return sd_varlink_replyb(link, SD_JSON_BUILD_EMPTY_OBJECT);
}
@@ -1294,6 +1358,7 @@ static int vl_method_add_cgroup_to_user_namespace(sd_varlink *link, sd_json_vari
.cgroup_fd_idx = UINT_MAX,
};
_cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL;
+ Context *c = ASSERT_PTR(userdata);
struct stat userns_st, cgroup_st;
uid_t peer_uid;
int r;
@@ -1309,7 +1374,7 @@ static int vl_method_add_cgroup_to_user_namespace(sd_varlink *link, sd_json_vari
if (r != 0)
return r;
- userns_fd = sd_varlink_take_fd(link, p.userns_fd_idx);
+ userns_fd = sd_varlink_peek_dup_fd(link, p.userns_fd_idx);
if (userns_fd < 0)
return log_debug_errno(userns_fd, "Failed to take user namespace fd from Varlink connection: %m");
@@ -1320,7 +1385,7 @@ static int vl_method_add_cgroup_to_user_namespace(sd_varlink *link, sd_json_vari
if (fstat(userns_fd, &userns_st) < 0)
return log_debug_errno(errno, "Failed to fstat() user namespace fd: %m");
- cgroup_fd = sd_varlink_take_fd(link, p.cgroup_fd_idx);
+ cgroup_fd = sd_varlink_peek_dup_fd(link, p.cgroup_fd_idx);
if (cgroup_fd < 0)
return log_debug_errno(cgroup_fd, "Failed to take cgroup fd from Varlink connection: %m");
@@ -1332,6 +1397,17 @@ static int vl_method_add_cgroup_to_user_namespace(sd_varlink *link, sd_json_vari
if (fstat(cgroup_fd, &cgroup_st) < 0)
return log_debug_errno(errno, "Failed to fstat() cgroup fd: %m");
+ r = varlink_verify_polkit_async_full(
+ link,
+ /* bus= */ NULL,
+ "io.systemd.namespace-resource.delegate-cgroup",
+ /* polkit_details= */ NULL,
+ /* good_user= */ UID_INVALID,
+ POLKIT_DEFAULT_ALLOW, /* If no polkit is installed, allow delegation of cgroups to registered userns */
+ &c->polkit_registry);
+ if (r <= 0)
+ return r;
+
registry_dir_fd = userns_registry_open_fd();
if (registry_dir_fd < 0)
return registry_dir_fd;
@@ -1379,7 +1455,7 @@ static int vl_method_add_cgroup_to_user_namespace(sd_varlink *link, sd_json_vari
if (r < 0)
return r;
- if (fchown(cgroup_fd, userns_info->start, userns_info->start) < 0)
+ if (fchown(cgroup_fd, userns_info->start_uid, userns_info->start_gid) < 0)
return log_debug_errno(errno, "Failed to change ownership of cgroup: %m");
if (fchmod(cgroup_fd, 0755) < 0)
@@ -1387,11 +1463,11 @@ static int vl_method_add_cgroup_to_user_namespace(sd_varlink *link, sd_json_vari
FOREACH_STRING(attr, "cgroup.procs", "cgroup.subtree_control", "cgroup.threads") {
(void) fchmodat(cgroup_fd, attr, 0644, AT_SYMLINK_NOFOLLOW);
- (void) fchownat(cgroup_fd, attr, userns_info->start, userns_info->start, AT_SYMLINK_NOFOLLOW);
+ (void) fchownat(cgroup_fd, attr, userns_info->start_uid, userns_info->start_gid, AT_SYMLINK_NOFOLLOW);
}
log_debug("Granting ownership to cgroup %" PRIu64 " to userns " INO_FMT " ('%s' @ UID " UID_FMT ")",
- cgroup_id, userns_st.st_ino, userns_info->name, userns_info->start);
+ cgroup_id, userns_st.st_ino, userns_info->name, userns_info->start_uid);
return sd_varlink_replyb(link, SD_JSON_BUILD_EMPTY_OBJECT);
}
@@ -1455,7 +1531,7 @@ static int create_veth(
_cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
r = sd_netlink_open(&rtnl);
if (r < 0)
- return r;
+ return log_error_errno(r, "Failed to allocation netlink connection: %m");
_cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
r = sd_rtnl_message_new_link(rtnl, &m, RTM_NEWLINK, 0);
@@ -1517,6 +1593,65 @@ static int create_veth(
return 0;
}
+static int create_tap(
+ int userns_fd,
+ const char *ifname_host,
+ char *const *altifname_host,
+ struct ether_addr *mac_host) {
+
+ int r;
+
+ assert(ifname_host);
+ assert(mac_host);
+
+ if (!ifname_valid(ifname_host))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid interface name: %s", ifname_host);
+
+ log_debug("Creating tap link on host %s (%s) with address %s",
+ ifname_host, strna(altifname_host ? altifname_host[0] : NULL), ETHER_ADDR_TO_STR(mac_host));
+
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ if (altifname_host) {
+ r = sd_netlink_open(&rtnl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate netlink connection: %m");
+ }
+
+ uid_t uid;
+ r = userns_get_base_uid(userns_fd, &uid, /* ret_gid= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get namespace base UID/GID: %m");
+
+ struct ifreq ifr = {
+ .ifr_flags = IFF_TAP | IFF_NO_PI | IFF_VNET_HDR,
+ };
+
+ assert(strlen(ifname_host) < sizeof(ifr.ifr_name));
+ strcpy(ifr.ifr_name, ifname_host);
+
+ _cleanup_close_ int fd = open("/dev/net/tun", O_RDWR|O_CLOEXEC);
+ if (fd < 0) {
+ if (errno == ENOENT) /* Turn ENOENT → EOPNOTSUPP */
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Network tap device node /dev/net/tun not found, cannot create network interface.");
+
+ return log_error_errno(errno, "Failed to open /dev/net/tun: %m");
+ }
+
+ if (ioctl(fd, TUNSETIFF, &ifr) < 0)
+ return log_error_errno(errno, "TUNSETIFF failed: %m");
+
+ if (ioctl(fd, TUNSETOWNER, uid) < 0)
+ return log_error_errno(errno, "TUNSETOWNER failed: %m");
+
+ if (!strv_isempty(altifname_host)) {
+ r = rtnl_set_link_alternative_names_by_ifname(&rtnl, ifname_host, altifname_host);
+ if (r < 0)
+ log_warning_errno(r, "Failed to set alternative interface names, ignoring: %m");
+ }
+
+ return TAKE_FD(fd);
+}
+
static int validate_netns(sd_varlink *link, int userns_fd, int netns_fd) {
int r;
@@ -1583,20 +1718,18 @@ typedef struct AddNetworkParameters {
static int vl_method_add_netif_to_user_namespace(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
static const sd_json_dispatch_field parameter_dispatch_table[] = {
{ "userNamespaceFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(AddNetworkParameters, userns_fd_idx), SD_JSON_MANDATORY },
- { "networkNamespaceFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(AddNetworkParameters, netns_fd_idx), SD_JSON_MANDATORY },
+ { "networkNamespaceFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(AddNetworkParameters, netns_fd_idx), 0 },
{ "namespaceInterfaceName", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(AddNetworkParameters, ifname), 0 },
{ "mode", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(AddNetworkParameters, mode), SD_JSON_MANDATORY },
{}
};
_cleanup_close_ int userns_fd = -EBADF, netns_fd = -EBADF, registry_dir_fd = -EBADF;
+ Context *c = ASSERT_PTR(userdata);
AddNetworkParameters p = {
.userns_fd_idx = UINT_MAX,
.netns_fd_idx = UINT_MAX,
};
- _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL;
- struct stat userns_st;
- uid_t peer_uid;
int r;
assert(link);
@@ -1610,7 +1743,7 @@ static int vl_method_add_netif_to_user_namespace(sd_varlink *link, sd_json_varia
if (r != 0)
return r;
- userns_fd = sd_varlink_take_fd(link, p.userns_fd_idx);
+ userns_fd = sd_varlink_peek_dup_fd(link, p.userns_fd_idx);
if (userns_fd < 0)
return userns_fd;
@@ -1618,32 +1751,65 @@ static int vl_method_add_netif_to_user_namespace(sd_varlink *link, sd_json_varia
if (r != 0)
return r;
+ struct stat userns_st;
if (fstat(userns_fd, &userns_st) < 0)
return -errno;
- netns_fd = sd_varlink_take_fd(link, p.netns_fd_idx);
- if (netns_fd < 0)
- return netns_fd;
+ if (p.netns_fd_idx != UINT_MAX) {
+ netns_fd = sd_varlink_peek_dup_fd(link, p.netns_fd_idx);
+ if (netns_fd < 0)
+ return netns_fd;
- r = validate_netns(link, userns_fd, netns_fd);
- if (r != 0)
- return r;
-
- if (!streq_ptr(p.mode, "veth"))
- return sd_varlink_error_invalid_parameter_name(link, "mode");
+ r = validate_netns(link, userns_fd, netns_fd);
+ if (r != 0)
+ return r;
+ }
if (p.ifname && !ifname_valid(p.ifname))
return sd_varlink_error_invalid_parameter_name(link, "interfaceName");
+ if (streq(p.mode, "veth")) {
+ /* In veth mode we need a netns */
+
+ if (netns_fd < 0)
+ return sd_varlink_error_invalid_parameter_name(link, "networkNamespaceFileDescriptor");
+
+ } else if (streq(p.mode, "tap")) {
+ /* In tap mode we do want a netns, nor an interface name for it */
+
+ if (p.ifname)
+ return sd_varlink_error_invalid_parameter_name(link, "namespaceInterfaceName");
+
+ if (netns_fd >= 0)
+ return sd_varlink_error_invalid_parameter_name(link, "networkNamespaceFileDescriptor");
+ } else
+ return sd_varlink_error_invalid_parameter_name(link, "mode");
+
+ const char *polkit_details[] = {
+ "type", p.mode,
+ NULL,
+ };
+
+ r = varlink_verify_polkit_async_full(
+ link,
+ /* bus= */ NULL,
+ "io.systemd.namespace-resource.delegate-network-interface",
+ polkit_details,
+ /* good_user= */ UID_INVALID,
+ POLKIT_DEFAULT_ALLOW, /* If no polkit is installed, allow delegation of network interfaces to registered userns */
+ &c->polkit_registry);
+ if (r <= 0)
+ return r;
+
registry_dir_fd = userns_registry_open_fd();
if (registry_dir_fd < 0)
return registry_dir_fd;
- _cleanup_close_ int lock_fd = -EBADF;
- lock_fd = userns_registry_lock(registry_dir_fd);
+ _cleanup_close_ int lock_fd = userns_registry_lock(registry_dir_fd);
if (lock_fd < 0)
return log_debug_errno(lock_fd, "Failed to open nsresource registry lock file: %m");
+ _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL;
r = userns_registry_load_by_userns_inode(
registry_dir_fd,
userns_st.st_ino,
@@ -1653,7 +1819,11 @@ static int vl_method_add_netif_to_user_namespace(sd_varlink *link, sd_json_varia
if (r < 0)
return r;
+ if (strv_length(userns_info->netifs) > USER_NAMESPACE_NETIFS_DELEGATE_MAX)
+ return sd_varlink_error(link, "io.systemd.NamespaceResource.TooManyNetworkInterfaces", NULL);
+
/* Registering a network interface for this client is only allowed for the root or the owner of a userns */
+ uid_t peer_uid;
r = sd_varlink_get_peer_uid(link, &peer_uid);
if (r < 0)
return r;
@@ -1670,6 +1840,17 @@ static int vl_method_add_netif_to_user_namespace(sd_varlink *link, sd_json_varia
return -ENOMEM;
strshorten(ifname_host, IFNAMSIZ-1);
+ /* Register the interface in the userns store first, so that we can be sure it's properly 'owned' at
+ * any time, in case setup fails for some reason. Given we the interface name is hashed accidental
+ * collisions should be unlikely. */
+ r = userns_info_add_netif(userns_info, ifname_host);
+ if (r < 0)
+ return r;
+
+ r = userns_registry_store(registry_dir_fd, userns_info);
+ if (r < 0)
+ return r;
+
if (p.ifname)
r = asprintf(&altifname_host, "ns-" UID_FMT "-%s-%s", userns_info->owner, userns_info->name, p.ifname);
else
@@ -1677,34 +1858,74 @@ static int vl_method_add_netif_to_user_namespace(sd_varlink *link, sd_json_varia
if (r < 0)
return -ENOMEM;
- struct ether_addr ether_addr_host, ether_addr_namespace;
+ if (!ifname_valid_full(altifname_host, IFNAME_VALID_ALTERNATIVE))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Generated alternative interface name not valid: %s", altifname_host);
+ struct ether_addr ether_addr_host;
hash_ether_addr(userns_info, p.ifname, 0, ðer_addr_host);
- hash_ether_addr(userns_info, p.ifname, 1, ðer_addr_namespace);
- r = create_veth(netns_fd,
- ifname_host, altifname_host, ðer_addr_host,
- ifname_namespace, ðer_addr_namespace);
- if (r < 0)
- return r;
+ if (streq(p.mode, "veth")) {
+ struct ether_addr ether_addr_namespace;
+ hash_ether_addr(userns_info, p.ifname, 1, ðer_addr_namespace);
- log_debug("Adding veth tunnel %s from host to userns " INO_FMT " ('%s' @ UID " UID_FMT ", interface %s).",
- ifname_host, userns_st.st_ino, userns_info->name, userns_info->start, ifname_namespace);
+ r = create_veth(netns_fd,
+ ifname_host, altifname_host, ðer_addr_host,
+ ifname_namespace, ðer_addr_namespace);
+ if (r < 0)
+ return r;
- return sd_varlink_replybo(
- link,
- SD_JSON_BUILD_PAIR("hostInterfaceName", SD_JSON_BUILD_STRING(ifname_host)),
- SD_JSON_BUILD_PAIR("namespaceInterfaceName", SD_JSON_BUILD_STRING(ifname_namespace)));
+ log_debug("Added veth tunnel %s from host to userns " INO_FMT " ('%s' @ UID " UID_FMT ", interface %s).",
+ ifname_host, userns_st.st_ino, userns_info->name, userns_info->start_uid, ifname_namespace);
+
+ return sd_varlink_replybo(
+ link,
+ SD_JSON_BUILD_PAIR("hostInterfaceName", SD_JSON_BUILD_STRING(ifname_host)),
+ SD_JSON_BUILD_PAIR("namespaceInterfaceName", SD_JSON_BUILD_STRING(ifname_namespace)));
+
+ } else if (streq(p.mode, "tap")) {
+ /* NB: when we do the "tap" stuff we do not actually do any namespace operation here, neither
+ * netns nor userns. We use the userns only as conduit for user identity information and
+ * indication that the calling user has some control over the UID they want to assign the tap
+ * device to. */
+
+ _cleanup_close_ int tap_fd = create_tap(userns_fd, ifname_host, STRV_MAKE(altifname_host), ðer_addr_host);
+ if (tap_fd < 0)
+ return tap_fd;
+
+ log_debug("Added tap device %s from host to userns " INO_FMT " ('%s' @ UID " UID_FMT ").",
+ ifname_host, userns_st.st_ino, userns_info->name, userns_info->start_uid);
+
+ int fd_index = sd_varlink_push_fd(link, tap_fd);
+ if (fd_index < 0)
+ return log_error_errno(fd_index, "Failed to push tap fd into varlink socket: %m");
+
+ TAKE_FD(tap_fd);
+
+ return sd_varlink_replybo(
+ link,
+ SD_JSON_BUILD_PAIR_STRING("hostInterfaceName", ifname_host),
+ SD_JSON_BUILD_PAIR_INTEGER("interfaceFileDescriptor", fd_index));
+ } else
+ assert_not_reached();
}
static int process_connection(sd_varlink_server *server, int _fd) {
_cleanup_close_ int fd = TAKE_FD(_fd); /* always take possession */
_cleanup_(sd_varlink_close_unrefp) sd_varlink *vl = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
int r;
assert(server);
assert(fd >= 0);
+ r = sd_event_new(&event);
+ if (r < 0)
+ return r;
+
+ r = sd_varlink_server_attach_event(server, event, /* priority= */ 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach Varlink server to event loop: %m");
+
r = sd_varlink_server_add_connection(server, fd, &vl);
if (r < 0)
return log_error_errno(r, "Failed to add connection: %m");
@@ -1712,31 +1933,28 @@ static int process_connection(sd_varlink_server *server, int _fd) {
TAKE_FD(fd);
vl = sd_varlink_ref(vl);
- for (;;) {
- r = sd_varlink_process(vl);
- if (r == -ENOTCONN) {
- log_debug("Connection terminated.");
- break;
- }
- if (r < 0)
- return log_error_errno(r, "Failed to process connection: %m");
- if (r > 0)
- continue;
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
- r = sd_varlink_wait(vl, CONNECTION_IDLE_USEC);
- if (r < 0)
- return log_error_errno(r, "Failed to wait for connection events: %m");
- if (r == 0)
- break;
- }
+ r = sd_varlink_server_detach_event(server);
+ if (r < 0)
+ return log_error_errno(r, "Failed to detach Varlink server from event loop: %m");
return 0;
}
+static void context_free(Context *c) {
+ assert(c);
+
+ c->polkit_registry = hashmap_free(c->polkit_registry);
+ c->bpf = userns_restrict_bpf_free(c->bpf);
+}
+
static int run(int argc, char *argv[]) {
- _cleanup_(userns_restrict_bpf_freep) struct userns_restrict_bpf *bpf = NULL;
usec_t start_time, listen_idle_usec, last_busy_usec = USEC_INFINITY;
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *server = NULL;
+ _cleanup_(context_free) Context c = {};
_cleanup_(pidref_done) PidRef parent = PIDREF_NULL;
unsigned n_iterations = 0;
int m, listen_fd, r;
@@ -1761,7 +1979,7 @@ static int run(int argc, char *argv[]) {
&server,
SD_VARLINK_SERVER_INHERIT_USERDATA|
SD_VARLINK_SERVER_ALLOW_FD_PASSING_INPUT|SD_VARLINK_SERVER_ALLOW_FD_PASSING_OUTPUT,
- &bpf);
+ &c);
if (r < 0)
return log_error_errno(r, "Failed to allocate varlink server: %m");
@@ -1785,6 +2003,10 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return log_error_errno(r, "Failed to bind methods: %m");
+ r = sd_varlink_server_set_exit_on_idle(server, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enable exit-on-idle mode: %m");
+
r = getenv_bool("NSRESOURCE_FIXED_WORKER");
if (r < 0)
return log_error_errno(r, "Failed to parse NSRESOURCE_FIXED_WORKER: %m");
diff --git a/src/nsresourced/test-userns-restrict.c b/src/nsresourced/test-userns-restrict.c
index d6df02428e..59ca1cabbb 100644
--- a/src/nsresourced/test-userns-restrict.c
+++ b/src/nsresourced/test-userns-restrict.c
@@ -78,7 +78,7 @@ static int run(int argc, char *argv[]) {
host_tmpfs = make_tmpfs_fsmount();
assert_se(host_tmpfs >= 0);
- userns_fd = userns_acquire("0 0 1", "0 0 1");
+ userns_fd = userns_acquire("0 0 1", "0 0 1", /* setgroups_deny= */ true);
if (userns_fd < 0)
return log_error_errno(userns_fd, "Failed to make user namespace: %m");
diff --git a/src/nsresourced/userns-registry.c b/src/nsresourced/userns-registry.c
index b1754b51fe..34205e7107 100644
--- a/src/nsresourced/userns-registry.c
+++ b/src/nsresourced/userns-registry.c
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-json.h"
+#include "sd-netlink.h"
#include "chase.h"
#include "fd-util.h"
@@ -55,8 +56,10 @@ UserNamespaceInfo* userns_info_new(void) {
*info = (UserNamespaceInfo) {
.owner = UID_INVALID,
- .start = UID_INVALID,
- .target = UID_INVALID,
+ .start_uid = UID_INVALID,
+ .target_uid = UID_INVALID,
+ .start_gid = GID_INVALID,
+ .target_gid = GID_INVALID,
};
return info;
@@ -69,6 +72,8 @@ UserNamespaceInfo *userns_info_free(UserNamespaceInfo *userns) {
free(userns->cgroups);
free(userns->name);
+ strv_free(userns->netifs);
+
return mfree(userns);
}
@@ -119,13 +124,16 @@ static int dispatch_cgroups_array(const char *name, sd_json_variant *variant, sd
static int userns_registry_load(int dir_fd, const char *fn, UserNamespaceInfo **ret) {
static const sd_json_dispatch_field dispatch_table[] = {
- { "owner", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, owner), SD_JSON_MANDATORY },
- { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserNamespaceInfo, name), SD_JSON_MANDATORY },
- { "userns", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(UserNamespaceInfo, userns_inode), SD_JSON_MANDATORY },
- { "start", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start), 0 },
- { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint32, offsetof(UserNamespaceInfo, size), 0 },
- { "target", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target), 0 },
- { "cgroups", SD_JSON_VARIANT_ARRAY, dispatch_cgroups_array, 0, 0 },
+ { "owner", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, owner), SD_JSON_MANDATORY },
+ { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserNamespaceInfo, name), SD_JSON_MANDATORY },
+ { "userns", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(UserNamespaceInfo, userns_inode), SD_JSON_MANDATORY },
+ { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint32, offsetof(UserNamespaceInfo, size), 0 },
+ { "start", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start_uid), 0 },
+ { "target", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target_uid), 0 },
+ { "startGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start_gid), 0 },
+ { "targetGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target_gid), 0 },
+ { "cgroups", SD_JSON_VARIANT_ARRAY, dispatch_cgroups_array, 0, 0 },
+ { "netifs", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserNamespaceInfo, netifs), 0 },
{}
};
@@ -156,17 +164,32 @@ static int userns_registry_load(int dir_fd, const char *fn, UserNamespaceInfo **
if (userns_info->userns_inode == 0)
return -EBADMSG;
- if (userns_info->start == 0)
+ if (userns_info->start_uid == 0 || userns_info->start_gid == 0)
return -EBADMSG;
+
if (userns_info->size == 0) {
- if (uid_is_valid(userns_info->start) || uid_is_valid(userns_info->target))
- return -EBADMSG;
- } else {
- if (!uid_is_valid(userns_info->start) || !uid_is_valid(userns_info->target))
+ if (uid_is_valid(userns_info->start_uid) || uid_is_valid(userns_info->target_uid))
return -EBADMSG;
- if (userns_info->size > UINT32_MAX - userns_info->start ||
- userns_info->size > UINT32_MAX - userns_info->target)
+ if (gid_is_valid(userns_info->start_gid) || gid_is_valid(userns_info->target_gid))
+ return -EBADMSG;
+ } else {
+ if (!uid_is_valid(userns_info->start_uid) || !uid_is_valid(userns_info->target_uid))
+ return -EBADMSG;
+
+ if (userns_info->size > UINT32_MAX - userns_info->start_uid ||
+ userns_info->size > UINT32_MAX - userns_info->target_uid)
+ return -EBADMSG;
+
+ /* Older versions of the registry didn't maintain UID/GID separately, hence copy over if not
+ * set */
+ if (!gid_is_valid(userns_info->start_gid))
+ userns_info->start_gid = userns_info->start_uid;
+ if (!gid_is_valid(userns_info->target_gid))
+ userns_info->target_gid = userns_info->target_gid;
+
+ if (userns_info->size > UINT32_MAX - userns_info->start_gid ||
+ userns_info->size > UINT32_MAX - userns_info->target_gid)
return -EBADMSG;
}
@@ -195,6 +218,26 @@ int userns_registry_uid_exists(int dir_fd, uid_t start) {
return true;
}
+int userns_registry_gid_exists(int dir_fd, gid_t start) {
+ _cleanup_free_ char *fn = NULL;
+
+ assert(dir_fd >= 0);
+
+ if (!gid_is_valid(start))
+ return -ENOENT;
+
+ if (start == 0)
+ return true;
+
+ if (asprintf(&fn, "g" GID_FMT ".userns", start) < 0)
+ return -ENOMEM;
+
+ if (faccessat(dir_fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0)
+ return errno == ENOENT ? false : -errno;
+
+ return true;
+}
+
int userns_registry_name_exists(int dir_fd, const char *name) {
_cleanup_free_ char *fn = NULL;
@@ -254,7 +297,40 @@ int userns_registry_load_by_start_uid(int dir_fd, uid_t start, UserNamespaceInfo
if (r < 0)
return r;
- if (userns_info->start != start)
+ if (userns_info->start_uid != start)
+ return -EBADMSG;
+
+ if (ret)
+ *ret = TAKE_PTR(userns_info);
+
+ return 0;
+}
+
+int userns_registry_load_by_start_gid(int dir_fd, gid_t start, UserNamespaceInfo **ret) {
+ _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL;
+ _cleanup_close_ int registry_fd = -EBADF;
+ _cleanup_free_ char *fn = NULL;
+ int r;
+
+ if (!gid_is_valid(start))
+ return -ENOENT;
+
+ if (dir_fd < 0) {
+ registry_fd = userns_registry_open_fd();
+ if (registry_fd < 0)
+ return registry_fd;
+
+ dir_fd = registry_fd;
+ }
+
+ if (asprintf(&fn, "g" GID_FMT ".userns", start) < 0)
+ return -ENOMEM;
+
+ r = userns_registry_load(dir_fd, fn, &userns_info);
+ if (r < 0)
+ return r;
+
+ if (userns_info->start_gid != start)
return -EBADMSG;
if (ret)
@@ -366,10 +442,13 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) {
SD_JSON_BUILD_PAIR("owner", SD_JSON_BUILD_UNSIGNED(info->owner)),
SD_JSON_BUILD_PAIR("name", SD_JSON_BUILD_STRING(info->name)),
SD_JSON_BUILD_PAIR("userns", SD_JSON_BUILD_UNSIGNED(info->userns_inode)),
- SD_JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->start), "start", SD_JSON_BUILD_UNSIGNED(info->start)),
- SD_JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->start), "size", SD_JSON_BUILD_UNSIGNED(info->size)),
- SD_JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->start), "target", SD_JSON_BUILD_UNSIGNED(info->target)),
- SD_JSON_BUILD_PAIR_CONDITION(!!cgroup_array, "cgroups", SD_JSON_BUILD_VARIANT(cgroup_array)));
+ SD_JSON_BUILD_PAIR_CONDITION(info->size > 0, "size", SD_JSON_BUILD_UNSIGNED(info->size)),
+ SD_JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->start_uid), "start", SD_JSON_BUILD_UNSIGNED(info->start_uid)),
+ SD_JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->target_uid), "target", SD_JSON_BUILD_UNSIGNED(info->target_uid)),
+ SD_JSON_BUILD_PAIR_CONDITION(gid_is_valid(info->start_gid), "startGid", SD_JSON_BUILD_UNSIGNED(info->start_gid)),
+ SD_JSON_BUILD_PAIR_CONDITION(gid_is_valid(info->target_gid), "targetGid", SD_JSON_BUILD_UNSIGNED(info->target_gid)),
+ SD_JSON_BUILD_PAIR_CONDITION(!!cgroup_array, "cgroups", SD_JSON_BUILD_VARIANT(cgroup_array)),
+ JSON_BUILD_PAIR_STRV_NON_EMPTY("netifs", info->netifs));
if (r < 0)
return r;
@@ -378,7 +457,7 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) {
if (r < 0)
return log_debug_errno(r, "Failed to format userns JSON object: %m");
- _cleanup_free_ char *reg_fn = NULL, *link1_fn = NULL, *link2_fn = NULL, *owner_fn = NULL, *uid_fn = NULL;
+ _cleanup_free_ char *reg_fn = NULL, *link1_fn = NULL, *link2_fn = NULL, *link3_fn = NULL, *owner_fn = NULL, *uid_fn = NULL;
if (asprintf(®_fn, "i%" PRIu64 ".userns", info->userns_inode) < 0)
return log_oom_debug();
@@ -398,8 +477,8 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) {
goto fail;
}
- if (uid_is_valid(info->start)) {
- if (asprintf(&link2_fn, "u" UID_FMT ".userns", info->start) < 0) {
+ if (uid_is_valid(info->start_uid)) {
+ if (asprintf(&link2_fn, "u" UID_FMT ".userns", info->start_uid) < 0) {
r = log_oom_debug();
goto fail;
}
@@ -411,6 +490,19 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) {
}
}
+ if (gid_is_valid(info->start_gid)) {
+ if (asprintf(&link3_fn, "g" GID_FMT ".userns", info->start_gid) < 0) {
+ r = log_oom_debug();
+ goto fail;
+ }
+
+ r = linkat_replace(dir_fd, reg_fn, dir_fd, link3_fn);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to link userns data to '%s' in registry: %m", link3_fn);
+ goto fail;
+ }
+ }
+
if (asprintf(&uid_fn, "o" UID_FMT ".owns", info->owner) < 0) {
r = log_oom_debug();
goto fail;
@@ -441,6 +533,8 @@ fail:
(void) unlinkat(dir_fd, link1_fn, /* flags= */ 0);
if (link2_fn)
(void) unlinkat(dir_fd, link2_fn, /* flags= */ 0);
+ if (link3_fn)
+ (void) unlinkat(dir_fd, link3_fn, /* flags= */ 0);
if (owner_fn)
(void) unlinkat(dir_fd, owner_fn, /* flags= */ 0);
if (uid_fn)
@@ -476,15 +570,24 @@ int userns_registry_remove(int dir_fd, UserNamespaceInfo *info) {
RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link1_fn, 0)));
- if (uid_is_valid(info->start)) {
+ if (uid_is_valid(info->start_uid)) {
_cleanup_free_ char *link2_fn = NULL;
- if (asprintf(&link2_fn, "u" UID_FMT ".userns", info->start) < 0)
+ if (asprintf(&link2_fn, "u" UID_FMT ".userns", info->start_uid) < 0)
return log_oom_debug();
RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link2_fn, 0)));
}
+ if (uid_is_valid(info->start_gid)) {
+ _cleanup_free_ char *link3_fn = NULL;
+
+ if (asprintf(&link3_fn, "g" GID_FMT ".userns", info->start_gid) < 0)
+ return log_oom_debug();
+
+ RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link3_fn, 0)));
+ }
+
_cleanup_free_ char *uid_fn = NULL;
if (asprintf(&uid_fn, "o" UID_FMT ".owns", info->owner) < 0)
return log_oom_debug();
@@ -513,6 +616,7 @@ bool userns_info_has_cgroup(UserNamespaceInfo *userns, uint64_t cgroup_id) {
}
int userns_info_add_cgroup(UserNamespaceInfo *userns, uint64_t cgroup_id) {
+ assert(userns);
if (userns_info_has_cgroup(userns, cgroup_id))
return 0;
@@ -588,6 +692,67 @@ int userns_info_remove_cgroups(UserNamespaceInfo *userns) {
return ret;
}
+int userns_info_add_netif(UserNamespaceInfo *userns, const char *netif) {
+ int r;
+
+ assert(userns);
+ assert(netif);
+
+ if (strv_contains(userns->netifs, netif))
+ return 0;
+
+ r = strv_extend(&userns->netifs, netif);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+static int userns_destroy_netif(sd_netlink **rtnl, const char *name) {
+ int r;
+
+ assert(rtnl);
+ assert(name);
+
+ log_debug("Removing delegated network interface '%s'", name);
+
+ if (!*rtnl) {
+ r = sd_netlink_open(rtnl);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to connect to netlink: %m");
+ }
+
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
+ r = sd_rtnl_message_new_link(*rtnl, &m, RTM_DELLINK, /* ifindex= */ 0);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_message_append_string(m, IFLA_IFNAME, name);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(*rtnl, m, /* timeout_usec= */ 0, /* ret_reply= */ NULL);
+ if (ERRNO_IS_NEG_DEVICE_ABSENT(r)) /* Already gone? */
+ return 0;
+ if (r < 0)
+ return log_debug_errno(r, "Failed to remove interface %s: %m", name);
+
+ return 1;
+}
+
+int userns_info_remove_netifs(UserNamespaceInfo *userns) {
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ int ret = 0;
+
+ assert(userns);
+
+ STRV_FOREACH(c, userns->netifs)
+ RET_GATHER(ret, userns_destroy_netif(&rtnl, *c));
+
+ userns->netifs = strv_free(userns->netifs);
+ return ret;
+}
+
bool userns_name_is_valid(const char *name) {
/* Checks if the specified string is suitable as user namespace name. */
diff --git a/src/nsresourced/userns-registry.h b/src/nsresourced/userns-registry.h
index 9e66a6feec..72fe990079 100644
--- a/src/nsresourced/userns-registry.h
+++ b/src/nsresourced/userns-registry.h
@@ -1,17 +1,23 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
-#define USER_NAMESPACE_CGROUPS_DELEGATE_MAX 16
+#include "macro.h"
+
+#define USER_NAMESPACE_CGROUPS_DELEGATE_MAX 16U
+#define USER_NAMESPACE_NETIFS_DELEGATE_MAX 16U
typedef struct UserNamespaceInfo {
uid_t owner;
char *name;
uint64_t userns_inode;
- uid_t start;
uint32_t size;
- uid_t target;
+ uid_t start_uid;
+ uid_t target_uid;
+ gid_t start_gid;
+ gid_t target_gid;
uint64_t *cgroups;
size_t n_cgroups;
+ char **netifs;
} UserNamespaceInfo;
UserNamespaceInfo* userns_info_new(void);
@@ -23,12 +29,16 @@ bool userns_info_has_cgroup(UserNamespaceInfo *userns, uint64_t cgroup_id);
int userns_info_add_cgroup(UserNamespaceInfo *userns, uint64_t cgroup_id);
int userns_info_remove_cgroups(UserNamespaceInfo *userns);
+int userns_info_add_netif(UserNamespaceInfo *userns, const char *netif);
+int userns_info_remove_netifs(UserNamespaceInfo *userns);
+
bool userns_name_is_valid(const char *name);
int userns_registry_open_fd(void);
int userns_registry_lock(int dir_fd);
int userns_registry_load_by_start_uid(int dir_fd, uid_t start, UserNamespaceInfo **ret);
+int userns_registry_load_by_start_gid(int dir_fd, gid_t start, UserNamespaceInfo **ret);
int userns_registry_load_by_userns_inode(int dir_fd, uint64_t userns, UserNamespaceInfo **ret);
int userns_registry_load_by_name(int dir_fd, const char *name, UserNamespaceInfo **ret);
@@ -38,5 +48,6 @@ int userns_registry_remove(int dir_fd, UserNamespaceInfo *info);
int userns_registry_inode_exists(int dir_fd, uint64_t inode);
int userns_registry_name_exists(int dir_fd, const char *name);
int userns_registry_uid_exists(int dir_fd, uid_t start);
+int userns_registry_gid_exists(int dir_fd, gid_t start);
int userns_registry_per_uid(int dir_fd, uid_t owner);
diff --git a/src/shared/mount-util.c b/src/shared/mount-util.c
index 97ff6d0764..d7471bc78d 100644
--- a/src/shared/mount-util.c
+++ b/src/shared/mount-util.c
@@ -1442,7 +1442,7 @@ int make_userns(uid_t uid_shift,
}
/* We always assign the same UID and GID ranges */
- userns_fd = userns_acquire(line, line);
+ userns_fd = userns_acquire(line, line, /* setgroups_deny= */ true);
if (userns_fd < 0)
return log_debug_errno(userns_fd, "Failed to acquire new userns: %m");
diff --git a/src/shared/nsresource.c b/src/shared/nsresource.c
index 5c6792507e..0255a6860c 100644
--- a/src/shared/nsresource.c
+++ b/src/shared/nsresource.c
@@ -144,6 +144,8 @@ int nsresource_register_userns(const char *name, int userns_fd) {
SD_JSON_BUILD_PAIR("userNamespaceFileDescriptor", SD_JSON_BUILD_UNSIGNED(userns_fd_idx)));
if (r < 0)
return log_debug_errno(r, "Failed to call RegisterUserNamespace() varlink call: %m");
+ if (streq_ptr(error_id, "io.systemd.NamespaceResource.UserNamespaceInterfaceNotSupported"))
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unprivileged user namespace delegation is not supported on this system.");
if (error_id)
return log_debug_errno(sd_varlink_error_to_errno(error_id, reply), "Failed to register user namespace: %s", error_id);
@@ -257,6 +259,7 @@ int nsresource_add_cgroup(int userns_fd, int cgroup_fd) {
typedef struct InterfaceParams {
char *host_interface_name;
char *namespace_interface_name;
+ unsigned interface_fd_index;
} InterfaceParams;
static void interface_params_done(InterfaceParams *p) {
@@ -266,7 +269,7 @@ static void interface_params_done(InterfaceParams *p) {
free(p->namespace_interface_name);
}
-int nsresource_add_netif(
+int nsresource_add_netif_veth(
int userns_fd,
int netns_fd,
const char *namespace_ifname,
@@ -330,8 +333,8 @@ int nsresource_add_netif(
return log_debug_errno(sd_varlink_error_to_errno(error_id, reply), "Failed to add network to user namespace: %s", error_id);
static const sd_json_dispatch_field dispatch_table[] = {
- { "hostInterfaceName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(InterfaceParams, host_interface_name), 0 },
- { "namespaceInterfaceName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(InterfaceParams, namespace_interface_name), 0 },
+ { "hostInterfaceName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(InterfaceParams, host_interface_name), SD_JSON_MANDATORY },
+ { "namespaceInterfaceName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(InterfaceParams, namespace_interface_name), SD_JSON_MANDATORY },
};
_cleanup_(interface_params_done) InterfaceParams p = {};
@@ -346,3 +349,69 @@ int nsresource_add_netif(
return 1;
}
+
+int nsresource_add_netif_tap(
+ int userns_fd,
+ char **ret_host_ifname) {
+
+ _cleanup_close_ int _userns_fd = -EBADF;
+ _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
+ int r, userns_fd_idx;
+ const char *error_id;
+
+ if (userns_fd < 0) {
+ _userns_fd = namespace_open_by_type(NAMESPACE_USER);
+ if (_userns_fd < 0)
+ return _userns_fd;
+
+ userns_fd = _userns_fd;
+ }
+
+ r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.NamespaceResource");
+ if (r < 0)
+ return log_debug_errno(r, "Failed to connect to namespace resource manager: %m");
+
+ r = sd_varlink_set_allow_fd_passing_output(vl, true);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to enable varlink fd passing for write: %m");
+
+ r = sd_varlink_set_allow_fd_passing_input(vl, true);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to enable varlink fd passing for read: %m");
+
+ userns_fd_idx = sd_varlink_push_dup_fd(vl, userns_fd);
+ if (userns_fd_idx < 0)
+ return log_debug_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m");
+
+ sd_json_variant *reply = NULL;
+ r = sd_varlink_callbo(
+ vl,
+ "io.systemd.NamespaceResource.AddNetworkToUserNamespace",
+ &reply,
+ &error_id,
+ SD_JSON_BUILD_PAIR("userNamespaceFileDescriptor", SD_JSON_BUILD_UNSIGNED(userns_fd_idx)),
+ SD_JSON_BUILD_PAIR("mode", JSON_BUILD_CONST_STRING("tap")));
+ if (r < 0)
+ return log_debug_errno(r, "Failed to call AddNetworkToUserNamespace() varlink call: %m");
+ if (error_id)
+ return log_debug_errno(sd_varlink_error_to_errno(error_id, reply), "Failed to add network to user namespace: %s", error_id);
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "hostInterfaceName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(InterfaceParams, host_interface_name), SD_JSON_MANDATORY },
+ { "interfaceFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(InterfaceParams, namespace_interface_name), SD_JSON_MANDATORY },
+ };
+
+ _cleanup_(interface_params_done) InterfaceParams p = {};
+ r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
+ if (r < 0)
+ return r;
+
+ _cleanup_close_ int tap_fd = sd_varlink_take_fd(vl, p.interface_fd_index);
+ if (tap_fd < 0)
+ return tap_fd;
+
+ if (ret_host_ifname)
+ *ret_host_ifname = TAKE_PTR(p.host_interface_name);
+
+ return TAKE_FD(tap_fd);
+}
diff --git a/src/shared/nsresource.h b/src/shared/nsresource.h
index 6b807b327b..48274fd1a5 100644
--- a/src/shared/nsresource.h
+++ b/src/shared/nsresource.h
@@ -7,4 +7,5 @@ int nsresource_allocate_userns(const char *name, uint64_t size);
int nsresource_register_userns(const char *name, int userns_fd);
int nsresource_add_mount(int userns_fd, int mount_fd);
int nsresource_add_cgroup(int userns_fd, int cgroup_fd);
-int nsresource_add_netif(int userns_fd, int netns_fd, const char *namespace_ifname, char **ret_host_ifname, char **ret_namespace_ifname);
+int nsresource_add_netif_veth(int userns_fd, int netns_fd, const char *namespace_ifname, char **ret_host_ifname, char **ret_namespace_ifname);
+int nsresource_add_netif_tap(int userns_fd, char **ret_host_ifname);
diff --git a/src/shared/varlink-io.systemd.NamespaceResource.c b/src/shared/varlink-io.systemd.NamespaceResource.c
index 145d706c1b..03bfc41134 100644
--- a/src/shared/varlink-io.systemd.NamespaceResource.c
+++ b/src/shared/varlink-io.systemd.NamespaceResource.c
@@ -4,24 +4,24 @@
static SD_VARLINK_DEFINE_METHOD(
AllocateUserRange,
- SD_VARLINK_FIELD_COMMENT("The name for the user namespace, a short string that must be fit to be included in a file name and in a user name"),
+ SD_VARLINK_FIELD_COMMENT("The name for the user namespace, a short string that must be fit to be included in a file name and in a user name. This name is included in the user records announced via NSS and is otherwise useful for debugging."),
SD_VARLINK_DEFINE_INPUT(name, SD_VARLINK_STRING, 0),
SD_VARLINK_FIELD_COMMENT("Controls whether to mangle the provided name if needed so that it is suitable for naming a user namespace. If true this will shorten the name as necessary or randomize it if that's not sufficient. If null defaults to false."),
SD_VARLINK_DEFINE_INPUT(mangleName, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
- SD_VARLINK_FIELD_COMMENT("The number of UIDs to assign. Must be 1 or 65536"),
+ SD_VARLINK_FIELD_COMMENT("The number of UIDs to assign. Must be 1 or 65536."),
SD_VARLINK_DEFINE_INPUT(size, SD_VARLINK_INT, 0),
- SD_VARLINK_FIELD_COMMENT("The target UID inside the user namespace"),
+ SD_VARLINK_FIELD_COMMENT("The target UID inside the user namespace. If not specified defaults to 0."),
SD_VARLINK_DEFINE_INPUT(target, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("A file descriptor to an allocated userns with no current UID range assignments"),
SD_VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, SD_VARLINK_INT, 0),
- SD_VARLINK_FIELD_COMMENT("The name assigned to the user namespace"),
+ SD_VARLINK_FIELD_COMMENT("The name assigned to the user namespace. (This is particularly interesting in case mangleName was enabled)."),
SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_METHOD(
RegisterUserNamespace,
- SD_VARLINK_FIELD_COMMENT("The name for the user namespce, a short string that must be fit to be included in a file name and in a user name"),
+ SD_VARLINK_FIELD_COMMENT("The name for the user namespace, a short string that must be fit to be included in a file name and in a user name, as in AllocateUserRange()."),
SD_VARLINK_DEFINE_INPUT(name, SD_VARLINK_STRING, 0),
- SD_VARLINK_FIELD_COMMENT("Controls whether to mangle the provided name if needed so that it is suitable for naming a user namespace. If true this will shorten the name as necessary or randomize it if that's not sufficient. If null defaults to false."),
+ SD_VARLINK_FIELD_COMMENT("Controls whether to mangle the provided name if needed so that it is suitable for naming a user namespace, as in AllocateUserRange()"),
SD_VARLINK_DEFINE_INPUT(mangleName, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("A user namespace file descriptor that is fully initialized"),
SD_VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, SD_VARLINK_INT, 0),
@@ -30,22 +30,34 @@ static SD_VARLINK_DEFINE_METHOD(
static SD_VARLINK_DEFINE_METHOD(
AddMountToUserNamespace,
+ SD_VARLINK_FIELD_COMMENT("A user namespace file descriptor previously allocated via AllocateUserRange() or registered via RegisterUserNamespace()."),
SD_VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, SD_VARLINK_INT, 0),
+ SD_VARLINK_FIELD_COMMENT("A mount file descriptor for the mount to allowlist for the specified user namespace."),
SD_VARLINK_DEFINE_INPUT(mountFileDescriptor, SD_VARLINK_INT, 0));
static SD_VARLINK_DEFINE_METHOD(
AddControlGroupToUserNamespace,
+ SD_VARLINK_FIELD_COMMENT("A user namespace file descriptor previously allocated via AllocateUserRange() or registered via RegisterUserNamespace()."),
SD_VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, SD_VARLINK_INT, 0),
+ SD_VARLINK_FIELD_COMMENT("A file descriptor referencing a cgroup to assign to the root user of the specified user namespace."),
SD_VARLINK_DEFINE_INPUT(controlGroupFileDescriptor, SD_VARLINK_INT, 0));
static SD_VARLINK_DEFINE_METHOD(
AddNetworkToUserNamespace,
+ SD_VARLINK_FIELD_COMMENT("A user namespace file descriptor previously allocated via AllocateUserRange() or registered via RegisterUserNamespace()."),
SD_VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, SD_VARLINK_INT, 0),
- SD_VARLINK_DEFINE_INPUT(networkNamespaceFileDescriptor, SD_VARLINK_INT, 0),
+ SD_VARLINK_FIELD_COMMENT("A network namespace file descriptor to assign the network interface to. Only applies to network interfaces of type 'veth'."),
+ SD_VARLINK_DEFINE_INPUT(networkNamespaceFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The network interface name to use for the network interface inside the network namespace. Only applies to network interfaces of type 'veth'."),
SD_VARLINK_DEFINE_INPUT(namespaceInterfaceName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The networking mode, one of 'veth' or 'tap'. If the former is selected a virtual Ethernet link between host and namespace is created. If the latter is selected an Ethernet Tap link is created and a file descriptor is returned for the namespace side."),
SD_VARLINK_DEFINE_INPUT(mode, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("The chosen network interface name for the host side of the network."),
SD_VARLINK_DEFINE_OUTPUT(hostInterfaceName, SD_VARLINK_STRING, 0),
- SD_VARLINK_DEFINE_OUTPUT(namespaceInterfaceName, SD_VARLINK_STRING, 0));
+ SD_VARLINK_FIELD_COMMENT("The chosen network interface name for the namespace side of the network. Only applies to network interfaces of type 'veth'."),
+ SD_VARLINK_DEFINE_OUTPUT(namespaceInterfaceName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The file descriptor for the namespace side of the network. Only applies to network interfaces of type 'tap'."),
+ SD_VARLINK_DEFINE_OUTPUT(interfaceFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_ERROR(UserNamespaceInterfaceNotSupported);
static SD_VARLINK_DEFINE_ERROR(NameExists);
@@ -56,21 +68,39 @@ static SD_VARLINK_DEFINE_ERROR(UserNamespaceNotRegistered);
static SD_VARLINK_DEFINE_ERROR(UserNamespaceWithoutUserRange);
static SD_VARLINK_DEFINE_ERROR(TooManyControlGroups);
static SD_VARLINK_DEFINE_ERROR(ControlGroupAlreadyAdded);
+static SD_VARLINK_DEFINE_ERROR(TooManyNetworkInterfaces);
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_NamespaceResource,
"io.systemd.NamespaceResource",
+ SD_VARLINK_INTERFACE_COMMENT("Allocate transient UID ranges for user namespace, and assign mounts, cgroups and networking devices to them"),
+ SD_VARLINK_SYMBOL_COMMENT("Assigns a UID range to a client-allocated user namespace that has no UID range assigned so far, and registers it for assignment of other resources."),
&vl_method_AllocateUserRange,
+ SD_VARLINK_SYMBOL_COMMENT("Registers an already initialized user namespace for assignment of resources."),
&vl_method_RegisterUserNamespace,
+ SD_VARLINK_SYMBOL_COMMENT("Adds a mount to a user namespace previously allocated or registered via AllocateUserRange() or RegisterUserNamespace(). This allowlists the mount for access by the user namespace."),
&vl_method_AddMountToUserNamespace,
+ SD_VARLINK_SYMBOL_COMMENT("Adds a cgroup to a user namespace previously allocated or registered via AllocateUserRange() or RegisterUserNamespace(). This passes ownership of the referenced cgroup to the user namespace's root user, and ensures the cgroup gets deleted when the user namespace is released."),
&vl_method_AddControlGroupToUserNamespace,
+ SD_VARLINK_SYMBOL_COMMENT("Adds a network interface to a user namespace previously allocated or registered via AllocateUserRange() or RegisterUserNamespace(). This creates a 'veth' or 'tap' device that is assigned to the user namespace, or owned by its root user."),
&vl_method_AddNetworkToUserNamespace,
+ SD_VARLINK_SYMBOL_COMMENT("User namespace delegation via io.systemd.NamespaceResource is not supported."),
&vl_error_UserNamespaceInterfaceNotSupported,
+ SD_VARLINK_SYMBOL_COMMENT("A user namespace under the specified name exists already."),
&vl_error_NameExists,
+ SD_VARLINK_SYMBOL_COMMENT("The specified user namespace has been registered before."),
&vl_error_UserNamespaceExists,
+ SD_VARLINK_SYMBOL_COMMENT("The dynamic UID range is exhausted already."),
&vl_error_DynamicRangeUnavailable,
+ SD_VARLINK_SYMBOL_COMMENT("The dynamic UID range is not available on this system."),
&vl_error_NoDynamicRange,
+ SD_VARLINK_SYMBOL_COMMENT("The specified user namespace has not been registered."),
&vl_error_UserNamespaceNotRegistered,
+ SD_VARLINK_SYMBOL_COMMENT("The specified user namespace has no UID range assigned."),
&vl_error_UserNamespaceWithoutUserRange,
+ SD_VARLINK_SYMBOL_COMMENT("The per-user namespace limit of cgroups has been reached."),
&vl_error_TooManyControlGroups,
- &vl_error_ControlGroupAlreadyAdded);
+ SD_VARLINK_SYMBOL_COMMENT("The specified cgroup has already been added to the user namespace."),
+ &vl_error_ControlGroupAlreadyAdded,
+ SD_VARLINK_SYMBOL_COMMENT("The per-user namespace limit of network interfaces has been reached."),
+ &vl_error_TooManyNetworkInterfaces);
diff --git a/src/test/meson.build b/src/test/meson.build
index e86a28f876..14d813fee1 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -132,6 +132,7 @@ simple_tests += files(
'test-modhex.c',
'test-mountpoint-util.c',
'test-net-naming-scheme.c',
+ 'test-nsresource.c',
'test-nulstr-util.c',
'test-open-file.c',
'test-ordered-set.c',
diff --git a/src/test/test-namespace.c b/src/test/test-namespace.c
index cae65a14b1..52049efdb8 100644
--- a/src/test/test-namespace.c
+++ b/src/test/test-namespace.c
@@ -258,7 +258,7 @@ TEST(namespace_is_init) {
TEST(userns_get_base_uid) {
_cleanup_close_ int fd = -EBADF;
- fd = userns_acquire("0 1 1", "0 2 1");
+ fd = userns_acquire("0 1 1", "0 2 1", /* setgroups_deny= */ true);
if (ERRNO_IS_NEG_NOT_SUPPORTED(fd))
return (void) log_tests_skipped("userns is not supported");
if (ERRNO_IS_NEG_PRIVILEGE(fd))
diff --git a/src/test/test-nsresource.c b/src/test/test-nsresource.c
new file mode 100644
index 0000000000..804b63f8ee
--- /dev/null
+++ b/src/test/test-nsresource.c
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "fd-util.h"
+#include "namespace-util.h"
+#include "nsresource.h"
+#include "tests.h"
+
+TEST(delegatetap) {
+ int r;
+
+ _cleanup_close_ int userns_fd = userns_acquire_self_root();
+ if (ERRNO_IS_NEG_PRIVILEGE(userns_fd) || ERRNO_IS_NEG_NOT_SUPPORTED(userns_fd))
+ return (void) log_tests_skipped_errno(userns_fd, "User namespaces not available");
+ ASSERT_OK(userns_fd);
+
+ r = nsresource_register_userns("foobar", userns_fd);
+ if (ERRNO_IS_NEG_DISCONNECT(r) || r == -ENOENT || ERRNO_IS_NEG_NOT_SUPPORTED(r))
+ return (void) log_tests_skipped_errno(r, "systemd-nsresourced cannot be reached");
+ ASSERT_OK(r);
+
+ _cleanup_free_ char *ifname = NULL;
+ _cleanup_close_ int tap_fd = nsresource_add_netif_tap(userns_fd, &ifname);
+ if (ERRNO_IS_NEG_NOT_SUPPORTED(tap_fd))
+ return (void) log_tests_skipped_errno(tap_fd, "tap device support not available");
+ ASSERT_OK(tap_fd);
+
+ ASSERT_GE(if_nametoindex(ifname), 2U);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c
index 5fc24ea1c1..849dd617dc 100644
--- a/src/vmspawn/vmspawn.c
+++ b/src/vmspawn/vmspawn.c
@@ -21,6 +21,7 @@
#include "bus-internal.h"
#include "bus-locator.h"
#include "bus-wait-for-jobs.h"
+#include "capability-util.h"
#include "chase.h"
#include "common-signal.h"
#include "copy.h"
@@ -49,6 +50,7 @@
#include "mkdir.h"
#include "namespace-util.h"
#include "netif-util.h"
+#include "nsresource.h"
#include "osc-context.h"
#include "pager.h"
#include "parse-argument.h"
@@ -1645,30 +1647,62 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
}
}
+ _cleanup_close_ int delegate_userns_fd = -EBADF, tap_fd = -EBADF;
if (arg_network_stack == NETWORK_STACK_TAP) {
- _cleanup_free_ char *tap_name = NULL;
- struct ether_addr mac_vm = {};
+ if (have_effective_cap(CAP_NET_ADMIN) <= 0) {
+ delegate_userns_fd = userns_acquire_self_root();
+ if (delegate_userns_fd < 0)
+ return log_error_errno(delegate_userns_fd, "Failed to acquire userns: %m");
- tap_name = strjoin("vt-", arg_machine);
- if (!tap_name)
- return log_oom();
+ _cleanup_free_ char *userns_name = NULL;
+ if (asprintf(&userns_name, "vmspawn-" PID_FMT "-%s", getpid_cached(), arg_machine) < 0)
+ return log_oom();
- (void) net_shorten_ifname(tap_name, /* check_naming_scheme= */ false);
-
- if (ether_addr_is_null(&arg_network_provided_mac)){
- r = net_generate_mac(arg_machine, &mac_vm, VM_TAP_HASH_KEY, 0);
+ r = nsresource_register_userns(userns_name, delegate_userns_fd);
if (r < 0)
- return log_error_errno(r, "Failed to generate predictable MAC address for VM side: %m");
- } else
- mac_vm = arg_network_provided_mac;
+ return log_error_errno(r, "Failed to register user namespace with systemd-nsresourced: %m");
- r = strv_extend(&cmdline, "-nic");
- if (r < 0)
- return log_oom();
+ tap_fd = nsresource_add_netif_tap(delegate_userns_fd, /* ret_host_ifname= */ NULL);
+ if (tap_fd < 0)
+ return log_error_errno(tap_fd, "Failed to allocate network tap device: %m");
- r = strv_extendf(&cmdline, "tap,ifname=%s,script=no,downscript=no,model=virtio-net-pci,mac=%s", tap_name, ETHER_ADDR_TO_STR(&mac_vm));
- if (r < 0)
- return log_oom();
+ r = strv_extend(&cmdline, "-nic");
+ if (r < 0)
+ return log_oom();
+
+ r = strv_extendf(&cmdline, "tap,fd=%i,model=virtio-net-pci", tap_fd);
+ if (r < 0)
+ return log_oom();
+
+ if (!GREEDY_REALLOC(pass_fds, n_pass_fds + 1))
+ return log_oom();
+
+ pass_fds[n_pass_fds++] = tap_fd;
+ } else {
+ _cleanup_free_ char *tap_name = NULL;
+ struct ether_addr mac_vm = {};
+
+ tap_name = strjoin("vt-", arg_machine);
+ if (!tap_name)
+ return log_oom();
+
+ (void) net_shorten_ifname(tap_name, /* check_naming_scheme= */ false);
+
+ if (ether_addr_is_null(&arg_network_provided_mac)){
+ r = net_generate_mac(arg_machine, &mac_vm, VM_TAP_HASH_KEY, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate predictable MAC address for VM side: %m");
+ } else
+ mac_vm = arg_network_provided_mac;
+
+ r = strv_extend(&cmdline, "-nic");
+ if (r < 0)
+ return log_oom();
+
+ r = strv_extendf(&cmdline, "tap,ifname=%s,script=no,downscript=no,model=virtio-net-pci,mac=%s", tap_name, ETHER_ADDR_TO_STR(&mac_vm));
+ if (r < 0)
+ return log_oom();
+ }
} else if (arg_network_stack == NETWORK_STACK_USER)
r = strv_extend_many(&cmdline, "-nic", "user,model=virtio-net-pci");
else
@@ -2282,8 +2316,9 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
_exit(EXIT_FAILURE);
}
- /* Close the vsock fd we passed to qemu in the parent. We don't need it anymore. */
+ /* Close relevant fds we passed to qemu in the parent. We don't need them anymore. */
child_vsock_fd = safe_close(child_vsock_fd);
+ tap_fd = safe_close(tap_fd);
int exit_status = INT_MAX;
if (use_vsock) {
@@ -2424,9 +2459,6 @@ static int determine_names(void) {
}
static int verify_arguments(void) {
- if (arg_network_stack == NETWORK_STACK_TAP && !arg_privileged)
- return log_error_errno(SYNTHETIC_ERRNO(EPERM), "--network-tap requires root privileges, refusing.");
-
if (!strv_isempty(arg_initrds) && !arg_linux)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --initrd= cannot be used without --linux=.");