From f137b32d31174e5226f4ab7d3657c90851e50000 Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Sun, 18 Jul 2021 08:53:43 +0200 Subject: [PATCH] sudo: introduce nm-sudo D-Bus service NetworkManager runs as root and has lots of capabilities. We want to reduce the attach surface by dropping capabilities, but there is a genuine need to do certain things. For example, we currently require dac_override capability, to open the unix socket of ovsdb. Most users wouldn't use OVS, so we should find a way to not require that dac_override capability. The solution is to have a separate, D-Bus activate service (nm-sudo), which has the capability to open and provide the file descriptor. For authentication, we only rely on D-Bus. We watch the name owner of NetworkManager, and only accept requests from that service. We trust D-Bus to get it right a request from that name owner is really coming from NetworkManager. If we couldn't trust that, how could PolicyKit or any authentication via D-Bus work? For testing, the user can set NM_SUDO_NO_AUTH_FOR_TESTING=1. https://bugzilla.redhat.com/show_bug.cgi?id=1921826 --- .gitignore | 4 + Makefile.am | 62 +- contrib/fedora/rpm/NetworkManager.spec | 8 +- data/meson.build | 1 + data/nm-sudo.service.in | 66 ++ po/POTFILES.skip | 1 + src/core/meson.build | 1 + src/core/nm-sudo-call.c | 5 + src/core/nm-sudo-call.h | 6 + src/libnm-base/meson.build | 1 + src/libnm-base/nm-sudo-utils.c | 5 + src/libnm-base/nm-sudo-utils.h | 14 + src/meson.build | 1 + src/nm-sudo/meson.build | 36 ++ src/nm-sudo/nm-sudo.c | 600 ++++++++++++++++++ src/nm-sudo/nm-sudo.conf | 13 + .../org.freedesktop.nm.sudo.service.in | 5 + 17 files changed, 826 insertions(+), 3 deletions(-) create mode 100644 data/nm-sudo.service.in create mode 100644 src/core/nm-sudo-call.c create mode 100644 src/core/nm-sudo-call.h create mode 100644 src/libnm-base/nm-sudo-utils.c create mode 100644 src/libnm-base/nm-sudo-utils.h create mode 100644 src/nm-sudo/meson.build create mode 100644 src/nm-sudo/nm-sudo.c create mode 100644 src/nm-sudo/nm-sudo.conf create mode 100644 src/nm-sudo/org.freedesktop.nm.sudo.service.in diff --git a/.gitignore b/.gitignore index 041f681f00..b62e3aa7de 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,9 @@ test-*.trs /src/nm-dispatcher/org.freedesktop.nm_dispatcher.service /src/nm-dispatcher/tests/test-dispatcher-envp +/src/nm-sudo/nm-sudo +/src/nm-sudo/org.freedesktop.nm.sudo.service + /data/NetworkManager.service /data/NetworkManager-wait-online.service /data/NetworkManager-dispatcher.service @@ -75,6 +78,7 @@ test-*.trs /data/server.conf /data/org.freedesktop.NetworkManager.policy /data/org.freedesktop.NetworkManager.policy.in +/data/nm-sudo.service /docs/api/version.xml /docs/api/settings-spec.html diff --git a/Makefile.am b/Makefile.am index af327de094..7a6f56199a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -504,6 +504,8 @@ src_libnm_base_libnm_base_la_SOURCES = \ src/libnm-base/nm-ethtool-utils-base.h \ src/libnm-base/nm-net-aux.c \ src/libnm-base/nm-net-aux.h \ + src/libnm-base/nm-sudo-utils.c \ + src/libnm-base/nm-sudo-utils.h \ $(NULL) src_libnm_base_libnm_base_la_LDFLAGS = \ @@ -2584,8 +2586,10 @@ src_core_libNetworkManager_la_SOURCES = \ src/core/nm-policy.h \ src/core/nm-rfkill-manager.c \ src/core/nm-rfkill-manager.h \ - src/core/nm-session-monitor.h \ src/core/nm-session-monitor.c \ + src/core/nm-session-monitor.h \ + src/core/nm-sudo-call.c \ + src/core/nm-sudo-call.h \ src/core/nm-keep-alive.c \ src/core/nm-keep-alive.h \ src/core/nm-sleep-monitor.c \ @@ -4617,6 +4621,56 @@ EXTRA_DIST += \ src/nm-dispatcher/tests/meson.build \ $(NULL) +############################################################################### +# src/nm-sudo +############################################################################### + +libexec_PROGRAMS += src/nm-sudo/nm-sudo + +src_nm_sudo_nm_sudo_SOURCES = \ + src/nm-sudo/nm-sudo.c \ + $(NULL) + +src_nm_sudo_nm_sudo_CPPFLAGS = \ + $(dflt_cppflags) \ + -I$(builddir)/src/libnm-core-public \ + -I$(srcdir)/src/libnm-core-public \ + -I$(builddir)/src/libnm-client-public \ + -I$(srcdir)/src/libnm-client-public \ + -I$(srcdir)/src \ + -I$(builddir)/src \ + $(GLIB_CFLAGS) \ + $(NULL) + +src_nm_sudo_nm_sudo_LDFLAGS = \ + -Wl,--version-script="$(srcdir)/linker-script-binary.ver" \ + $(SANITIZER_EXEC_LDFLAGS) \ + $(NULL) + +src_nm_sudo_nm_sudo_LDADD = \ + src/libnm-base/libnm-base.la \ + src/libnm-glib-aux/libnm-glib-aux.la \ + src/libnm-std-aux/libnm-std-aux.la \ + src/c-siphash/libc-siphash.la \ + $(GLIB_LIBS) \ + $(NULL) + +src/nm-sudo/org.freedesktop.nm.sudo.service: $(srcdir)/src/nm-sudo/org.freedesktop.nm.sudo.service.in + @sed \ + -e 's|@libexecdir[@]|$(libexecdir)|g' \ + $< >$@ + +dbusactivation_DATA += src/nm-sudo/org.freedesktop.nm.sudo.service +CLEANFILES += src/nm-sudo/org.freedesktop.nm.sudo.service + +dbusservice_DATA += src/nm-sudo/nm-sudo.conf + +EXTRA_DIST += \ + src/nm-sudo/nm-sudo.conf \ + src/nm-sudo/org.freedesktop.nm.sudo.service.in \ + src/nm-sudo/meson.build \ + $(NULL) + ############################################################################### # src/nm-daemon-helper ############################################################################### @@ -5299,6 +5353,7 @@ systemdsystemunit_DATA += \ data/NetworkManager.service \ data/NetworkManager-wait-online.service \ data/NetworkManager-dispatcher.service \ + data/nm-sudo.service \ $(NULL) data/NetworkManager.service: $(srcdir)/data/NetworkManager.service.in @@ -5315,6 +5370,9 @@ endif data/NetworkManager-dispatcher.service: $(srcdir)/data/NetworkManager-dispatcher.service.in $(AM_V_GEN) $(data_edit) $< >$@ +data/nm-sudo.service: $(srcdir)/data/nm-sudo.service.in + $(AM_V_GEN) $(data_edit) $< >$@ + endif examples_DATA += data/server.conf @@ -5344,6 +5402,7 @@ EXTRA_DIST += \ data/NetworkManager-wait-online-systemd-pre200.service.in \ data/NetworkManager-wait-online.service.in \ data/NetworkManager.service.in \ + data/nm-sudo.service.in \ data/meson.build \ data/nm-shared.xml \ data/server.conf.in \ @@ -5353,6 +5412,7 @@ CLEANFILES += \ data/NetworkManager-dispatcher.service \ data/NetworkManager-wait-online.service \ data/NetworkManager.service \ + data/nm-sudo.service \ data/server.conf \ $(NULL) diff --git a/contrib/fedora/rpm/NetworkManager.spec b/contrib/fedora/rpm/NetworkManager.spec index f8374519db..9a213cac73 100644 --- a/contrib/fedora/rpm/NetworkManager.spec +++ b/contrib/fedora/rpm/NetworkManager.spec @@ -40,7 +40,7 @@ %global real_version_major %(printf '%s' '%{real_version}' | sed -n 's/^\\([1-9][0-9]*\\.[0-9][0-9]*\\)\\.[0-9][0-9]*$/\\1/p') -%global systemd_units NetworkManager.service NetworkManager-wait-online.service NetworkManager-dispatcher.service +%global systemd_units NetworkManager.service NetworkManager-wait-online.service NetworkManager-dispatcher.service nm-sudo.service %global systemd_units_cloud_setup nm-cloud-setup.service nm-cloud-setup.timer @@ -940,7 +940,7 @@ if [ $1 -eq 0 ]; then /usr/sbin/update-alternatives --remove ifup %{_libexecdir}/nm-ifup >/dev/null 2>&1 || : fi -%systemd_preun NetworkManager-wait-online.service NetworkManager-dispatcher.service +%systemd_preun NetworkManager-wait-online.service NetworkManager-dispatcher.service nm-sudo.service %if %{with nm_cloud_setup} @@ -974,6 +974,7 @@ fi %files %{dbus_sys_dir}/org.freedesktop.NetworkManager.conf %{dbus_sys_dir}/nm-dispatcher.conf +%{dbus_sys_dir}/nm-sudo.conf %{dbus_sys_dir}/nm-ifcfg-rh.conf %{_sbindir}/%{name} %{_bindir}/nmcli @@ -999,6 +1000,7 @@ fi %{_libexecdir}/nm-iface-helper %{_libexecdir}/nm-initrd-generator %{_libexecdir}/nm-daemon-helper +%{_libexecdir}/nm-sudo %dir %{_libdir}/%{name} %dir %{nmplugindir} %{nmplugindir}/libnm-settings-plugin*.so @@ -1022,6 +1024,7 @@ fi %dir %{_localstatedir}/lib/NetworkManager %dir %{_sysconfdir}/sysconfig/network-scripts %{_datadir}/dbus-1/system-services/org.freedesktop.nm_dispatcher.service +%{_datadir}/dbus-1/system-services/org.freedesktop.nm.sudo.service %{_datadir}/polkit-1/actions/*.policy %{_prefix}/lib/udev/rules.d/*.rules %if %{with firewalld_zone} @@ -1031,6 +1034,7 @@ fi %{systemd_dir}/NetworkManager.service %{systemd_dir}/NetworkManager-wait-online.service %{systemd_dir}/NetworkManager-dispatcher.service +%{systemd_dir}/nm-sudo.service %dir %{_datadir}/doc/NetworkManager/examples %{_datadir}/doc/NetworkManager/examples/server.conf %doc NEWS AUTHORS README CONTRIBUTING.md TODO diff --git a/data/meson.build b/data/meson.build index 64a1372b41..edb6418d0e 100644 --- a/data/meson.build +++ b/data/meson.build @@ -11,6 +11,7 @@ if install_systemdunitdir services = [ 'NetworkManager-dispatcher.service.in', 'NetworkManager.service.in', + 'nm-sudo.service.in', ] if have_systemd_200 diff --git a/data/nm-sudo.service.in b/data/nm-sudo.service.in new file mode 100644 index 0000000000..9cd30dbfdf --- /dev/null +++ b/data/nm-sudo.service.in @@ -0,0 +1,66 @@ +[Unit] +Description=Network Manager Sudo Helper +# +# nm-sudo exists for privilege separation. It allows to run NetworkManager +# without certain capabilities, and ask nm-sudo for special operations +# where more privileges are required. +# +# While nm-sudo has privileges that NetworkManager has not, it does not +# mean that itself should run totally unconstrained. On the contrary, it +# also should only have permissions it requires. +# +# nm-sudo rejects all requests that come from any other than the name +# owner of "org.freedesktop.NetworkManager" (that is, NetworkManager process +# itself). It is thus only an implementation detail and provides no public +# API to the user. + +[Service] +Type=dbus +BusName=org.freedesktop.nm.sudo +ExecStart=@libexecdir@/nm-sudo + +# Environment=NM_SUDO_NO_AUTH_FOR_TESTING=0 +# Environment=NM_SUDO_IDLE_TIMEOUT=10 +# Environment=NM_SUDO_LOG=TRACE +# Environment=G_DEBUG=fatal-warnings +# Environment=G_DBUS_DEBUG=all + +[Install] +Alias=dbus-org.freedesktop.nm.sudo.service + +[Service] +# Restrict: +AmbientCapabilities= +CapabilityBoundingSet= +PrivateDevices=true +PrivateMounts=true +PrivateNetwork=true +PrivateTmp=true +ProtectClock=true +ProtectControlGroups=true +ProtectHome=true +ProtectHostname=true +ProtectKernelLogs=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectSystem=strict +RestrictAddressFamilies= +RestrictNamespaces=true +SystemCallFilter=~@clock +SystemCallFilter=~@cpu-emulation +SystemCallFilter=~@debug +SystemCallFilter=~@module +SystemCallFilter=~@mount +SystemCallFilter=~@obsolete +SystemCallFilter=~@privileged +SystemCallFilter=~@raw-io +SystemCallFilter=~@reboot +SystemCallFilter=~@swap +NoNewPrivileges=true +SupplementaryGroups= + +# Grant: +CapabilityBoundingSet=CAP_DAC_OVERRIDE +PrivateUsers=no +RestrictAddressFamilies=AF_UNIX +SystemCallFilter=@resources diff --git a/po/POTFILES.skip b/po/POTFILES.skip index 692aa74950..3f70738f8b 100644 --- a/po/POTFILES.skip +++ b/po/POTFILES.skip @@ -1,6 +1,7 @@ contrib/fedora/rpm/ data/NetworkManager-wait-online.service.in data/NetworkManager.service.in +data/nm-sudo.service.in data/org.freedesktop.NetworkManager.policy.in examples/python/NetworkManager.py examples/python/systray/eggtrayicon.c diff --git a/src/core/meson.build b/src/core/meson.build index 1ce641d6d8..fc94def205 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -172,6 +172,7 @@ libNetworkManager = static_library( 'nm-rfkill-manager.c', 'nm-session-monitor.c', 'nm-sleep-monitor.c', + 'nm-sudo-call.c', ), dependencies: nm_deps, link_with: [ diff --git a/src/core/nm-sudo-call.c b/src/core/nm-sudo-call.c new file mode 100644 index 0000000000..deeab52cb2 --- /dev/null +++ b/src/core/nm-sudo-call.c @@ -0,0 +1,5 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-sudo-call.h" diff --git a/src/core/nm-sudo-call.h b/src/core/nm-sudo-call.h new file mode 100644 index 0000000000..74b0a28cbb --- /dev/null +++ b/src/core/nm-sudo-call.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#ifndef __NM_SUDO_CALL_H__ +#define __NM_SUDO_CALL_H__ + +#endif /* __NM_SUDO_CALL_H__ */ diff --git a/src/libnm-base/meson.build b/src/libnm-base/meson.build index d7e7a1b85f..3cd554d269 100644 --- a/src/libnm-base/meson.build +++ b/src/libnm-base/meson.build @@ -5,6 +5,7 @@ libnm_base = static_library( sources: files( 'nm-ethtool-base.c', 'nm-net-aux.c', + 'nm-sudo-utils.c', ), include_directories: [ src_inc, diff --git a/src/libnm-base/nm-sudo-utils.c b/src/libnm-base/nm-sudo-utils.c new file mode 100644 index 0000000000..6ae1e78cd7 --- /dev/null +++ b/src/libnm-base/nm-sudo-utils.c @@ -0,0 +1,5 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-glib-aux/nm-default-glib-i18n-lib.h" + +#include "nm-sudo-utils.h" diff --git a/src/libnm-base/nm-sudo-utils.h b/src/libnm-base/nm-sudo-utils.h new file mode 100644 index 0000000000..cffea48dbc --- /dev/null +++ b/src/libnm-base/nm-sudo-utils.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#ifndef __NM_SUDO_UTILS_H__ +#define __NM_SUDO_UTILS_H__ + +/*****************************************************************************/ + +#define NM_SUDO_DBUS_BUS_NAME "org.freedesktop.nm.sudo" +#define NM_SUDO_DBUS_OBJECT_PATH "/org/freedesktop/nm/sudo" +#define NM_SUDO_DBUS_IFACE_NAME "org.freedesktop.nm.sudo" + +/*****************************************************************************/ + +#endif /* __NM_SUDO_UTILS_H__ */ diff --git a/src/meson.build b/src/meson.build index 39bfe7ef78..4751a29684 100644 --- a/src/meson.build +++ b/src/meson.build @@ -93,6 +93,7 @@ if enable_nmtui endif subdir('nmcli') subdir('nm-dispatcher') +subdir('nm-sudo') subdir('nm-daemon-helper') subdir('nm-online') if enable_nmtui diff --git a/src/nm-sudo/meson.build b/src/nm-sudo/meson.build new file mode 100644 index 0000000000..875ce3d515 --- /dev/null +++ b/src/nm-sudo/meson.build @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +configure_file( + input: 'org.freedesktop.nm.sudo.service.in', + output: '@BASENAME@', + install_dir: dbus_system_bus_services_dir, + configuration: data_conf, +) + +install_data( + 'nm-sudo.conf', + install_dir: dbus_conf_dir, +) + +executable( + 'nm-sudo', + 'nm-sudo.c', + include_directories : [ + src_inc, + top_inc, + ], + dependencies: [ + glib_dep, + ], + link_with: [ + libnm_base, + libnm_log_null, + libnm_glib_aux, + libnm_std_aux, + libc_siphash, + ], + link_args: ldflags_linker_script_binary, + link_depends: linker_script_binary, + install: true, + install_dir: nm_libexecdir, +) diff --git a/src/nm-sudo/nm-sudo.c b/src/nm-sudo/nm-sudo.c new file mode 100644 index 0000000000..5be29d5a96 --- /dev/null +++ b/src/nm-sudo/nm-sudo.c @@ -0,0 +1,600 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-glib-aux/nm-default-glib-i18n-prog.h" + +#include "c-list/src/c-list.h" +#include "libnm-glib-aux/nm-logging-base.h" +#include "libnm-glib-aux/nm-shared-utils.h" +#include "libnm-glib-aux/nm-time-utils.h" +#include "libnm-glib-aux/nm-dbus-aux.h" +#include "libnm-base/nm-sudo-utils.h" + +/* nm-sudo doesn't link with libnm-core nor libnm-base, but these headers + * can be used independently. */ +#include "libnm-core-public/nm-dbus-interface.h" + +/*****************************************************************************/ + +#define IDLE_TIMEOUT_MSEC 2000 +#define IDLE_TIMEOUT_INFINITY G_MAXINT32 + +/*****************************************************************************/ + +/* Serves only the purpose to mark environment variables that are honored by + * the application. You can search for this macro, and find what options are supported. */ +#define _ENV(var) ("" var "") + +/*****************************************************************************/ + +typedef struct _GlobalData GlobalData; + +typedef struct { + CList pending_jobs_lst; + GlobalData *gl; +} PendingJobData; + +struct _GlobalData { + GCancellable * quit_cancellable; + GDBusConnection *dbus_connection; + GSource * source_sigterm; + + CList pending_jobs_lst_head; + + GSource *source_idle_timeout; + char * name_owner; + guint name_owner_changed_id; + guint service_regist_id; + gint64 start_timestamp_msec; + guint32 timeout_msec; + bool name_owner_initialized; + bool service_registered; + + /* This is controlled by $NM_SUDO_NO_AUTH_FOR_TESTING. It disables authentication + * of the request, so it is ONLY for testing. */ + bool no_auth_for_testing; + + bool is_shutting_down_quitting; + bool is_shutting_down_timeout; + bool is_shutting_down_cleanup; +}; + +/*****************************************************************************/ + +static void _pending_job_register_object(GlobalData *gl, GObject *obj); + +/*****************************************************************************/ + +#define _nm_log(level, ...) _nm_log_simple_printf((level), __VA_ARGS__); + +#define _NMLOG(level, ...) \ + G_STMT_START \ + { \ + const NMLogLevel _level = (level); \ + \ + if (_nm_logging_enabled(_level)) { \ + _nm_log(_level, __VA_ARGS__); \ + } \ + } \ + G_STMT_END + +/*****************************************************************************/ + +static void +_handle_ping(GlobalData *gl, GDBusMethodInvocation *invocation, const char *arg) +{ + gs_free char *msg = NULL; + gint64 running_msec; + + running_msec = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME) - gl->start_timestamp_msec; + + msg = g_strdup_printf("pid=%lu, unique-name=%s, nm-name-owner=%s, since=%ld.%03d%s, pong=%s", + (unsigned long) getpid(), + g_dbus_connection_get_unique_name(gl->dbus_connection), + gl->name_owner ?: "(none)", + running_msec / 1000, + (int) (running_msec % 1000), + gl->no_auth_for_testing ? ", no-auth-for-testing" : "", + arg); + g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", msg)); +} + +/*****************************************************************************/ + +static gboolean +_signal_callback_term(gpointer user_data) +{ + GlobalData *gl = user_data; + + _LOGD("sigterm received (%s)", + c_list_is_empty(&gl->pending_jobs_lst_head) ? "quit mainloop" : "cancel operations"); + + gl->is_shutting_down_quitting = TRUE; + g_cancellable_cancel(gl->quit_cancellable); + return G_SOURCE_CONTINUE; +} + +/*****************************************************************************/ + +typedef struct { + GDBusConnection **p_dbus_connection; + GError ** p_error; +} BusGetData; + +static void +_bus_get_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + BusGetData *data = user_data; + + *data->p_dbus_connection = g_bus_get_finish(result, data->p_error); +} + +static GDBusConnection * +_bus_get(GCancellable *cancellable, int *out_exit_code) +{ + gs_free_error GError *error = NULL; + gs_unref_object GDBusConnection *dbus_connection = NULL; + BusGetData data = { + .p_dbus_connection = &dbus_connection, + .p_error = &error, + }; + + g_bus_get(G_BUS_TYPE_SYSTEM, cancellable, _bus_get_cb, &data); + + while (!dbus_connection && !error) + g_main_context_iteration(NULL, TRUE); + + if (!dbus_connection) { + gboolean was_cancelled = nm_utils_error_is_cancelled(error); + + NM_SET_OUT(out_exit_code, was_cancelled ? EXIT_SUCCESS : EXIT_FAILURE); + if (!was_cancelled) + _LOGE("dbus: failure to get D-Bus connection: %s", error->message); + return NULL; + } + + /* On bus-disconnect, GDBus will raise(SIGTERM), which we handle like a + * regular request to quit. */ + g_dbus_connection_set_exit_on_close(dbus_connection, TRUE); + + _LOGD("dbus: unique name: %s", g_dbus_connection_get_unique_name(dbus_connection)); + + return g_steal_pointer(&dbus_connection); +} + +/*****************************************************************************/ + +static void +_name_owner_changed_cb(GDBusConnection *connection, + const char * sender_name, + const char * object_path, + const char * interface_name, + const char * signal_name, + GVariant * parameters, + gpointer user_data) +{ + GlobalData *gl = user_data; + const char *new_owner; + + if (!gl->name_owner_initialized) + return; + + if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)"))) + return; + + g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner); + new_owner = nm_str_not_empty(new_owner); + + _LOGD("%s name-owner changed: %s -> %s", + NM_DBUS_SERVICE, + gl->name_owner ?: "(null)", + new_owner ?: "(null)"); + + nm_utils_strdup_reset(&gl->name_owner, new_owner); +} + +typedef struct { + GlobalData *gl; + char ** p_name_owner; + gboolean is_cancelled; +} BusFindNMNameOwnerData; + +static void +_bus_find_nm_nameowner_cb(const char *name_owner, GError *error, gpointer user_data) +{ + BusFindNMNameOwnerData *data = user_data; + + *data->p_name_owner = nm_strdup_not_empty(name_owner); + data->is_cancelled = nm_utils_error_is_cancelled(error); + data->gl->name_owner_initialized = TRUE; +} + +static gboolean +_bus_find_nm_nameowner(GlobalData *gl) +{ + BusFindNMNameOwnerData data; + guint name_owner_changed_id; + gs_free char * name_owner = NULL; + + name_owner_changed_id = + nm_dbus_connection_signal_subscribe_name_owner_changed(gl->dbus_connection, + NM_DBUS_SERVICE, + _name_owner_changed_cb, + gl, + NULL); + + data = (BusFindNMNameOwnerData){ + .gl = gl, + .is_cancelled = FALSE, + .p_name_owner = &name_owner, + }; + nm_dbus_connection_call_get_name_owner(gl->dbus_connection, + NM_DBUS_SERVICE, + 10000, + gl->quit_cancellable, + _bus_find_nm_nameowner_cb, + &data); + while (!gl->name_owner_initialized) + g_main_context_iteration(NULL, TRUE); + + if (data.is_cancelled) { + g_dbus_connection_signal_unsubscribe(gl->dbus_connection, name_owner_changed_id); + return FALSE; + } + + gl->name_owner_changed_id = name_owner_changed_id; + gl->name_owner = g_steal_pointer(&name_owner); + return TRUE; +} + +/*****************************************************************************/ + +static void +_bus_method_call(GDBusConnection * connection, + const char * sender, + const char * object_path, + const char * interface_name, + const char * method_name, + GVariant * parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + GlobalData *gl = user_data; + const char *arg_s; + + nm_assert(nm_streq(object_path, NM_SUDO_DBUS_OBJECT_PATH)); + nm_assert(nm_streq(interface_name, NM_SUDO_DBUS_IFACE_NAME)); + + if (!gl->no_auth_for_testing && !nm_streq0(sender, gl->name_owner)) { + _LOGT("dbus: request sender=%s, %s%s, ACCESS DENIED", + sender, + method_name, + g_variant_get_type_string(parameters)); + g_dbus_method_invocation_return_error(invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Access denied"); + return; + } + + _pending_job_register_object(gl, G_OBJECT(invocation)); + + _LOGT("dbus: request sender=%s, %s%s", + sender, + method_name, + g_variant_get_type_string(parameters)); + + if (nm_streq(method_name, "Ping")) { + g_variant_get(parameters, "(&s)", &arg_s); + _handle_ping(gl, invocation, arg_s); + } else + nm_assert_not_reached(); +} + +static GDBusInterfaceInfo *const interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO( + NM_SUDO_DBUS_IFACE_NAME, + .methods = NM_DEFINE_GDBUS_METHOD_INFOS( + NM_DEFINE_GDBUS_METHOD_INFO( + "Ping", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("arg", "s"), ), + .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("arg", "s"), ), ), ), ); + +typedef struct { + GlobalData *gl; + gboolean is_waiting; +} BusRegisterServiceRequestNameData; + +static void +_bus_register_service_request_name_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + BusRegisterServiceRequestNameData *data = user_data; + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *ret = NULL; + gboolean success = FALSE; + + ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error); + + if (nm_utils_error_is_cancelled(error)) + goto out; + + if (error) + _LOGE("d-bus: failed to request name %s: %s", NM_SUDO_DBUS_BUS_NAME, error->message); + else { + guint32 ret_val; + + g_variant_get(ret, "(u)", &ret_val); + if (ret_val != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { + _LOGW("dbus: request name for %s failed to take name (response %u)", + NM_SUDO_DBUS_BUS_NAME, + ret_val); + } else { + _LOGD("dbus: request name for %s succeeded", NM_SUDO_DBUS_BUS_NAME); + success = TRUE; + } + } + +out: + if (success) + data->gl->service_registered = TRUE; + data->is_waiting = FALSE; +} + +static void +_bus_register_service(GlobalData *gl) +{ + static const GDBusInterfaceVTable interface_vtable = { + .method_call = _bus_method_call, + }; + gs_free_error GError * error = NULL; + BusRegisterServiceRequestNameData data; + + nm_assert(!gl->service_registered); + + gl->service_regist_id = + g_dbus_connection_register_object(gl->dbus_connection, + NM_SUDO_DBUS_OBJECT_PATH, + interface_info, + NM_UNCONST_PTR(GDBusInterfaceVTable, &interface_vtable), + gl, + NULL, + &error); + if (gl->service_regist_id == 0) { + _LOGE("dbus: error registering object %s: %s", NM_SUDO_DBUS_OBJECT_PATH, error->message); + return; + } + + _LOGD("dbus: object %s registered", NM_SUDO_DBUS_OBJECT_PATH); + + data = (BusRegisterServiceRequestNameData){ + .gl = gl, + .is_waiting = TRUE, + }; + + g_dbus_connection_call( + gl->dbus_connection, + DBUS_SERVICE_DBUS, + DBUS_PATH_DBUS, + DBUS_INTERFACE_DBUS, + "RequestName", + g_variant_new("(su)", + NM_SUDO_DBUS_BUS_NAME, + (guint) (DBUS_NAME_FLAG_ALLOW_REPLACEMENT | DBUS_NAME_FLAG_REPLACE_EXISTING)), + G_VARIANT_TYPE("(u)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + gl->quit_cancellable, + _bus_register_service_request_name_cb, + &data); + + /* Note that with D-Bus activation, the first request will already hit us before RequestName + * completes. */ + + while (data.is_waiting) + g_main_context_iteration(NULL, TRUE); +} + +/*****************************************************************************/ + +static gboolean +_idle_timeout_cb(gpointer user_data) +{ + GlobalData *gl = user_data; + + _LOGT("idle-timeout: expired"); + gl->is_shutting_down_timeout = TRUE; + return G_SOURCE_CONTINUE; +} + +static void +_idle_timeout_restart(GlobalData *gl) +{ + nm_clear_g_source_inst(&gl->source_idle_timeout); + + if (gl->is_shutting_down_quitting) + return; + + if (gl->is_shutting_down_cleanup) + return; + + if (!c_list_is_empty(&gl->pending_jobs_lst_head)) + return; + + if (gl->timeout_msec == IDLE_TIMEOUT_INFINITY) + return; + + nm_assert(gl->timeout_msec < G_MAXINT32); + G_STATIC_ASSERT_EXPR(G_MAXINT32 < G_MAXUINT); + + _LOGT("idle-timeout: start (%u msec)", gl->timeout_msec); + gl->source_idle_timeout = nm_g_timeout_add_source(gl->timeout_msec, _idle_timeout_cb, gl); +} + +/*****************************************************************************/ + +static gboolean +_pending_job_register_object_release_on_idle_cb(gpointer data) +{ + PendingJobData *idle_data = data; + GlobalData * gl = idle_data->gl; + + c_list_unlink_stale(&idle_data->pending_jobs_lst); + nm_g_slice_free(idle_data); + + _idle_timeout_restart(gl); + return G_SOURCE_REMOVE; +} + +static void +_pending_job_register_object_weak_cb(gpointer data, GObject *where_the_object_was) +{ + /* The object might be destroyed on another thread. We need + * to sync with the main GMainContext by scheduling an idle action + * there. */ + nm_g_idle_add(_pending_job_register_object_release_on_idle_cb, data); +} + +static void +_pending_job_register_object(GlobalData *gl, GObject *obj) +{ + PendingJobData *idle_data; + + /* if we just hit the timeout, we can ignore it. */ + gl->is_shutting_down_timeout = FALSE; + + if (nm_clear_g_source_inst(&gl->source_idle_timeout)) + _LOGT("idle-timeout: suspend timeout for pending request"); + + idle_data = g_slice_new(PendingJobData); + + idle_data->gl = gl; + c_list_link_tail(&gl->pending_jobs_lst_head, &idle_data->pending_jobs_lst); + + g_object_weak_ref(obj, _pending_job_register_object_weak_cb, idle_data); +} + +/*****************************************************************************/ + +static void +_initial_setup(GlobalData *gl) +{ + gl->no_auth_for_testing = + _nm_utils_ascii_str_to_int64(g_getenv(_ENV("NM_SUDO_NO_AUTH_FOR_TESTING")), 0, 0, 1, 0); + gl->timeout_msec = _nm_utils_ascii_str_to_int64(g_getenv(_ENV("NM_SUDO_IDLE_TIMEOUT_MSEC")), + 0, + 0, + G_MAXINT32, + IDLE_TIMEOUT_MSEC); + + gl->quit_cancellable = g_cancellable_new(); + + signal(SIGPIPE, SIG_IGN); + gl->source_sigterm = nm_g_unix_signal_add_source(SIGTERM, _signal_callback_term, gl); +} + +int +main(int argc, char **argv) +{ + GlobalData _gl = { + .quit_cancellable = NULL, + .pending_jobs_lst_head = C_LIST_INIT(_gl.pending_jobs_lst_head), + }; + GlobalData *const gl = &_gl; + int exit_code; + int r = 0; + + _nm_logging_enabled_init(g_getenv(_ENV("NM_SUDO_LOG"))); + + gl->start_timestamp_msec = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME); + + _LOGD("starting nm-sudo (%s)", NM_DIST_VERSION); + + _initial_setup(gl); + + if (gl->no_auth_for_testing) { + _LOGW("WARNING: running in debug mode without authentication " + "(NM_SUDO_NO_AUTH_FOR_TESTING). "); + } + + if (gl->timeout_msec != IDLE_TIMEOUT_INFINITY) + _LOGT("idle-timeout: %u msec", gl->timeout_msec); + else + _LOGT("idle-timeout: disabled"); + + gl->dbus_connection = _bus_get(gl->quit_cancellable, &r); + if (!gl->dbus_connection) { + exit_code = r; + goto done; + } + + if (!_bus_find_nm_nameowner(gl)) { + /* abort due to cancellation. That is success. */ + exit_code = EXIT_SUCCESS; + goto done; + } + _LOGD("%s name-owner: %s", NM_DBUS_SERVICE, gl->name_owner ?: "(null)"); + + _idle_timeout_restart(gl); + + exit_code = EXIT_SUCCESS; + + _bus_register_service(gl); + if (!gl->service_registered) { + /* We failed to RequestName, but due to D-Bus activation we + * might have a pending request still (on the unique name). + * Process it below. + * + * Let's fake a shutdown signal, and still process the request below. */ + exit_code = EXIT_FAILURE; + gl->is_shutting_down_quitting = TRUE; + } + + while (TRUE) { + if (!c_list_is_empty(&gl->pending_jobs_lst_head)) { + /* we must first reply to all requests. No matter what. */ + } else { + if (gl->is_shutting_down_quitting || gl->is_shutting_down_timeout) { + /* we either hit the idle timeout or received SIGTERM. Note that + * if we received an idle-timeout and the very moment afterwards + * a new request, then _bus_method_call() will clear gl->is_shutting_down_timeout + * (via _pending_job_register_object()). */ + break; + } + } + + g_main_context_iteration(NULL, TRUE); + } + +done: + gl->is_shutting_down_cleanup = TRUE; + _LOGD("exiting..."); + + nm_assert(c_list_is_empty(&gl->pending_jobs_lst_head)); + + if (gl->service_regist_id != 0) { + g_dbus_connection_unregister_object(gl->dbus_connection, + nm_steal_int(&gl->service_regist_id)); + } + if (gl->name_owner_changed_id != 0) { + g_dbus_connection_signal_unsubscribe(gl->dbus_connection, + nm_steal_int(&gl->name_owner_changed_id)); + } + nm_clear_g_cancellable(&gl->quit_cancellable); + nm_clear_g_source_inst(&gl->source_sigterm); + nm_clear_g_source_inst(&gl->source_idle_timeout); + nm_clear_g_free(&gl->name_owner); + + while (g_main_context_iteration(NULL, FALSE)) { + ; + } + + if (gl->dbus_connection) { + g_dbus_connection_flush_sync(gl->dbus_connection, NULL, NULL); + g_clear_object(&gl->dbus_connection); + + while (g_main_context_iteration(NULL, FALSE)) { + ; + } + } + + _LOGD("exit (%d)", exit_code); + return exit_code; +} diff --git a/src/nm-sudo/nm-sudo.conf b/src/nm-sudo/nm-sudo.conf new file mode 100644 index 0000000000..922c62314a --- /dev/null +++ b/src/nm-sudo/nm-sudo.conf @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/nm-sudo/org.freedesktop.nm.sudo.service.in b/src/nm-sudo/org.freedesktop.nm.sudo.service.in new file mode 100644 index 0000000000..43d29de14d --- /dev/null +++ b/src/nm-sudo/org.freedesktop.nm.sudo.service.in @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=org.freedesktop.nm.sudo +Exec=@libexecdir@/nm-sudo +User=root +SystemdService=dbus-org.freedesktop.nm.sudo.service