/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * Copyright (C) 2016 Red Hat, Inc. */ #include "nm-glib-aux/nm-default-glib-i18n-lib.h" #include "nmp-netns.h" #include #include #include #include #include #include "nm-log-core/nm-logging.h" /*****************************************************************************/ /* NOTE: NMPNetns and all code used here must be thread-safe! */ /* we may not call logging functions from the main-thread alone. Hence, we * require locking from nm-logging. Indicate that by setting NM_THREAD_SAFE_ON_MAIN_THREAD * to zero. */ #undef NM_THREAD_SAFE_ON_MAIN_THREAD #define NM_THREAD_SAFE_ON_MAIN_THREAD 0 /*****************************************************************************/ #define PROC_SELF_NS_MNT "/proc/self/ns/mnt" #define PROC_SELF_NS_NET "/proc/self/ns/net" #define _CLONE_NS_ALL ((int) (CLONE_NEWNS | CLONE_NEWNET)) #define _CLONE_NS_ALL_V CLONE_NEWNS, CLONE_NEWNET static NM_UTILS_FLAGS2STR_DEFINE(_clone_ns_to_str, int, NM_UTILS_FLAGS2STR(CLONE_NEWNS, "mnt"), NM_UTILS_FLAGS2STR(CLONE_NEWNET, "net"), ); static const char * __ns_types_to_str(int ns_types, int ns_types_already_set, char *buf, gsize len) { const char *b = buf; char bb[200]; nm_utils_strbuf_append_c(&buf, &len, '['); if (ns_types & ~ns_types_already_set) { nm_utils_strbuf_append_str( &buf, &len, _clone_ns_to_str(ns_types & ~ns_types_already_set, bb, sizeof(bb))); } if (ns_types & ns_types_already_set) { if (ns_types & ~ns_types_already_set) nm_utils_strbuf_append_c(&buf, &len, '/'); nm_utils_strbuf_append_str( &buf, &len, _clone_ns_to_str(ns_types & ns_types_already_set, bb, sizeof(bb))); } nm_utils_strbuf_append_c(&buf, &len, ']'); return b; } #define _ns_types_to_str(ns_types, ns_types_already_set, buf) \ __ns_types_to_str(ns_types, ns_types_already_set, buf, sizeof(buf)) /*****************************************************************************/ #define _NMLOG_DOMAIN LOGD_PLATFORM #define _NMLOG_PREFIX_NAME "netns" #define _NMLOG(level, netns, ...) \ G_STMT_START \ { \ NMLogLevel _level = (level); \ \ if (nm_logging_enabled(_level, _NMLOG_DOMAIN)) { \ NMPNetns *_netns = (netns); \ char _sbuf[20]; \ \ _nm_log(_level, \ _NMLOG_DOMAIN, \ 0, \ NULL, \ NULL, \ "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ _NMLOG_PREFIX_NAME, \ (_netns ? nm_sprintf_buf(_sbuf, "[%p]", _netns) \ : "") _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ } \ } \ G_STMT_END /*****************************************************************************/ NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_FD_NET, PROP_FD_MNT, ); typedef struct { int fd_net; int fd_mnt; } NMPNetnsPrivate; struct _NMPNetns { GObject parent; NMPNetnsPrivate _priv; }; struct _NMPNetnsClass { GObjectClass parent; }; G_DEFINE_TYPE(NMPNetns, nmp_netns, G_TYPE_OBJECT); #define NMP_NETNS_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMPNetns, NMP_IS_NETNS) /*****************************************************************************/ typedef struct { NMPNetns *netns; int count; int ns_types; } NetnsInfo; static void _stack_push(GArray *netns_stack, NMPNetns *netns, int ns_types); static NMPNetns *_netns_new(GError **error); /*****************************************************************************/ static NMPNetns * _netns_get(NetnsInfo *info) { nm_assert(!info || NMP_IS_NETNS(info->netns)); return info ? info->netns : NULL; } /*****************************************************************************/ static _nm_thread_local GArray *_netns_stack = NULL; static void _netns_stack_clear_cb(gpointer data) { NetnsInfo *info = data; nm_assert(NMP_IS_NETNS(info->netns)); g_object_unref(info->netns); } static GArray * _netns_stack_get_impl(void) { gs_unref_object NMPNetns *netns = NULL; gs_free_error GError *error = NULL; pthread_key_t key; GArray * s; s = g_array_new(FALSE, FALSE, sizeof(NetnsInfo)); g_array_set_clear_func(s, _netns_stack_clear_cb); _netns_stack = s; /* register a destructor function to cleanup the array. If we fail * to do so, we will leak NMPNetns instances (and their file descriptor) when the * thread exits. */ if (pthread_key_create(&key, (void (*)(void *)) g_array_unref) != 0) _LOGE(NULL, "failure to initialize thread-local storage"); else if (pthread_setspecific(key, s) != 0) _LOGE(NULL, "failure to set thread-local storage"); /* at the bottom of the stack we must try to create a netns instance * that we never pop. It's the base to which we need to return. */ netns = _netns_new(&error); if (!netns) { _LOGD(NULL, "failed to create initial netns: %s", error->message); return s; } /* we leak this instance inside the stack. */ _stack_push(s, netns, _CLONE_NS_ALL); return s; } #define _netns_stack_get() \ ({ \ GArray *_s = _netns_stack; \ \ if (G_UNLIKELY(!_s)) \ _s = _netns_stack_get_impl(); \ _s; \ }) /*****************************************************************************/ static NMPNetns * _stack_current_netns(GArray *netns_stack, int ns_types) { guint j; nm_assert(netns_stack && netns_stack->len > 0); /* we search the stack top-down to find the netns that has * all @ns_types set. */ for (j = netns_stack->len; ns_types && j >= 1;) { NetnsInfo *info; info = &g_array_index(netns_stack, NetnsInfo, --j); if (NM_FLAGS_ALL(info->ns_types, ns_types)) return info->netns; } g_return_val_if_reached(NULL); } static int _stack_current_ns_types(GArray *netns_stack, NMPNetns *netns, int ns_types) { const int ns_types_check[] = {_CLONE_NS_ALL_V}; guint i, j; int res = 0; nm_assert(netns); nm_assert(netns_stack && netns_stack->len > 0); /* we search the stack top-down to check which of @ns_types * are already set to @netns. */ for (j = netns_stack->len; ns_types && j >= 1;) { NetnsInfo *info; info = &g_array_index(netns_stack, NetnsInfo, --j); if (info->netns != netns) { ns_types = NM_FLAGS_UNSET(ns_types, info->ns_types); continue; } for (i = 0; i < G_N_ELEMENTS(ns_types_check); i++) { if (NM_FLAGS_ANY(ns_types, ns_types_check[i]) && NM_FLAGS_ANY(info->ns_types, ns_types_check[i])) { res = NM_FLAGS_SET(res, ns_types_check[i]); ns_types = NM_FLAGS_UNSET(ns_types, ns_types_check[i]); } } } return res; } static NetnsInfo * _stack_peek(GArray *netns_stack) { if (netns_stack->len > 0) return &g_array_index(netns_stack, NetnsInfo, (netns_stack->len - 1)); return NULL; } static NetnsInfo * _stack_bottom(GArray *netns_stack) { if (netns_stack->len > 0) return &g_array_index(netns_stack, NetnsInfo, 0); return NULL; } static void _stack_push(GArray *netns_stack, NMPNetns *netns, int ns_types) { NetnsInfo *info; nm_assert(netns_stack); nm_assert(NMP_IS_NETNS(netns)); nm_assert(NM_FLAGS_ANY(ns_types, _CLONE_NS_ALL)); nm_assert(!NM_FLAGS_ANY(ns_types, ~_CLONE_NS_ALL)); g_array_set_size(netns_stack, netns_stack->len + 1); info = &g_array_index(netns_stack, NetnsInfo, (netns_stack->len - 1)); *info = (NetnsInfo){ .netns = g_object_ref(netns), .ns_types = ns_types, .count = 1, }; } static void _stack_pop(GArray *netns_stack) { NetnsInfo *info; nm_assert(netns_stack); nm_assert(netns_stack->len > 1); info = &g_array_index(netns_stack, NetnsInfo, (netns_stack->len - 1)); nm_assert(NMP_IS_NETNS(info->netns)); nm_assert(info->count == 1); g_array_set_size(netns_stack, netns_stack->len - 1); } static guint _stack_size(GArray *netns_stack) { nm_assert(netns_stack); return netns_stack->len; } /*****************************************************************************/ static NMPNetns * _netns_new(GError **error) { NMPNetns *self; int fd_net, fd_mnt; int errsv; fd_net = open(PROC_SELF_NS_NET, O_RDONLY | O_CLOEXEC); if (fd_net == -1) { errsv = errno; g_set_error(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "Failed opening netns: %s", nm_strerror_native(errsv)); errno = errsv; return NULL; } fd_mnt = open(PROC_SELF_NS_MNT, O_RDONLY | O_CLOEXEC); if (fd_mnt == -1) { errsv = errno; g_set_error(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "Failed opening mntns: %s", nm_strerror_native(errsv)); nm_close(fd_net); errno = errsv; return NULL; } self = g_object_new(NMP_TYPE_NETNS, NMP_NETNS_FD_NET, fd_net, NMP_NETNS_FD_MNT, fd_mnt, NULL); _LOGD(self, "new netns (net:%d, mnt:%d)", fd_net, fd_mnt); return self; } static int _setns(NMPNetns *self, int type) { char buf[100]; int fd; NMPNetnsPrivate *priv = NMP_NETNS_GET_PRIVATE(self); nm_assert(NM_IN_SET(type, _CLONE_NS_ALL_V)); fd = (type == CLONE_NEWNET) ? priv->fd_net : priv->fd_mnt; _LOGt(self, "set netns(%s, %d)", _ns_types_to_str(type, 0, buf), fd); return setns(fd, type); } static gboolean _netns_switch_push(GArray *netns_stack, NMPNetns *self, int ns_types) { int errsv; if (NM_FLAGS_HAS(ns_types, CLONE_NEWNET) && !_stack_current_ns_types(netns_stack, self, CLONE_NEWNET) && _setns(self, CLONE_NEWNET) != 0) { errsv = errno; _LOGE(self, "failed to switch netns: %s", nm_strerror_native(errsv)); return FALSE; } if (NM_FLAGS_HAS(ns_types, CLONE_NEWNS) && !_stack_current_ns_types(netns_stack, self, CLONE_NEWNS) && _setns(self, CLONE_NEWNS) != 0) { errsv = errno; _LOGE(self, "failed to switch mntns: %s", nm_strerror_native(errsv)); /* try to fix the mess by returning to the previous netns. */ if (NM_FLAGS_HAS(ns_types, CLONE_NEWNET) && !_stack_current_ns_types(netns_stack, self, CLONE_NEWNET)) { self = _stack_current_netns(netns_stack, CLONE_NEWNET); if (self && _setns(self, CLONE_NEWNET) != 0) { errsv = errno; _LOGE(self, "failed to restore netns: %s", nm_strerror_native(errsv)); } } return FALSE; } return TRUE; } static gboolean _netns_switch_pop(GArray *netns_stack, NMPNetns *self, int ns_types) { int errsv; NMPNetns *current; int success = TRUE; if (NM_FLAGS_HAS(ns_types, CLONE_NEWNET) && (!self || !_stack_current_ns_types(netns_stack, self, CLONE_NEWNET))) { current = _stack_current_netns(netns_stack, CLONE_NEWNET); if (!current) { g_warn_if_reached(); success = FALSE; } else if (_setns(current, CLONE_NEWNET) != 0) { errsv = errno; _LOGE(self, "failed to switch netns: %s", nm_strerror_native(errsv)); success = FALSE; } } if (NM_FLAGS_HAS(ns_types, CLONE_NEWNS) && (!self || !_stack_current_ns_types(netns_stack, self, CLONE_NEWNS))) { current = _stack_current_netns(netns_stack, CLONE_NEWNS); if (!current) { g_warn_if_reached(); success = FALSE; } else if (_setns(current, CLONE_NEWNS) != 0) { errsv = errno; _LOGE(self, "failed to switch mntns: %s", nm_strerror_native(errsv)); success = FALSE; } } return success; } /*****************************************************************************/ int nmp_netns_get_fd_net(NMPNetns *self) { g_return_val_if_fail(NMP_IS_NETNS(self), 0); return NMP_NETNS_GET_PRIVATE(self)->fd_net; } int nmp_netns_get_fd_mnt(NMPNetns *self) { g_return_val_if_fail(NMP_IS_NETNS(self), 0); return NMP_NETNS_GET_PRIVATE(self)->fd_mnt; } /*****************************************************************************/ static gboolean _nmp_netns_push_type(NMPNetns *self, int ns_types) { GArray * netns_stack = _netns_stack_get(); NetnsInfo *info; char sbuf[100]; info = _stack_peek(netns_stack); g_return_val_if_fail(info, FALSE); if (info->netns == self && info->ns_types == ns_types) { info->count++; _LOGt(self, "push#%u* %s (increase count to %d)", _stack_size(netns_stack) - 1, _ns_types_to_str(ns_types, ns_types, sbuf), info->count); return TRUE; } _LOGD(self, "push#%u %s", _stack_size(netns_stack), _ns_types_to_str(ns_types, _stack_current_ns_types(netns_stack, self, ns_types), sbuf)); if (!_netns_switch_push(netns_stack, self, ns_types)) return FALSE; _stack_push(netns_stack, self, ns_types); return TRUE; } gboolean nmp_netns_push(NMPNetns *self) { g_return_val_if_fail(NMP_IS_NETNS(self), FALSE); return _nmp_netns_push_type(self, _CLONE_NS_ALL); } gboolean nmp_netns_push_type(NMPNetns *self, int ns_types) { g_return_val_if_fail(NMP_IS_NETNS(self), FALSE); g_return_val_if_fail(!NM_FLAGS_ANY(ns_types, ~_CLONE_NS_ALL), FALSE); return _nmp_netns_push_type(self, ns_types == 0 ? _CLONE_NS_ALL : ns_types); } NMPNetns * nmp_netns_new(void) { GArray * netns_stack = _netns_stack_get(); NMPNetns * self; int errsv; GError * error = NULL; unsigned long mountflags = 0; if (!_stack_peek(netns_stack)) { /* there are no netns instances. We cannot create a new one * (because after unshare we couldn't return to the original one). */ errno = ENOTSUP; return NULL; } if (unshare(_CLONE_NS_ALL) != 0) { errsv = errno; _LOGE(NULL, "failed to create new net and mnt namespace: %s", nm_strerror_native(errsv)); return NULL; } if (mount("", "/", "none", MS_SLAVE | MS_REC, NULL) != 0) { errsv = errno; _LOGE(NULL, "failed mount --make-rslave: %s", nm_strerror_native(errsv)); goto err_out; } if (umount2("/sys", MNT_DETACH) != 0) { errsv = errno; _LOGE(NULL, "failed umount /sys: %s", nm_strerror_native(errsv)); goto err_out; } if (access("/sys", W_OK) == -1) mountflags = MS_RDONLY; if (mount("sysfs", "/sys", "sysfs", mountflags, NULL) != 0) { errsv = errno; _LOGE(NULL, "failed mount /sys: %s", nm_strerror_native(errsv)); goto err_out; } self = _netns_new(&error); if (!self) { errsv = errno; _LOGE(NULL, "failed to create netns after unshare: %s", error->message); g_clear_error(&error); goto err_out; } _stack_push(netns_stack, self, _CLONE_NS_ALL); return self; err_out: _netns_switch_pop(netns_stack, NULL, _CLONE_NS_ALL); errno = errsv; return NULL; } gboolean nmp_netns_pop(NMPNetns *self) { GArray * netns_stack = _netns_stack_get(); NetnsInfo *info; int ns_types; g_return_val_if_fail(NMP_IS_NETNS(self), FALSE); info = _stack_peek(netns_stack); g_return_val_if_fail(info, FALSE); g_return_val_if_fail(info->netns == self, FALSE); if (info->count > 1) { info->count--; _LOGt(self, "pop#%u* (decrease count to %d)", _stack_size(netns_stack) - 1, info->count); return TRUE; } g_return_val_if_fail(info->count == 1, FALSE); /* cannot pop the original netns. */ g_return_val_if_fail(_stack_size(netns_stack) > 1, FALSE); _LOGD(self, "pop#%u", _stack_size(netns_stack) - 1); ns_types = info->ns_types; _stack_pop(netns_stack); return _netns_switch_pop(netns_stack, self, ns_types); } NMPNetns * nmp_netns_get_current(void) { return _netns_get(_stack_peek(_netns_stack_get())); } NMPNetns * nmp_netns_get_initial(void) { return _netns_get(_stack_bottom(_netns_stack_get())); } gboolean nmp_netns_is_initial(void) { GArray *netns_stack = _netns_stack_get(); return (_netns_get(_stack_peek(netns_stack)) == _netns_get(_stack_bottom(netns_stack))); } /*****************************************************************************/ gboolean nmp_netns_bind_to_path(NMPNetns *self, const char *filename, int *out_fd) { gs_free char * dirname = NULL; int errsv; int fd; nm_auto_pop_netns NMPNetns *netns_pop = NULL; g_return_val_if_fail(NMP_IS_NETNS(self), FALSE); g_return_val_if_fail(filename && filename[0] == '/', FALSE); if (!nmp_netns_push_type(self, CLONE_NEWNET)) return FALSE; netns_pop = self; dirname = g_path_get_dirname(filename); if (mkdir(dirname, 0) != 0) { errsv = errno; if (errsv != EEXIST) { _LOGE(self, "bind: failed to create directory %s: %s", dirname, nm_strerror_native(errsv)); return FALSE; } } if ((fd = creat(filename, S_IRUSR | S_IRGRP | S_IROTH)) == -1) { errsv = errno; _LOGE(self, "bind: failed to create %s: %s", filename, nm_strerror_native(errsv)); return FALSE; } nm_close(fd); if (mount(PROC_SELF_NS_NET, filename, "none", MS_BIND, NULL) != 0) { errsv = errno; _LOGE(self, "bind: failed to mount %s to %s: %s", PROC_SELF_NS_NET, filename, nm_strerror_native(errsv)); unlink(filename); return FALSE; } if (out_fd) { if ((fd = open(filename, O_RDONLY | O_CLOEXEC)) == -1) { errsv = errno; _LOGE(self, "bind: failed to open %s: %s", filename, nm_strerror_native(errsv)); umount2(filename, MNT_DETACH); unlink(filename); return FALSE; } *out_fd = fd; } return TRUE; } gboolean nmp_netns_bind_to_path_destroy(NMPNetns *self, const char *filename) { int errsv; g_return_val_if_fail(NMP_IS_NETNS(self), FALSE); g_return_val_if_fail(filename && filename[0] == '/', FALSE); if (umount2(filename, MNT_DETACH) != 0) { errsv = errno; _LOGE(self, "bind: failed to unmount2 %s: %s", filename, nm_strerror_native(errsv)); return FALSE; } if (unlink(filename) != 0) { errsv = errno; _LOGE(self, "bind: failed to unlink %s: %s", filename, nm_strerror_native(errsv)); return FALSE; } return TRUE; } /*****************************************************************************/ static void set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { NMPNetns * self = NMP_NETNS(object); NMPNetnsPrivate *priv = NMP_NETNS_GET_PRIVATE(self); switch (prop_id) { case PROP_FD_NET: /* construct-only */ priv->fd_net = g_value_get_int(value); g_return_if_fail(priv->fd_net > 0); break; case PROP_FD_MNT: /* construct-only */ priv->fd_mnt = g_value_get_int(value); g_return_if_fail(priv->fd_mnt > 0); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void nmp_netns_init(NMPNetns *self) {} static void dispose(GObject *object) { NMPNetns * self = NMP_NETNS(object); NMPNetnsPrivate *priv = NMP_NETNS_GET_PRIVATE(self); nm_close(priv->fd_net); priv->fd_net = -1; nm_close(priv->fd_mnt); priv->fd_mnt = -1; G_OBJECT_CLASS(nmp_netns_parent_class)->dispose(object); } static void nmp_netns_class_init(NMPNetnsClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->set_property = set_property; object_class->dispose = dispose; obj_properties[PROP_FD_NET] = g_param_spec_int(NMP_NETNS_FD_NET, "", "", 0, G_MAXINT, 0, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); obj_properties[PROP_FD_MNT] = g_param_spec_int(NMP_NETNS_FD_MNT, "", "", 0, G_MAXINT, 0, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); }