NetworkManager/src/core/nm-core-utils.c
Beniamino Galvani 8d8edda3f4 core,libnm-core: introduce property flag for certificate and keys
If we add a new property in the future and it references a certificate
or key stored on disk, we need to also implement the logic to verify
the access to the file for private connections.

Add a new property flag NM_SETTING_PARAM_CERT_KEY_FILE to existing
certificate and key properties, so that it's easier to see that they
need special treatment. Also add some assertions to verify that the
properties with the flag are handled properly.

While at it, move the enumeration of private-files to the settings.
2025-12-12 12:38:50 +01:00

5837 lines
190 KiB
C

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2004 - 2018 Red Hat, Inc.
* Copyright (C) 2005 - 2008 Novell, Inc.
*/
#include "src/core/nm-default-daemon.h"
#include "nm-core-utils.h"
#include <fcntl.h>
#include <fnmatch.h>
#include <unistd.h>
#include <stdlib.h>
#include <resolv.h>
#include <byteswap.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <linux/if.h>
#include <linux/if_infiniband.h>
#include <net/if_arp.h>
#include <net/ethernet.h>
#include "libnm-glib-aux/nm-uuid.h"
#include "libnm-platform/nmp-base.h"
#include "libnm-std-aux/unaligned.h"
#include "libnm-glib-aux/nm-random-utils.h"
#include "libnm-glib-aux/nm-io-utils.h"
#include "libnm-glib-aux/nm-secret-utils.h"
#include "libnm-glib-aux/nm-time-utils.h"
#include "libnm-glib-aux/nm-str-buf.h"
#include "nm-utils.h"
#include "libnm-core-intern/nm-core-internal.h"
#include "nm-setting-connection.h"
#include "nm-setting-ip4-config.h"
#include "nm-setting-ip6-config.h"
#include "nm-setting-wireless.h"
#include "nm-setting-wireless-security.h"
#ifdef __NM_SD_UTILS_H__
#error \
"nm-core-utils.c should stay independent of systemd utils. Are you looking for NetworkMangerUtils.c? "
#endif
G_STATIC_ASSERT(sizeof(NMUtilsTestFlags) <= sizeof(int));
/* we read _nm_utils_testing without memory barrier. This is thread-safe,
* because the static variable is initialized to zero, and only reset
* once to a non-zero value (via g_atomic_int_compare_and_exchange()).
*
* Since there is only one integer that contains the data, there is no
* caching problem reading this (atomic int) variable without
* synchronization/memory-barrier. Contrary to a double-checked locking,
* where one needs a memory barrier to read the variable and ensure
* that also the related data is coherent in cache. Here there is no
* related data. */
static int _nm_utils_testing = 0;
gboolean
nm_utils_get_testing_initialized(void)
{
NMUtilsTestFlags flags;
flags = (NMUtilsTestFlags) _nm_utils_testing;
if (flags == NM_UTILS_TEST_NONE)
flags = (NMUtilsTestFlags) g_atomic_int_get(&_nm_utils_testing);
return flags != NM_UTILS_TEST_NONE;
}
NMUtilsTestFlags
nm_utils_get_testing(void)
{
NMUtilsTestFlags flags;
again:
flags = (NMUtilsTestFlags) _nm_utils_testing;
if (flags != NM_UTILS_TEST_NONE) {
/* Flags already initialized. Return them. */
return flags & NM_UTILS_TEST_ALL;
}
/* Accessing nm_utils_get_testing() causes us to set the flags to initialized.
* Detecting running tests also based on g_test_initialized(). */
flags = _NM_UTILS_TEST_INITIALIZED;
if (g_test_initialized())
flags |= _NM_UTILS_TEST_GENERAL;
g_atomic_int_compare_and_exchange(&_nm_utils_testing, 0, (int) flags);
/* regardless of whether we won the race of initializing _nm_utils_testing,
* go back and read the value again. It must be non-zero by now. */
goto again;
}
void
_nm_utils_set_testing(NMUtilsTestFlags flags)
{
g_assert(!NM_FLAGS_ANY(flags, ~NM_UTILS_TEST_ALL));
/* mask out everything except ALL, and always set GENERAL. */
flags = (flags & NM_UTILS_TEST_ALL) | (_NM_UTILS_TEST_GENERAL | _NM_UTILS_TEST_INITIALIZED);
if (!g_atomic_int_compare_and_exchange(&_nm_utils_testing, 0, (int) flags)) {
/* We only allow setting _nm_utils_set_testing() once, before fetching the
* value with nm_utils_get_testing(). */
g_return_if_reached();
}
}
/*****************************************************************************/
static GSList *_singletons = NULL;
static gboolean _singletons_shutdown = FALSE;
static void
_nm_singleton_instance_weak_cb(gpointer data, GObject *where_the_object_was)
{
nm_assert(g_slist_find(_singletons, where_the_object_was));
_singletons = g_slist_remove(_singletons, where_the_object_was);
}
static void __attribute__((destructor))
_nm_singleton_instance_destroy(void)
{
_singletons_shutdown = TRUE;
while (_singletons) {
GObject *instance = _singletons->data;
_singletons = g_slist_delete_link(_singletons, _singletons);
g_object_weak_unref(instance, _nm_singleton_instance_weak_cb, NULL);
if (instance->ref_count > 1) {
nm_log_dbg(LOGD_CORE,
"disown %s singleton (" NM_HASH_OBFUSCATE_PTR_FMT ")",
G_OBJECT_TYPE_NAME(instance),
NM_HASH_OBFUSCATE_PTR(instance));
}
g_object_unref(instance);
}
}
void
_nm_singleton_instance_register_destruction(GObject *instance)
{
g_return_if_fail(G_IS_OBJECT(instance));
/* Don't allow registration after shutdown. We only destroy the singletons
* once. */
g_return_if_fail(!_singletons_shutdown);
g_object_weak_ref(instance, _nm_singleton_instance_weak_cb, NULL);
_singletons = g_slist_prepend(_singletons, instance);
}
/*****************************************************************************/
gboolean
nm_ether_addr_is_valid(const NMEtherAddr *addr)
{
static const guint8 invalid_addr[][ETH_ALEN] = {
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
{0x44, 0x44, 0x44, 0x44, 0x44, 0x44},
{0x00, 0x30, 0xb4, 0x00, 0x00, 0x00}, /* prism54 dummy MAC */
};
int i;
if (!addr)
return FALSE;
/* Check for multicast address */
if (addr->ether_addr_octet[0] & 0x01u)
return FALSE;
for (i = 0; i < (int) G_N_ELEMENTS(invalid_addr); i++) {
if (memcmp(addr, invalid_addr[i], ETH_ALEN) == 0)
return FALSE;
}
return TRUE;
}
gboolean
nm_ether_addr_is_valid_str(const char *str)
{
NMEtherAddr addr_bin;
if (!str)
return FALSE;
if (!nm_utils_hwaddr_aton(str, &addr_bin, ETH_ALEN))
return FALSE;
return nm_ether_addr_is_valid(&addr_bin);
}
/*****************************************************************************/
void
nm_utils_array_remove_at_indexes(GArray *array, const guint *indexes_to_delete, gsize len)
{
gsize elt_size;
guint index_to_delete;
guint i_src;
guint mm_src, mm_dst, mm_len;
gsize i_itd;
guint res_length;
g_return_if_fail(array);
if (!len)
return;
g_return_if_fail(indexes_to_delete);
elt_size = g_array_get_element_size(array);
i_itd = 0;
index_to_delete = indexes_to_delete[0];
if (index_to_delete >= array->len)
g_return_if_reached();
res_length = array->len - 1;
mm_dst = index_to_delete;
mm_src = index_to_delete;
mm_len = 0;
for (i_src = index_to_delete; i_src < array->len; i_src++) {
if (i_src < index_to_delete)
mm_len++;
else {
/* we require indexes_to_delete to contain non-repeated, ascending
* indexes. Otherwise, we would need to presort the indexes. */
while (TRUE) {
guint dd;
if (i_itd + 1 >= len) {
index_to_delete = G_MAXUINT;
break;
}
dd = indexes_to_delete[++i_itd];
if (dd > index_to_delete) {
if (dd >= array->len)
g_warn_if_reached();
else {
g_assert(res_length > 0);
res_length--;
}
index_to_delete = dd;
break;
}
g_warn_if_reached();
}
if (mm_len) {
memmove(&array->data[mm_dst * elt_size],
&array->data[mm_src * elt_size],
mm_len * elt_size);
mm_dst += mm_len;
mm_src += mm_len + 1;
mm_len = 0;
} else
mm_src++;
}
}
if (mm_len) {
memmove(&array->data[mm_dst * elt_size],
&array->data[mm_src * elt_size],
mm_len * elt_size);
}
g_array_set_size(array, res_length);
}
/*****************************************************************************/
typedef struct {
pid_t pid;
NMLogDomain log_domain;
union {
struct {
gint64 wait_start_us;
guint source_timeout_kill_id;
} async;
struct {
gboolean success;
int child_status;
} sync;
};
NMUtilsKillChildAsyncCb callback;
void *user_data;
char log_name[1]; /* variable-length object, must be last element!! */
} KillChildAsyncData;
#define LOG_NAME_FMT "kill child process '%s' (%ld)"
#define LOG_NAME_PROCESS_FMT "kill process '%s' (%ld)"
#define LOG_NAME_ARGS log_name, (long) pid
static KillChildAsyncData *
_kc_async_data_alloc(pid_t pid,
NMLogDomain log_domain,
const char *log_name,
NMUtilsKillChildAsyncCb callback,
void *user_data)
{
KillChildAsyncData *data;
size_t log_name_len;
/* append the name at the end of our KillChildAsyncData. */
log_name_len = strlen(LOG_NAME_FMT) + 20 + strlen(log_name);
data = g_malloc(sizeof(KillChildAsyncData) - 1 + log_name_len);
g_snprintf(data->log_name, log_name_len, LOG_NAME_FMT, LOG_NAME_ARGS);
data->pid = pid;
data->user_data = user_data;
data->callback = callback;
data->log_domain = log_domain;
return data;
}
#define KC_EXIT_TO_STRING_BUF_SIZE 128
static const char *
_kc_exit_to_string(char *buf, int exit)
#define _kc_exit_to_string(buf, exit) \
(G_STATIC_ASSERT_EXPR(sizeof(buf) == KC_EXIT_TO_STRING_BUF_SIZE && sizeof((buf)[0]) == 1), \
_kc_exit_to_string(buf, exit))
{
if (WIFEXITED(exit))
g_snprintf(buf, KC_EXIT_TO_STRING_BUF_SIZE, "normally with status %d", WEXITSTATUS(exit));
else if (WIFSIGNALED(exit))
g_snprintf(buf, KC_EXIT_TO_STRING_BUF_SIZE, "by signal %d", WTERMSIG(exit));
else
g_snprintf(buf, KC_EXIT_TO_STRING_BUF_SIZE, "with unexpected status %d", exit);
return buf;
}
static const char *
_kc_signal_to_string(int sig)
{
switch (sig) {
case 0:
return "no signal (0)";
case SIGKILL:
return "SIGKILL (" G_STRINGIFY(SIGKILL) ")";
case SIGTERM:
return "SIGTERM (" G_STRINGIFY(SIGTERM) ")";
default:
return "Unexpected signal";
}
}
#define KC_WAITED_TO_STRING 100
static const char *
_kc_waited_to_string(char *buf, gint64 wait_start_us)
#define _kc_waited_to_string(buf, wait_start_us) \
(G_STATIC_ASSERT_EXPR(sizeof(buf) == KC_WAITED_TO_STRING && sizeof((buf)[0]) == 1), \
_kc_waited_to_string(buf, wait_start_us))
{
g_snprintf(buf,
KC_WAITED_TO_STRING,
" (%ld usec elapsed)",
(long) (nm_utils_get_monotonic_timestamp_usec() - wait_start_us));
return buf;
}
static void
_kc_cb_watch_child(GPid pid, int status, gpointer user_data)
{
KillChildAsyncData *data = user_data;
char buf_exit[KC_EXIT_TO_STRING_BUF_SIZE], buf_wait[KC_WAITED_TO_STRING];
if (data->async.source_timeout_kill_id)
g_source_remove(data->async.source_timeout_kill_id);
nm_log_dbg(data->log_domain,
"%s: terminated %s%s",
data->log_name,
_kc_exit_to_string(buf_exit, status),
_kc_waited_to_string(buf_wait, data->async.wait_start_us));
if (data->callback)
data->callback(pid, TRUE, status, data->user_data);
g_free(data);
}
static gboolean
_kc_cb_timeout_grace_period(void *user_data)
{
KillChildAsyncData *data = user_data;
int ret, errsv;
data->async.source_timeout_kill_id = 0;
if ((ret = kill(data->pid, SIGKILL)) != 0) {
errsv = errno;
/* ESRCH means, process does not exist or is already a zombie. */
if (errsv != ESRCH) {
nm_log_err(LOGD_CORE | data->log_domain,
"%s: kill(SIGKILL) returned unexpected return value %d: (%s, %d)",
data->log_name,
ret,
nm_strerror_native(errsv),
errsv);
}
} else {
nm_log_dbg(data->log_domain,
"%s: process not terminated after %ld usec. Sending SIGKILL signal",
data->log_name,
(long) (nm_utils_get_monotonic_timestamp_usec() - data->async.wait_start_us));
}
return G_SOURCE_REMOVE;
}
static gboolean
_kc_invoke_callback_idle(gpointer user_data)
{
KillChildAsyncData *data = user_data;
if (data->sync.success) {
char buf_exit[KC_EXIT_TO_STRING_BUF_SIZE];
nm_log_dbg(data->log_domain,
"%s: invoke callback: terminated %s",
data->log_name,
_kc_exit_to_string(buf_exit, data->sync.child_status));
} else
nm_log_dbg(data->log_domain, "%s: invoke callback: killing child failed", data->log_name);
data->callback(data->pid, data->sync.success, data->sync.child_status, data->user_data);
g_free(data);
return G_SOURCE_REMOVE;
}
static void
_kc_invoke_callback(pid_t pid,
NMLogDomain log_domain,
const char *log_name,
NMUtilsKillChildAsyncCb callback,
void *user_data,
gboolean success,
int child_status)
{
KillChildAsyncData *data;
if (!callback)
return;
data = _kc_async_data_alloc(pid, log_domain, log_name, callback, user_data);
data->sync.success = success;
data->sync.child_status = child_status;
nm_g_idle_add(_kc_invoke_callback_idle, data);
}
/* nm_utils_kill_child_async:
* @pid: the process id of the process to kill
* @sig: signal to send initially. Set to 0 to send not signal.
* @log_domain: the logging domain used for logging (LOGD_NONE to suppress logging)
* @log_name: for logging, the name of the processes to kill
* @wait_before_kill_msec: Waittime in milliseconds before sending %SIGKILL signal. Set this value
* to zero, not to send %SIGKILL. If @sig is already %SIGKILL, this parameter is ignored.
* @callback: (nullable): callback after the child terminated. This function will always
* be invoked asynchronously.
* @user_data: passed on to callback
*
* Uses g_child_watch_add(), so note the glib comment: if you obtain pid from g_spawn_async() or
* g_spawn_async_with_pipes() you will need to pass %G_SPAWN_DO_NOT_REAP_CHILD as flag to the spawn
* function for the child watching to work.
* Also note, that you must g_source_remove() any other child watchers for @pid because glib
* supports only one watcher per child.
**/
void
nm_utils_kill_child_async(pid_t pid,
int sig,
NMLogDomain log_domain,
const char *log_name,
guint32 wait_before_kill_msec,
NMUtilsKillChildAsyncCb callback,
void *user_data)
{
int status = 0, errsv;
pid_t ret;
KillChildAsyncData *data;
char buf_exit[KC_EXIT_TO_STRING_BUF_SIZE];
g_return_if_fail(pid > 0);
g_return_if_fail(log_name != NULL);
/* let's see if the child already terminated... */
ret = waitpid(pid, &status, WNOHANG);
if (ret > 0) {
nm_log_dbg(log_domain,
LOG_NAME_FMT ": process %ld already terminated %s",
LOG_NAME_ARGS,
(long) ret,
_kc_exit_to_string(buf_exit, status));
_kc_invoke_callback(pid, log_domain, log_name, callback, user_data, TRUE, status);
return;
} else if (ret != 0) {
errsv = errno;
nm_log_err(LOGD_CORE | log_domain,
LOG_NAME_FMT ": unexpected error while waitpid: %s (%d)",
LOG_NAME_ARGS,
nm_strerror_native(errsv),
errsv);
_kc_invoke_callback(pid, log_domain, log_name, callback, user_data, FALSE, -1);
return;
}
/* send the first signal. */
if (kill(pid, sig) != 0) {
errsv = errno;
/* ESRCH means, process does not exist or is already a zombie. */
if (errsv != ESRCH) {
nm_log_err(LOGD_CORE | log_domain,
LOG_NAME_FMT ": unexpected error sending %s: %s (%d)",
LOG_NAME_ARGS,
_kc_signal_to_string(sig),
nm_strerror_native(errsv),
errsv);
_kc_invoke_callback(pid, log_domain, log_name, callback, user_data, FALSE, -1);
return;
}
/* let's try again with waitpid, probably there was a race... */
ret = waitpid(pid, &status, 0);
if (ret > 0) {
nm_log_dbg(log_domain,
LOG_NAME_FMT ": process %ld already terminated %s",
LOG_NAME_ARGS,
(long) ret,
_kc_exit_to_string(buf_exit, status));
_kc_invoke_callback(pid, log_domain, log_name, callback, user_data, TRUE, status);
} else {
errsv = errno;
nm_log_err(
LOGD_CORE | log_domain,
LOG_NAME_FMT
": failed due to unexpected return value %ld by waitpid (%s, %d) after sending %s",
LOG_NAME_ARGS,
(long) ret,
nm_strerror_native(errsv),
errsv,
_kc_signal_to_string(sig));
_kc_invoke_callback(pid, log_domain, log_name, callback, user_data, FALSE, -1);
}
return;
}
data = _kc_async_data_alloc(pid, log_domain, log_name, callback, user_data);
data->async.wait_start_us = nm_utils_get_monotonic_timestamp_usec();
if (sig != SIGKILL && wait_before_kill_msec > 0) {
data->async.source_timeout_kill_id =
g_timeout_add(wait_before_kill_msec, _kc_cb_timeout_grace_period, data);
nm_log_dbg(log_domain,
"%s: wait for process to terminate after sending %s (send SIGKILL in %ld "
"milliseconds)...",
data->log_name,
_kc_signal_to_string(sig),
(long) wait_before_kill_msec);
} else {
data->async.source_timeout_kill_id = 0;
nm_log_dbg(log_domain,
"%s: wait for process to terminate after sending %s...",
data->log_name,
_kc_signal_to_string(sig));
}
g_child_watch_add(pid, _kc_cb_watch_child, data);
}
static gulong
_sleep_duration_convert_ms_to_us(guint32 sleep_duration_msec)
{
if (sleep_duration_msec > 0) {
guint64 x = ((guint64) sleep_duration_msec) * 1000UL;
nm_assert(x < G_MAXULONG);
return x;
}
return G_USEC_PER_SEC / 20;
}
/* nm_utils_kill_child_sync:
* @pid: process id to kill
* @sig: signal to sent initially. If 0, no signal is sent. If %SIGKILL, the
* second %SIGKILL signal is not sent after @wait_before_kill_msec milliseconds.
* @log_domain: log debug information for this domain. Errors and warnings are logged both
* as %LOGD_CORE and @log_domain.
* @log_name: name of the process to kill for logging.
* @child_status: (out) (optional): return the exit status of the child, if no error occurred.
* @wait_before_kill_msec: Waittime in milliseconds before sending %SIGKILL signal. Set this value
* to zero, not to send %SIGKILL. If @sig is already %SIGKILL, this parameter has not effect.
* @sleep_duration_msec: the synchronous function sleeps repeatedly waiting for the child to terminate.
* Set to zero, to use the default (meaning 20 wakeups per seconds).
*
* Kill a child process synchronously and wait. The function first checks if the child already terminated
* and if it did, return the exit status. Otherwise, send one @sig signal. @sig will always be
* sent unless the child already exited. If the child does not exit within @wait_before_kill_msec milliseconds,
* the function will send %SIGKILL and waits for the child indefinitely. If @wait_before_kill_msec is zero, no
* %SIGKILL signal will be sent.
*
* In case of error, errno is preserved to contain the last reason of failure.
**/
gboolean
nm_utils_kill_child_sync(pid_t pid,
int sig,
NMLogDomain log_domain,
const char *log_name,
int *child_status,
guint32 wait_before_kill_msec,
guint32 sleep_duration_msec)
{
int status = 0, errsv = 0;
pid_t ret;
gboolean success = FALSE;
gboolean was_waiting = FALSE, send_kill = FALSE;
char buf_exit[KC_EXIT_TO_STRING_BUF_SIZE];
char buf_wait[KC_WAITED_TO_STRING];
gint64 wait_start_us;
g_return_val_if_fail(pid > 0, FALSE);
g_return_val_if_fail(log_name != NULL, FALSE);
/* check if the child process already terminated... */
ret = waitpid(pid, &status, WNOHANG);
if (ret > 0) {
nm_log_dbg(log_domain,
LOG_NAME_FMT ": process %ld already terminated %s",
LOG_NAME_ARGS,
(long) ret,
_kc_exit_to_string(buf_exit, status));
success = TRUE;
goto out;
} else if (ret != 0) {
errsv = errno;
nm_log_err(LOGD_CORE | log_domain,
LOG_NAME_FMT ": unexpected error while waitpid: %s (%d)",
LOG_NAME_ARGS,
nm_strerror_native(errsv),
errsv);
goto out;
}
/* send first signal @sig */
if (kill(pid, sig) != 0) {
errsv = errno;
/* ESRCH means, process does not exist or is already a zombie. */
if (errsv != ESRCH) {
nm_log_err(LOGD_CORE | log_domain,
LOG_NAME_FMT ": failed to send %s: %s (%d)",
LOG_NAME_ARGS,
_kc_signal_to_string(sig),
nm_strerror_native(errsv),
errsv);
} else {
/* let's try again with waitpid, probably there was a race... */
ret = waitpid(pid, &status, 0);
if (ret > 0) {
nm_log_dbg(log_domain,
LOG_NAME_FMT ": process %ld already terminated %s",
LOG_NAME_ARGS,
(long) ret,
_kc_exit_to_string(buf_exit, status));
success = TRUE;
} else {
errsv = errno;
nm_log_err(LOGD_CORE | log_domain,
LOG_NAME_FMT ": failed due to unexpected return value %ld by waitpid "
"(%s, %d) after sending %s",
LOG_NAME_ARGS,
(long) ret,
nm_strerror_native(errsv),
errsv,
_kc_signal_to_string(sig));
}
}
goto out;
}
wait_start_us = nm_utils_get_monotonic_timestamp_usec();
/* wait for the process to terminated... */
if (sig != SIGKILL) {
gint64 wait_until, now;
gulong sleep_time, sleep_duration_usec;
int loop_count = 0;
sleep_duration_usec = _sleep_duration_convert_ms_to_us(sleep_duration_msec);
wait_until = wait_before_kill_msec <= 0
? 0
: wait_start_us + (((gint64) wait_before_kill_msec) * 1000L);
while (TRUE) {
ret = waitpid(pid, &status, WNOHANG);
if (ret > 0) {
nm_log_dbg(log_domain,
LOG_NAME_FMT ": after sending %s, process %ld exited %s%s",
LOG_NAME_ARGS,
_kc_signal_to_string(sig),
(long) ret,
_kc_exit_to_string(buf_exit, status),
was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
success = TRUE;
goto out;
}
if (ret == -1) {
errsv = errno;
/* ECHILD means, the process is not a child/does not exist or it has SIGCHILD blocked. */
if (errsv != ECHILD) {
nm_log_err(LOGD_CORE | log_domain,
LOG_NAME_FMT ": after sending %s, waitpid failed with %s (%d)%s",
LOG_NAME_ARGS,
_kc_signal_to_string(sig),
nm_strerror_native(errsv),
errsv,
was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
goto out;
}
}
if (!wait_until)
break;
now = nm_utils_get_monotonic_timestamp_usec();
if (now >= wait_until)
break;
if (!was_waiting) {
nm_log_dbg(log_domain,
LOG_NAME_FMT ": waiting up to %lu milliseconds for process to terminate "
"normally after sending %s...",
LOG_NAME_ARGS,
(unsigned long) wait_before_kill_msec,
_kc_signal_to_string(sig));
was_waiting = TRUE;
}
sleep_time = NM_MIN(wait_until - now, (gint64) sleep_duration_usec);
if (loop_count < 20) {
/* At the beginning we expect the process to die fast.
* Limit the sleep time, the limit doubles with every iteration. */
sleep_time =
NM_MIN(sleep_time, (((guint64) 1) << loop_count) * G_USEC_PER_SEC / 2000);
loop_count++;
}
g_usleep(sleep_time);
}
/* send SIGKILL, if called with @wait_before_kill_msec > 0 */
if (wait_until) {
nm_log_dbg(log_domain, LOG_NAME_FMT ": sending SIGKILL...", LOG_NAME_ARGS);
send_kill = TRUE;
if (kill(pid, SIGKILL) != 0) {
errsv = errno;
/* ESRCH means, process does not exist or is already a zombie. */
if (errsv != ESRCH) {
nm_log_err(LOGD_CORE | log_domain,
LOG_NAME_FMT ": failed to send SIGKILL (after sending %s), %s (%d)",
LOG_NAME_ARGS,
_kc_signal_to_string(sig),
nm_strerror_native(errsv),
errsv);
goto out;
}
}
}
}
if (!was_waiting) {
nm_log_dbg(log_domain,
LOG_NAME_FMT ": waiting for process to terminate after sending %s%s...",
LOG_NAME_ARGS,
_kc_signal_to_string(sig),
send_kill ? " and SIGKILL" : "");
}
/* block until the child terminates. */
while ((ret = waitpid(pid, &status, 0)) <= 0) {
errsv = errno;
if (errsv != EINTR) {
nm_log_err(LOGD_CORE | log_domain,
LOG_NAME_FMT ": after sending %s%s, waitpid failed with %s (%d)%s",
LOG_NAME_ARGS,
_kc_signal_to_string(sig),
send_kill ? " and SIGKILL" : "",
nm_strerror_native(errsv),
errsv,
_kc_waited_to_string(buf_wait, wait_start_us));
goto out;
}
}
nm_log_dbg(log_domain,
LOG_NAME_FMT ": after sending %s%s, process %ld exited %s%s",
LOG_NAME_ARGS,
_kc_signal_to_string(sig),
send_kill ? " and SIGKILL" : "",
(long) ret,
_kc_exit_to_string(buf_exit, status),
_kc_waited_to_string(buf_wait, wait_start_us));
success = TRUE;
out:
if (child_status)
*child_status = success ? status : -1;
errno = success ? 0 : errsv;
return success;
}
/* nm_utils_kill_process_sync:
* @pid: process id to kill
* @start_time: the start time of the process to kill (as obtained by nm_utils_get_start_time_for_pid()).
* This is an optional argument, to avoid (somewhat) killing the wrong process as @pid
* might get recycled. You can pass 0, to not provide this parameter.
* @sig: signal to sent initially. If 0, no signal is sent. If %SIGKILL, the
* second %SIGKILL signal is not sent after @wait_before_kill_msec milliseconds.
* @log_domain: log debug information for this domain. Errors and warnings are logged both
* as %LOGD_CORE and @log_domain.
* @log_name: name of the process to kill for logging.
* @wait_before_kill_msec: Waittime in milliseconds before sending %SIGKILL signal. Set this value
* to zero, not to send %SIGKILL. If @sig is already %SIGKILL, this parameter has no effect.
* If @max_wait_msec is set but less then @wait_before_kill_msec, the final %SIGKILL will also
* not be send.
* @sleep_duration_msec: the synchronous function sleeps repeatedly waiting for the child to terminate.
* Set to zero, to use the default (meaning 20 wakeups per seconds).
* @max_wait_msec: if 0, waits indefinitely until the process is gone (or a zombie). Otherwise, this
* is the maximum wait time until returning. If @max_wait_msec is non-zero but smaller then @wait_before_kill_msec,
* we will not send a final %SIGKILL.
*
* Kill a non-child process synchronously and wait. This function will not return before the
* process with PID @pid is gone, the process is a zombie, or @max_wait_msec expires.
**/
void
nm_utils_kill_process_sync(pid_t pid,
guint64 start_time,
int sig,
NMLogDomain log_domain,
const char *log_name,
guint32 wait_before_kill_msec,
guint32 sleep_duration_msec,
guint32 max_wait_msec)
{
int errsv;
guint64 start_time0;
gint64 wait_until_sigkill, now, wait_start_us, max_wait_until;
gulong sleep_time, sleep_duration_usec;
int loop_count = 0;
gboolean was_waiting = FALSE;
char buf_wait[KC_WAITED_TO_STRING];
char p_state;
g_return_if_fail(pid > 0);
g_return_if_fail(log_name != NULL);
start_time0 = nm_utils_get_start_time_for_pid(pid, &p_state, NULL);
if (start_time0 == 0) {
nm_log_dbg(log_domain,
LOG_NAME_PROCESS_FMT ": cannot kill process %ld because it seems already gone",
LOG_NAME_ARGS,
(long int) pid);
return;
}
if (start_time != 0 && start_time != start_time0) {
nm_log_dbg(
log_domain,
LOG_NAME_PROCESS_FMT
": don't kill process %ld because the start_time is unexpectedly %lu instead of %ld",
LOG_NAME_ARGS,
(long int) pid,
(unsigned long) start_time0,
(unsigned long) start_time);
return;
}
switch (p_state) {
case 'Z':
case 'x':
case 'X':
nm_log_dbg(log_domain,
LOG_NAME_PROCESS_FMT
": cannot kill process %ld because it is already a zombie (%c)",
LOG_NAME_ARGS,
(long int) pid,
p_state);
return;
default:
break;
}
if (kill(pid, sig) != 0) {
errsv = errno;
/* ESRCH means, process does not exist or is already a zombie. */
if (errsv == ESRCH) {
nm_log_dbg(log_domain,
LOG_NAME_PROCESS_FMT ": failed to send %s because process seems gone",
LOG_NAME_ARGS,
_kc_signal_to_string(sig));
} else {
nm_log_warn(LOGD_CORE | log_domain,
LOG_NAME_PROCESS_FMT ": failed to send %s: %s (%d)",
LOG_NAME_ARGS,
_kc_signal_to_string(sig),
nm_strerror_native(errsv),
errsv);
}
return;
}
/* wait for the process to terminate... */
wait_start_us = nm_utils_get_monotonic_timestamp_usec();
sleep_duration_usec = _sleep_duration_convert_ms_to_us(sleep_duration_msec);
if (sig != SIGKILL && wait_before_kill_msec)
wait_until_sigkill = wait_start_us + (((gint64) wait_before_kill_msec) * 1000L);
else
wait_until_sigkill = 0;
if (max_wait_msec > 0) {
max_wait_until = wait_start_us + (((gint64) max_wait_msec) * 1000L);
if (wait_until_sigkill > 0 && wait_until_sigkill > max_wait_msec)
wait_until_sigkill = 0;
} else
max_wait_until = 0;
while (TRUE) {
start_time = nm_utils_get_start_time_for_pid(pid, &p_state, NULL);
if (start_time != start_time0) {
nm_log_dbg(log_domain,
LOG_NAME_PROCESS_FMT ": process is gone after sending signal %s%s",
LOG_NAME_ARGS,
_kc_signal_to_string(sig),
was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
return;
}
switch (p_state) {
case 'Z':
case 'x':
case 'X':
nm_log_dbg(log_domain,
LOG_NAME_PROCESS_FMT ": process is a zombie (%c) after sending signal %s%s",
LOG_NAME_ARGS,
p_state,
_kc_signal_to_string(sig),
was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
return;
default:
break;
}
if (kill(pid, 0) != 0) {
errsv = errno;
/* ESRCH means, process does not exist or is already a zombie. */
if (errsv == ESRCH) {
nm_log_dbg(log_domain,
LOG_NAME_PROCESS_FMT
": process is gone or a zombie after sending signal %s%s",
LOG_NAME_ARGS,
_kc_signal_to_string(sig),
was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
} else {
nm_log_warn(LOGD_CORE | log_domain,
LOG_NAME_PROCESS_FMT ": failed to kill(%ld, 0): %s (%d)%s",
LOG_NAME_ARGS,
(long int) pid,
nm_strerror_native(errsv),
errsv,
was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
}
return;
}
sleep_time = sleep_duration_usec;
now = nm_utils_get_monotonic_timestamp_usec();
if (max_wait_until != 0 && now >= max_wait_until) {
if (wait_until_sigkill != 0) {
/* wait_before_kill_msec is not larger then max_wait_until but we did not yet send
* SIGKILL. Although we already reached our timeout, we don't want to skip sending
* the signal. Even if we don't wait for the process to disappear. */
nm_log_dbg(log_domain, LOG_NAME_PROCESS_FMT ": sending SIGKILL", LOG_NAME_ARGS);
kill(pid, SIGKILL);
}
nm_log_warn(log_domain,
LOG_NAME_PROCESS_FMT
": timeout %u msec waiting for process to disappear (after sending %s)%s",
LOG_NAME_ARGS,
(unsigned) max_wait_until,
_kc_signal_to_string(sig),
was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
return;
}
if (wait_until_sigkill != 0) {
if (now >= wait_until_sigkill) {
/* Still not dead. SIGKILL now... */
nm_log_dbg(log_domain, LOG_NAME_PROCESS_FMT ": sending SIGKILL", LOG_NAME_ARGS);
if (kill(pid, SIGKILL) != 0) {
errsv = errno;
/* ESRCH means, process does not exist or is already a zombie. */
if (errsv != ESRCH) {
nm_log_dbg(log_domain,
LOG_NAME_PROCESS_FMT ": process is gone or a zombie%s",
LOG_NAME_ARGS,
_kc_waited_to_string(buf_wait, wait_start_us));
} else {
nm_log_warn(LOGD_CORE | log_domain,
LOG_NAME_PROCESS_FMT
": failed to send SIGKILL (after sending %s), %s (%d)%s",
LOG_NAME_ARGS,
_kc_signal_to_string(sig),
nm_strerror_native(errsv),
errsv,
_kc_waited_to_string(buf_wait, wait_start_us));
}
return;
}
sig = SIGKILL;
wait_until_sigkill = 0;
loop_count =
0; /* reset the loop_count. Now we really expect the process to die quickly. */
} else
sleep_time = NM_MIN(wait_until_sigkill - now, (gint64) sleep_duration_usec);
}
if (!was_waiting) {
if (wait_until_sigkill != 0) {
nm_log_dbg(log_domain,
LOG_NAME_PROCESS_FMT
": waiting up to %lu milliseconds for process to disappear before "
"sending KILL signal after sending %s...",
LOG_NAME_ARGS,
(unsigned long) wait_before_kill_msec,
_kc_signal_to_string(sig));
} else if (max_wait_until != 0) {
nm_log_dbg(
log_domain,
LOG_NAME_PROCESS_FMT
": waiting up to %ld milliseconds for process to disappear after sending %s...",
LOG_NAME_ARGS,
(long) max_wait_msec,
_kc_signal_to_string(sig));
} else {
nm_log_dbg(log_domain,
LOG_NAME_PROCESS_FMT
": waiting for process to disappear after sending %s...",
LOG_NAME_ARGS,
_kc_signal_to_string(sig));
}
was_waiting = TRUE;
}
if (loop_count < 20) {
/* At the beginning we expect the process to die fast.
* Limit the sleep time, the limit doubles with every iteration. */
sleep_time = NM_MIN(sleep_time, (((guint64) 1) << loop_count) * G_USEC_PER_SEC / 2000);
loop_count++;
}
g_usleep(sleep_time);
}
}
#undef LOG_NAME_FMT
#undef LOG_NAME_PROCESS_FMT
#undef LOG_NAME_ARGS
const char *const NM_PATHS_DEFAULT[] = {
PREFIX "/sbin/",
PREFIX "/bin/",
"/usr/local/sbin/",
"/sbin/",
"/usr/sbin/",
"/usr/local/bin/",
"/bin/",
"/usr/bin/",
NULL,
};
const char *
nm_utils_find_helper(const char *progname, const char *try_first, GError **error)
{
return nm_utils_file_search_in_paths(progname,
try_first,
NM_PATHS_DEFAULT,
G_FILE_TEST_IS_EXECUTABLE,
NULL,
NULL,
error);
}
/*****************************************************************************/
/**
* nm_utils_read_link_absolute:
* @link_file: file name of the symbolic link
* @error: error reason in case of failure
*
* Uses to g_file_read_link()/readlink() to read the symlink
* and returns the result as absolute path.
**/
char *
nm_utils_read_link_absolute(const char *link_file, GError **error)
{
char *ln, *dirname, *ln_abs;
ln = g_file_read_link(link_file, error);
if (!ln)
return NULL;
if (g_path_is_absolute(ln))
return ln;
dirname = g_path_get_dirname(link_file);
if (!g_path_is_absolute(dirname)) {
gs_free char *current_dir = g_get_current_dir();
/* @link_file argument was not an absolute path in the first place.
* That actually may be a bug, because the CWD is not well defined
* in most cases. Anyway, apparently we were able to load the file
* even from a relative path. So, when making the link absolute, we
* also need to prepend the CWD. */
ln_abs = g_build_filename(current_dir, dirname, ln, NULL);
} else
ln_abs = g_build_filename(dirname, ln, NULL);
g_free(dirname);
g_free(ln);
return ln_abs;
}
/*****************************************************************************/
#define DEVICE_TYPE_TAG "type:"
#define DRIVER_TAG "driver:"
#define DHCP_PLUGIN_TAG "dhcp-plugin:"
#define EXCEPT_TAG "except:"
#define MATCH_TAG_CONFIG_NM_VERSION "nm-version:"
#define MATCH_TAG_CONFIG_NM_VERSION_MIN "nm-version-min:"
#define MATCH_TAG_CONFIG_NM_VERSION_MAX "nm-version-max:"
#define MATCH_TAG_CONFIG_ENV "env:"
typedef struct {
/* This struct contains pre-processed data from NMMatchSpecDeviceData so
* we only need to parse it once. */
const NMMatchSpecDeviceData *data;
const char *device_type;
const char *driver;
const char *driver_version;
const char *dhcp_plugin;
struct {
gboolean is_parsed;
guint len;
guint8 bin[_NM_UTILS_HWADDR_LEN_MAX];
} hwaddr;
struct {
gboolean is_parsed;
gboolean is_good;
guint32 a;
guint32 b;
guint32 c;
} s390_subchannels;
} MatchSpecDeviceData;
static gboolean
match_device_s390_subchannels_parse(const char *s390_subchannels,
guint32 *out_a,
guint32 *out_b,
guint32 *out_c)
{
char buf[30 + 1];
const int BUFSIZE = G_N_ELEMENTS(buf) - 1;
guint i = 0;
char *pa = NULL, *pb = NULL, *pc = NULL;
gint64 a, b, c;
nm_assert(s390_subchannels);
nm_assert(out_a);
nm_assert(out_b);
nm_assert(out_c);
if (!g_ascii_isxdigit(s390_subchannels[0]))
return FALSE;
/* Get the first channel */
for (i = 0; s390_subchannels[i]; i++) {
char ch = s390_subchannels[i];
if (!g_ascii_isxdigit(ch) && ch != '.') {
if (ch == ',') {
/* FIXME: currently we consider the first channel and ignore
* everything after the first ',' separator. Maybe we should
* validate all present channels? */
break;
}
return FALSE; /* Invalid chars */
}
if (i >= BUFSIZE)
return FALSE; /* Too long to be a subchannel */
buf[i] = ch;
}
buf[i] = '\0';
/* and grab each of its elements, there should be 3 */
pa = &buf[0];
pb = strchr(pa, '.');
if (pb)
pc = strchr(pb + 1, '.');
if (!pb || !pc)
return FALSE;
*pb++ = '\0';
*pc++ = '\0';
a = _nm_utils_ascii_str_to_int64(pa, 16, 0, G_MAXUINT32, -1);
if (a == -1)
return FALSE;
b = _nm_utils_ascii_str_to_int64(pb, 16, 0, G_MAXUINT32, -1);
if (b == -1)
return FALSE;
c = _nm_utils_ascii_str_to_int64(pc, 16, 0, G_MAXUINT32, -1);
if (c == -1)
return FALSE;
*out_a = (guint32) a;
*out_b = (guint32) b;
*out_c = (guint32) c;
return TRUE;
}
static gboolean
match_data_s390_subchannels_eval(const char *spec_str, MatchSpecDeviceData *match_data)
{
guint32 a;
guint32 b;
guint32 c;
if (G_UNLIKELY(!match_data->s390_subchannels.is_parsed)) {
nm_assert(!match_data->s390_subchannels.is_good);
match_data->s390_subchannels.is_parsed = TRUE;
if (!match_data->data->s390_subchannels
|| !match_device_s390_subchannels_parse(match_data->data->s390_subchannels,
&match_data->s390_subchannels.a,
&match_data->s390_subchannels.b,
&match_data->s390_subchannels.c)) {
return FALSE;
}
match_data->s390_subchannels.is_good = TRUE;
} else if (!match_data->s390_subchannels.is_good)
return FALSE;
if (!match_device_s390_subchannels_parse(spec_str, &a, &b, &c))
return FALSE;
return match_data->s390_subchannels.a == a && match_data->s390_subchannels.b == b
&& match_data->s390_subchannels.c == c;
}
static gboolean
match_device_hwaddr_eval(const char *spec_str, MatchSpecDeviceData *match_data)
{
if (G_UNLIKELY(!match_data->hwaddr.is_parsed)) {
match_data->hwaddr.is_parsed = TRUE;
nm_assert(match_data->hwaddr.len == 0);
if (match_data->data->hwaddr) {
gsize l;
if (!_nm_utils_hwaddr_aton(match_data->data->hwaddr,
match_data->hwaddr.bin,
sizeof(match_data->hwaddr.bin),
&l))
g_return_val_if_reached(FALSE);
match_data->hwaddr.len = l;
} else
return FALSE;
} else if (match_data->hwaddr.len == 0)
return FALSE;
return nm_utils_hwaddr_matches(spec_str, -1, match_data->hwaddr.bin, match_data->hwaddr.len);
}
#define _MATCH_CHECK(spec_str, tag) \
({ \
gboolean _has = FALSE; \
\
if (!g_ascii_strncasecmp(spec_str, ("" tag ""), NM_STRLEN(tag))) { \
spec_str += NM_STRLEN(tag); \
_has = TRUE; \
} \
_has; \
})
static NMMatchSpecMatchType
_match_result(gboolean has_except,
gboolean has_not_except,
gboolean has_match,
gboolean has_match_except)
{
if (has_except && !has_not_except) {
/* a match spec that only consists of a list of except matches is treated specially. */
nm_assert(!has_match);
if (has_match_except) {
/* one of the "except:" matches matched. The result is an explicit
* negative match. */
return NM_MATCH_SPEC_NEG_MATCH;
} else {
/* none of the "except:" matches matched. The result is a positive match,
* despite there being no positive match. */
return NM_MATCH_SPEC_MATCH;
}
}
if (has_match_except)
return NM_MATCH_SPEC_NEG_MATCH;
if (has_match)
return NM_MATCH_SPEC_MATCH;
return NM_MATCH_SPEC_NO_MATCH;
}
static const char *
match_except(const char *spec_str, gboolean *out_except)
{
if (_MATCH_CHECK(spec_str, EXCEPT_TAG))
*out_except = TRUE;
else
*out_except = FALSE;
return spec_str;
}
static gboolean
match_device_eval(const char *spec_str, gboolean allow_fuzzy, MatchSpecDeviceData *match_data)
{
if (spec_str[0] == '*' && spec_str[1] == '\0')
return TRUE;
if (_MATCH_CHECK(spec_str, DEVICE_TYPE_TAG)) {
return match_data->device_type && nm_streq(spec_str, match_data->device_type);
}
if (_MATCH_CHECK(spec_str, NM_MATCH_SPEC_MAC_TAG))
return match_device_hwaddr_eval(spec_str, match_data);
if (_MATCH_CHECK(spec_str, NM_MATCH_SPEC_INTERFACE_NAME_TAG)) {
gboolean use_pattern = FALSE;
if (spec_str[0] == '=')
spec_str += 1;
else {
if (spec_str[0] == '~')
spec_str += 1;
use_pattern = TRUE;
}
if (match_data->data->interface_name) {
if (nm_streq(spec_str, match_data->data->interface_name))
return TRUE;
if (use_pattern && g_pattern_match_simple(spec_str, match_data->data->interface_name))
return TRUE;
}
return FALSE;
}
if (_MATCH_CHECK(spec_str, DRIVER_TAG)) {
const char *t;
if (!match_data->driver)
return FALSE;
/* support:
* 1) "${DRIVER}"
* In this case, DRIVER may not contain a '/' character.
* It matches any driver version.
* 2) "${DRIVER}/${DRIVER_VERSION}"
* In this case, DRIVER may contains '/' but DRIVER_VERSION
* may not. A '/' in DRIVER_VERSION may be replaced by '?'.
*
* It follows, that "${DRIVER}/""*" is like 1), but allows
* '/' inside DRIVER.
*
* The fields match to what `nmcli -f GENERAL.DRIVER,GENERAL.DRIVER-VERSION device show`
* gives. However, DRIVER matches literally, while DRIVER_VERSION is a glob
* supporting ? and *.
*/
t = strrchr(spec_str, '/');
if (!t)
return nm_streq(spec_str, match_data->driver);
return (strncmp(spec_str, match_data->driver, t - spec_str) == 0)
&& g_pattern_match_simple(&t[1], match_data->driver_version ?: "");
}
if (_MATCH_CHECK(spec_str, NM_MATCH_SPEC_S390_SUBCHANNELS_TAG))
return match_data_s390_subchannels_eval(spec_str, match_data);
if (_MATCH_CHECK(spec_str, DHCP_PLUGIN_TAG))
return nm_streq0(spec_str, match_data->dhcp_plugin);
if (allow_fuzzy) {
if (match_device_hwaddr_eval(spec_str, match_data))
return TRUE;
if (match_data->data->interface_name
&& nm_streq(spec_str, match_data->data->interface_name))
return TRUE;
}
return FALSE;
}
NMMatchSpecMatchType
nm_match_spec_device(const GSList *specs, const NMMatchSpecDeviceData *data)
{
const GSList *iter;
gboolean has_match = FALSE;
gboolean has_match_except = FALSE;
gboolean has_except = FALSE;
gboolean has_not_except = FALSE;
const char *spec_str;
MatchSpecDeviceData match_data;
nm_assert(data);
nm_assert(!data->hwaddr || nm_utils_hwaddr_valid(data->hwaddr, -1));
if (!specs)
return NM_MATCH_SPEC_NO_MATCH;
match_data = (MatchSpecDeviceData) {
.data = data,
.device_type = nm_str_not_empty(data->device_type),
.driver = nm_str_not_empty(data->driver),
.driver_version = nm_str_not_empty(data->driver_version),
.dhcp_plugin = nm_str_not_empty(data->dhcp_plugin),
.hwaddr =
{
.is_parsed = FALSE,
.len = 0,
},
.s390_subchannels =
{
.is_parsed = FALSE,
.is_good = FALSE,
},
};
for (iter = specs; iter; iter = iter->next) {
gboolean except;
spec_str = iter->data;
if (!spec_str || !*spec_str)
continue;
spec_str = match_except(spec_str, &except);
if (except)
has_except = TRUE;
else
has_not_except = TRUE;
if ((except && has_match_except) || (!except && has_match)) {
/* evaluating the match does not give new information. Skip it. */
continue;
}
if (!match_device_eval(spec_str, !except, &match_data))
continue;
if (except)
has_match_except = TRUE;
else
has_match = TRUE;
}
return _match_result(has_except, has_not_except, has_match, has_match_except);
}
int
nm_match_spec_match_type_to_bool(NMMatchSpecMatchType m, int no_match_value)
{
switch (m) {
case NM_MATCH_SPEC_MATCH:
return TRUE;
case NM_MATCH_SPEC_NEG_MATCH:
return FALSE;
case NM_MATCH_SPEC_NO_MATCH:
return no_match_value;
}
return nm_assert_unreachable_val(no_match_value);
}
typedef struct {
const char *uuid;
const char *id;
const char *origin;
} MatchConnectionData;
static gboolean
match_connection_eval(const char *spec_str, const MatchConnectionData *match_data)
{
if (spec_str[0] == '*' && spec_str[1] == '\0')
return TRUE;
if (_MATCH_CHECK(spec_str, "id:"))
return nm_streq0(spec_str, match_data->id);
if (_MATCH_CHECK(spec_str, "uuid:"))
return nm_streq0(spec_str, match_data->uuid);
if (_MATCH_CHECK(spec_str, "origin:"))
return nm_streq0(spec_str, match_data->origin);
return FALSE;
}
static NMMatchSpecMatchType
match_spec_connection(const GSList *specs, const char *id, const char *uuid, const char *origin)
{
const GSList *iter;
gboolean has_match = FALSE;
gboolean has_match_except = FALSE;
gboolean has_except = FALSE;
gboolean has_not_except = FALSE;
const char *spec_str;
const MatchConnectionData match_data = {
.id = nm_str_not_empty(id),
.uuid = nm_str_not_empty(uuid),
.origin = nm_str_not_empty(origin),
};
if (!specs)
return NM_MATCH_SPEC_NO_MATCH;
for (iter = specs; iter; iter = iter->next) {
gboolean except;
spec_str = iter->data;
if (!spec_str || !*spec_str)
continue;
spec_str = match_except(spec_str, &except);
if (except)
has_except = TRUE;
else
has_not_except = TRUE;
if ((except && has_match_except) || (!except && has_match)) {
/* evaluating the match does not give new information. Skip it. */
continue;
}
if (!match_connection_eval(spec_str, &match_data))
continue;
if (except)
has_match_except = TRUE;
else
has_match = TRUE;
}
return _match_result(has_except, has_not_except, has_match, has_match_except);
}
int
nm_utils_connection_match_spec_list(NMConnection *connection,
const GSList *specs,
int no_match_value)
{
NMMatchSpecMatchType m;
NMSettingUser *s_user;
const char *origin = NULL;
if (!specs)
return no_match_value;
s_user = _nm_connection_get_setting(connection, NM_TYPE_SETTING_USER);
if (s_user)
origin = nm_setting_user_get_data(s_user, NM_USER_TAG_ORIGIN);
m = match_spec_connection(specs,
nm_connection_get_id(connection),
nm_connection_get_uuid(connection),
origin);
switch (m) {
case NM_MATCH_SPEC_MATCH:
return TRUE;
case NM_MATCH_SPEC_NEG_MATCH:
return FALSE;
case NM_MATCH_SPEC_NO_MATCH:
return no_match_value;
}
nm_assert_not_reached();
return no_match_value;
}
static gboolean
match_config_eval(const char *str, const char *tag, guint cur_nm_version)
{
gs_free char *s_ver = NULL;
gs_strfreev char **s_ver_tokens = NULL;
int v_maj = -1, v_min = -1, v_mic = -1;
guint c_maj = -1, c_min = -1, c_mic = -1;
guint n_tokens;
s_ver = g_strdup(str);
g_strstrip(s_ver);
/* Let's be strict with the accepted format here. No funny stuff!! */
if (s_ver[strspn(s_ver, ".0123456789")] != '\0')
return FALSE;
s_ver_tokens = g_strsplit(s_ver, ".", -1);
n_tokens = g_strv_length(s_ver_tokens);
if (n_tokens == 0 || n_tokens > 3)
return FALSE;
v_maj = _nm_utils_ascii_str_to_int64(s_ver_tokens[0], 10, 0, 0xFFFF, -1);
if (v_maj < 0)
return FALSE;
if (n_tokens >= 2) {
v_min = _nm_utils_ascii_str_to_int64(s_ver_tokens[1], 10, 0, 0xFF, -1);
if (v_min < 0)
return FALSE;
}
if (n_tokens >= 3) {
v_mic = _nm_utils_ascii_str_to_int64(s_ver_tokens[2], 10, 0, 0xFF, -1);
if (v_mic < 0)
return FALSE;
}
nm_decode_version(cur_nm_version, &c_maj, &c_min, &c_mic);
#define CHECK_AND_RETURN_FALSE(cur, val, tag, is_last_digit) \
G_STMT_START \
{ \
if (!strcmp(tag, MATCH_TAG_CONFIG_NM_VERSION_MIN)) { \
if (cur < val) \
return FALSE; \
} else if (!strcmp(tag, MATCH_TAG_CONFIG_NM_VERSION_MAX)) { \
if (cur > val) \
return FALSE; \
} else { \
if (cur != val) \
return FALSE; \
} \
if (!(is_last_digit)) { \
if (cur != val) \
return FALSE; \
} \
} \
G_STMT_END
if (v_mic >= 0)
CHECK_AND_RETURN_FALSE(c_mic, v_mic, tag, TRUE);
if (v_min >= 0)
CHECK_AND_RETURN_FALSE(c_min, v_min, tag, v_mic < 0);
CHECK_AND_RETURN_FALSE(c_maj, v_maj, tag, v_min < 0);
return TRUE;
}
NMMatchSpecMatchType
nm_match_spec_config(const GSList *specs, guint cur_nm_version, const char *env)
{
const GSList *iter;
gboolean has_match = FALSE;
gboolean has_match_except = FALSE;
gboolean has_except = FALSE;
gboolean has_not_except = FALSE;
if (!specs)
return NM_MATCH_SPEC_NO_MATCH;
for (iter = specs; iter; iter = g_slist_next(iter)) {
const char *spec_str = iter->data;
gboolean except;
gboolean v_match;
if (!spec_str || !*spec_str)
continue;
spec_str = match_except(spec_str, &except);
if (except)
has_except = TRUE;
else
has_not_except = TRUE;
if ((except && has_match_except) || (!except && has_match)) {
/* evaluating the match does not give new information. Skip it. */
continue;
}
if (_MATCH_CHECK(spec_str, MATCH_TAG_CONFIG_NM_VERSION))
v_match = match_config_eval(spec_str, MATCH_TAG_CONFIG_NM_VERSION, cur_nm_version);
else if (_MATCH_CHECK(spec_str, MATCH_TAG_CONFIG_NM_VERSION_MIN))
v_match = match_config_eval(spec_str, MATCH_TAG_CONFIG_NM_VERSION_MIN, cur_nm_version);
else if (_MATCH_CHECK(spec_str, MATCH_TAG_CONFIG_NM_VERSION_MAX))
v_match = match_config_eval(spec_str, MATCH_TAG_CONFIG_NM_VERSION_MAX, cur_nm_version);
else if (_MATCH_CHECK(spec_str, MATCH_TAG_CONFIG_ENV))
v_match = env && env[0] && !strcmp(spec_str, env);
else
v_match = FALSE;
if (!v_match)
continue;
if (except)
has_match_except = TRUE;
else
has_match = TRUE;
}
return _match_result(has_except, has_not_except, has_match, has_match_except);
}
#undef _MATCH_CHECK
/**
* nm_match_spec_split:
* @value: the string of device specs
*
* Splits the specs from the string and returns them as individual
* entries in a #GSList.
*
* It does not validate any specs, it basically just does a special
* strsplit with ',' or ';' as separators and supporting '\\' as
* escape character.
*
* Leading and trailing spaces of each entry are removed. But the user
* can preserve them by specifying "\\s has 2 leading" or "has 2 trailing \\s".
*
* Specs can have a qualifier like "interface-name:". We still don't strip
* any whitespace after the colon, so "interface-name: X" matches an interface
* named " X".
*
* Returns: (transfer full): the list of device specs.
*/
GSList *
nm_match_spec_split(const char *value)
{
char *string_value, *p, *q0, *q;
GSList *pieces = NULL;
int trailing_ws;
if (!value || !*value)
return NULL;
/* Copied from glibs g_key_file_parse_value_as_string() function
* and adjusted. */
string_value = g_new(char, strlen(value) + 1);
p = (char *) value;
/* skip over leading whitespace */
while (g_ascii_isspace(*p))
p++;
q0 = q = string_value;
trailing_ws = 0;
while (*p) {
if (*p == '\\') {
p++;
switch (*p) {
case 's':
*q = ' ';
break;
case 'n':
*q = '\n';
break;
case 't':
*q = '\t';
break;
case 'r':
*q = '\r';
break;
case '\\':
*q = '\\';
break;
case '\0':
break;
default:
if (NM_IN_SET(*p, ',', ';'))
*q = *p;
else {
*q++ = '\\';
*q = *p;
}
break;
}
if (*p == '\0')
break;
p++;
trailing_ws = 0;
} else {
*q = *p;
if (*p == '\0')
break;
if (g_ascii_isspace(*p)) {
trailing_ws++;
p++;
} else if (NM_IN_SET(*p, ',', ';')) {
if (q0 < q - trailing_ws)
pieces = g_slist_prepend(pieces, g_strndup(q0, (q - q0) - trailing_ws));
q0 = q + 1;
p++;
trailing_ws = 0;
while (g_ascii_isspace(*p))
p++;
} else
p++;
}
q++;
}
*q = '\0';
if (q0 < q - trailing_ws)
pieces = g_slist_prepend(pieces, g_strndup(q0, (q - q0) - trailing_ws));
g_free(string_value);
return g_slist_reverse(pieces);
}
/**
* nm_match_spec_join:
* @specs: the device specs to join
*
* This is based on g_key_file_parse_string_as_value(), analog to
* nm_match_spec_split() which is based on g_key_file_parse_value_as_string().
*
* Returns: (transfer full): a joined list of device specs that can be
* split again with nm_match_spec_split(). Note that
* nm_match_spec_split (nm_match_spec_join (specs)) yields the original
* result (which is not true the other way around because there are multiple
* ways to encode the same joined specs string).
*/
char *
nm_match_spec_join(GSList *specs)
{
const char *p;
GString *str;
str = g_string_new("");
for (; specs; specs = specs->next) {
p = specs->data;
if (!p || !*p)
continue;
if (str->len > 0)
g_string_append_c(str, ',');
/* escape leading whitespace */
switch (*p) {
case ' ':
g_string_append(str, "\\s");
p++;
break;
case '\t':
g_string_append(str, "\\t");
p++;
break;
}
for (; *p; p++) {
switch (*p) {
case '\n':
g_string_append(str, "\\n");
break;
case '\r':
g_string_append(str, "\\r");
break;
case '\\':
g_string_append(str, "\\\\");
break;
case ',':
g_string_append(str, "\\,");
break;
case ';':
g_string_append(str, "\\;");
break;
default:
g_string_append_c(str, *p);
break;
}
}
/* escape trailing whitespaces */
switch (str->str[str->len - 1]) {
case ' ':
g_string_overwrite(str, str->len - 1, "\\s");
break;
case '\t':
g_string_overwrite(str, str->len - 1, "\\t");
break;
}
}
return g_string_free(str, FALSE);
}
static void
_pattern_parse(const char *input,
const char **out_pattern,
gboolean *out_is_inverted,
gboolean *out_is_mandatory)
{
gboolean is_inverted = FALSE;
gboolean is_mandatory = FALSE;
if (input[0] == '&') {
input++;
is_mandatory = TRUE;
if (input[0] == '!') {
input++;
is_inverted = TRUE;
}
goto out;
}
if (input[0] == '|') {
input++;
if (input[0] == '!') {
input++;
is_inverted = TRUE;
}
goto out;
}
if (input[0] == '!') {
input++;
is_inverted = TRUE;
is_mandatory = TRUE;
goto out;
}
out:
if (input[0] == '\\')
input++;
*out_pattern = input;
*out_is_inverted = is_inverted;
*out_is_mandatory = is_mandatory;
}
gboolean
nm_wildcard_match_check(const char *str, const char *const *patterns, guint num_patterns)
{
gboolean has_optional = FALSE;
gboolean has_any_optional = FALSE;
guint i;
for (i = 0; i < num_patterns; i++) {
gboolean is_inverted;
gboolean is_mandatory;
gboolean match;
const char *p;
_pattern_parse(patterns[i], &p, &is_inverted, &is_mandatory);
match = (fnmatch(p, str ?: "", 0) == 0);
if (is_inverted)
match = !match;
if (is_mandatory) {
if (!match)
return FALSE;
} else {
has_any_optional = TRUE;
if (match)
has_optional = TRUE;
}
}
return has_optional || !has_any_optional;
}
/*****************************************************************************/
static gboolean
_kernel_cmdline_match(const char *const *proc_cmdline, const char *pattern)
{
if (proc_cmdline) {
gboolean has_equal = (!!strchr(pattern, '='));
gsize pattern_len = strlen(pattern);
for (; proc_cmdline[0]; proc_cmdline++) {
const char *c = proc_cmdline[0];
if (has_equal) {
/* if pattern contains '=' compare full key=value */
if (nm_streq(c, pattern))
return TRUE;
continue;
}
/* otherwise consider pattern as key only */
if (strncmp(c, pattern, pattern_len) == 0 && NM_IN_SET(c[pattern_len], '\0', '='))
return TRUE;
}
}
return FALSE;
}
gboolean
nm_utils_kernel_cmdline_match_check(const char *const *proc_cmdline,
const char *const *patterns,
guint num_patterns,
GError **error)
{
gboolean has_optional = FALSE;
gboolean has_any_optional = FALSE;
guint i;
for (i = 0; i < num_patterns; i++) {
const char *element = patterns[i];
gboolean is_inverted = FALSE;
gboolean is_mandatory = FALSE;
gboolean match;
const char *p;
_pattern_parse(element, &p, &is_inverted, &is_mandatory);
match = _kernel_cmdline_match(proc_cmdline, p);
if (is_inverted)
match = !match;
if (is_mandatory) {
if (!match) {
nm_utils_error_set(error,
NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
"device does not satisfy match.kernel-command-line property %s",
patterns[i]);
return FALSE;
}
} else {
has_any_optional = TRUE;
if (match)
has_optional = TRUE;
}
}
if (!has_optional && has_any_optional) {
nm_utils_error_set(error,
NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
"device does not satisfy any match.kernel-command-line property");
return FALSE;
}
return TRUE;
}
/*****************************************************************************/
/**
* nm_utils_cmp_connection_by_autoconnect_priority:
* @a:
* @b:
*
* compare connections @a and @b for their autoconnect property
* (with sorting the connection that has autoconnect enabled before
* the other)
* If they both have autoconnect enabled, sort them depending on their
* autoconnect-priority (with the higher priority first).
*
* If their autoconnect/autoconnect-priority is the same, 0 is returned.
* That is, they compare equal.
*
* Returns: -1, 0, or 1
*/
int
nm_utils_cmp_connection_by_autoconnect_priority(NMConnection *a, NMConnection *b)
{
NMSettingConnection *a_s_con;
NMSettingConnection *b_s_con;
int a_ap, b_ap;
gboolean can_autoconnect;
if (a == b)
return 0;
if (!a)
return 1;
if (!b)
return -1;
a_s_con = nm_connection_get_setting_connection(a);
b_s_con = nm_connection_get_setting_connection(b);
if (!a_s_con)
return !b_s_con ? 0 : 1;
if (!b_s_con)
return -1;
can_autoconnect = !!nm_setting_connection_get_autoconnect(a_s_con);
if (can_autoconnect != (!!nm_setting_connection_get_autoconnect(b_s_con)))
return can_autoconnect ? -1 : 1;
if (can_autoconnect) {
a_ap = nm_setting_connection_get_autoconnect_priority(a_s_con);
b_ap = nm_setting_connection_get_autoconnect_priority(b_s_con);
if (a_ap != b_ap)
return (a_ap > b_ap) ? -1 : 1;
}
return 0;
}
/*****************************************************************************/
typedef struct {
const char *name;
NMSetting *setting;
NMSetting *diff_base_setting;
GHashTable *setting_diff;
} LogConnectionSettingData;
typedef struct {
const char *item_name;
NMSettingDiffResult diff_result;
} LogConnectionSettingItem;
static int
_log_connection_sort_hashes_fcn(gconstpointer a, gconstpointer b)
{
const LogConnectionSettingData *v1 = a;
const LogConnectionSettingData *v2 = b;
NMSettingPriority p1, p2;
NMSetting *s1, *s2;
s1 = v1->setting ?: v1->diff_base_setting;
s2 = v2->setting ?: v2->diff_base_setting;
g_assert(s1 && s2);
p1 = _nm_setting_get_setting_priority(s1);
p2 = _nm_setting_get_setting_priority(s2);
if (p1 != p2)
return p1 > p2 ? 1 : -1;
return strcmp(v1->name, v2->name);
}
static GArray *
_log_connection_sort_hashes(NMConnection *connection,
NMConnection *diff_base,
GHashTable *connection_diff)
{
GHashTableIter iter;
GArray *sorted_hashes;
LogConnectionSettingData setting_data;
sorted_hashes = g_array_sized_new(TRUE,
FALSE,
sizeof(LogConnectionSettingData),
g_hash_table_size(connection_diff));
g_hash_table_iter_init(&iter, connection_diff);
while (g_hash_table_iter_next(&iter,
(gpointer) &setting_data.name,
(gpointer) &setting_data.setting_diff)) {
setting_data.setting = nm_connection_get_setting_by_name(connection, setting_data.name);
setting_data.diff_base_setting =
diff_base ? nm_connection_get_setting_by_name(diff_base, setting_data.name) : NULL;
g_assert(setting_data.setting || setting_data.diff_base_setting);
g_array_append_val(sorted_hashes, setting_data);
}
g_array_sort(sorted_hashes, _log_connection_sort_hashes_fcn);
return sorted_hashes;
}
static int
_log_connection_sort_names_fcn(gconstpointer a, gconstpointer b)
{
const LogConnectionSettingItem *v1 = a;
const LogConnectionSettingItem *v2 = b;
/* we want to first show the items, that disappeared, then the one that changed and
* then the ones that were added. */
if ((v1->diff_result & NM_SETTING_DIFF_RESULT_IN_A)
!= (v2->diff_result & NM_SETTING_DIFF_RESULT_IN_A))
return (v1->diff_result & NM_SETTING_DIFF_RESULT_IN_A) ? -1 : 1;
if ((v1->diff_result & NM_SETTING_DIFF_RESULT_IN_B)
!= (v2->diff_result & NM_SETTING_DIFF_RESULT_IN_B))
return (v1->diff_result & NM_SETTING_DIFF_RESULT_IN_B) ? 1 : -1;
return strcmp(v1->item_name, v2->item_name);
}
static char *
_log_connection_get_property(NMSetting *setting, const char *name)
{
GValue val = G_VALUE_INIT;
char *s;
g_return_val_if_fail(setting, NULL);
if (!NM_IS_SETTING_VPN(setting) && nm_setting_get_secret_flags(setting, name, NULL, NULL))
return g_strdup("****");
if (!_nm_setting_get_property(setting, name, &val))
return g_strdup("<unknown>");
if (G_VALUE_HOLDS_STRING(&val)) {
const char *val_s;
val_s = g_value_get_string(&val);
if (!val_s) {
/* for NULL, we want to return the unquoted string "NULL". */
s = g_strdup("NULL");
} else {
char *escaped = g_strescape(val_s, "'");
s = g_strdup_printf("'%s'", escaped);
g_free(escaped);
}
} else if (G_VALUE_HOLDS_VARIANT(&val)) {
s = g_variant_print(g_value_get_variant(&val), FALSE);
} else {
s = g_strdup_value_contents(&val);
if (s == NULL)
s = g_strdup("NULL");
else {
char *escaped = g_strescape(s, "'");
g_free(s);
s = escaped;
}
}
g_value_unset(&val);
return s;
}
static void
_log_connection_sort_names(LogConnectionSettingData *setting_data, GArray *sorted_names)
{
GHashTableIter iter;
LogConnectionSettingItem item;
gpointer p;
g_array_set_size(sorted_names, 0);
g_hash_table_iter_init(&iter, setting_data->setting_diff);
while (g_hash_table_iter_next(&iter, (gpointer) &item.item_name, &p)) {
item.diff_result = GPOINTER_TO_UINT(p);
g_array_append_val(sorted_names, item);
}
g_array_sort(sorted_names, _log_connection_sort_names_fcn);
}
void
nm_utils_log_connection_diff(NMConnection *connection,
NMConnection *diff_base,
guint32 level,
guint64 domain,
const char *name,
const char *prefix,
const char *dbus_path)
{
GHashTable *connection_diff = NULL;
GArray *sorted_hashes;
GArray *sorted_names = NULL;
int i, j;
gboolean connection_diff_are_same;
gboolean print_header = TRUE;
gboolean print_setting_header;
GString *str1;
g_return_if_fail(NM_IS_CONNECTION(connection));
g_return_if_fail(!diff_base || (NM_IS_CONNECTION(diff_base) && diff_base != connection));
/* For VPN setting types, this is broken, because we cannot (generically) print the content of data/secrets. Bummer... */
if (!nm_logging_enabled(level, domain))
return;
if (!prefix)
prefix = "";
if (!name)
name = "";
connection_diff_are_same = nm_connection_diff(
connection,
diff_base,
NM_SETTING_COMPARE_FLAG_EXACT | NM_SETTING_COMPARE_FLAG_DIFF_RESULT_NO_DEFAULT,
&connection_diff);
if (connection_diff_are_same) {
const char *t1, *t2;
t1 = nm_connection_get_connection_type(connection);
if (diff_base) {
t2 = nm_connection_get_connection_type(diff_base);
nm_log(level,
domain,
NULL,
NULL,
"%sconnection '%s' (%p/%s/%s%s%s and %p/%s/%s%s%s): no difference",
prefix,
name,
connection,
G_OBJECT_TYPE_NAME(connection),
NM_PRINT_FMT_QUOTE_STRING(t1),
diff_base,
G_OBJECT_TYPE_NAME(diff_base),
NM_PRINT_FMT_QUOTE_STRING(t2));
} else {
nm_log(level,
domain,
NULL,
NULL,
"%sconnection '%s' (%p/%s/%s%s%s): no properties set",
prefix,
name,
connection,
G_OBJECT_TYPE_NAME(connection),
NM_PRINT_FMT_QUOTE_STRING(t1));
}
g_assert(!connection_diff);
return;
}
/* FIXME: it doesn't nicely show the content of NMSettingVpn, because nm_connection_diff() does not
* expand the hash values. */
sorted_hashes = _log_connection_sort_hashes(connection, diff_base, connection_diff);
if (sorted_hashes->len <= 0)
goto out;
sorted_names = g_array_new(FALSE, FALSE, sizeof(LogConnectionSettingItem));
str1 = g_string_new(NULL);
for (i = 0; i < sorted_hashes->len; i++) {
LogConnectionSettingData *setting_data =
&nm_g_array_index(sorted_hashes, LogConnectionSettingData, i);
_log_connection_sort_names(setting_data, sorted_names);
print_setting_header = TRUE;
for (j = 0; j < sorted_names->len; j++) {
char *str_conn, *str_diff;
LogConnectionSettingItem *item =
&nm_g_array_index(sorted_names, LogConnectionSettingItem, j);
str_conn = (item->diff_result & NM_SETTING_DIFF_RESULT_IN_A)
? _log_connection_get_property(setting_data->setting, item->item_name)
: NULL;
str_diff =
(item->diff_result & NM_SETTING_DIFF_RESULT_IN_B)
? _log_connection_get_property(setting_data->diff_base_setting, item->item_name)
: NULL;
if (print_header) {
GError *err_verify = NULL;
const char *t1, *t2;
t1 = nm_connection_get_connection_type(connection);
if (diff_base) {
t2 = nm_connection_get_connection_type(diff_base);
nm_log(level,
domain,
NULL,
NULL,
"%sconnection '%s' (%p/%s/%s%s%s < %p/%s/%s%s%s)%s%s%s:",
prefix,
name,
connection,
G_OBJECT_TYPE_NAME(connection),
NM_PRINT_FMT_QUOTE_STRING(t1),
diff_base,
G_OBJECT_TYPE_NAME(diff_base),
NM_PRINT_FMT_QUOTE_STRING(t2),
NM_PRINT_FMT_QUOTED(dbus_path, " [", dbus_path, "]", ""));
} else {
nm_log(level,
domain,
NULL,
NULL,
"%sconnection '%s' (%p/%s/%s%s%s):%s%s%s",
prefix,
name,
connection,
G_OBJECT_TYPE_NAME(connection),
NM_PRINT_FMT_QUOTE_STRING(t1),
NM_PRINT_FMT_QUOTED(dbus_path, " [", dbus_path, "]", ""));
}
print_header = FALSE;
if (!nm_connection_verify(connection, &err_verify)) {
nm_log(level,
domain,
NULL,
NULL,
"%sconnection %p does not verify: %s",
prefix,
connection,
err_verify->message);
g_clear_error(&err_verify);
}
}
#define _NM_LOG_ALIGN "-25"
if (print_setting_header) {
if (diff_base) {
if (setting_data->setting && setting_data->diff_base_setting)
g_string_printf(str1,
"%p < %p",
setting_data->setting,
setting_data->diff_base_setting);
else if (setting_data->diff_base_setting)
g_string_printf(str1, "*missing* < %p", setting_data->diff_base_setting);
else
g_string_printf(str1, "%p < *missing*", setting_data->setting);
nm_log(level,
domain,
NULL,
NULL,
"%s%"_NM_LOG_ALIGN
"s [ %s ]",
prefix,
setting_data->name,
str1->str);
} else
nm_log(level,
domain,
NULL,
NULL,
"%s%"_NM_LOG_ALIGN
"s [ %p ]",
prefix,
setting_data->name,
setting_data->setting);
print_setting_header = FALSE;
}
g_string_printf(str1, "%s.%s", setting_data->name, item->item_name);
switch (item->diff_result
& (NM_SETTING_DIFF_RESULT_IN_A | NM_SETTING_DIFF_RESULT_IN_B)) {
case NM_SETTING_DIFF_RESULT_IN_B:
nm_log(level,
domain,
NULL,
NULL,
"%s%"_NM_LOG_ALIGN
"s < %s",
prefix,
str1->str,
str_diff ?: "NULL");
break;
case NM_SETTING_DIFF_RESULT_IN_A:
nm_log(level,
domain,
NULL,
NULL,
"%s%"_NM_LOG_ALIGN
"s = %s",
prefix,
str1->str,
str_conn ?: "NULL");
break;
default:
nm_log(level,
domain,
NULL,
NULL,
"%s%"_NM_LOG_ALIGN
"s = %s < %s",
prefix,
str1->str,
str_conn ?: "NULL",
str_diff ?: "NULL");
break;
#undef _NM_LOG_ALIGN
}
g_free(str_conn);
g_free(str_diff);
}
}
g_array_free(sorted_names, TRUE);
g_string_free(str1, TRUE);
out:
g_hash_table_destroy(connection_diff);
g_array_free(sorted_hashes, TRUE);
}
/*****************************************************************************/
typedef struct {
NMUuid bin;
/* depending on whether the string is packed or not (with/without hyphens),
* it's 32 or 36 characters long (plus the trailing NUL).
*
* The difference is that boot-id is a valid RFC 4211 UUID and represented
* as a 36 ascii string (with hyphens). The machine-id technically is not
* a UUID, but just a 32 byte sequence of hexchars. */
char str[37];
bool is_fake;
} UuidData;
static UuidData *
_uuid_data_init(UuidData *uuid_data, gboolean packed, gboolean is_fake, const NMUuid *uuid)
{
nm_assert(uuid_data);
nm_assert(uuid);
uuid_data->bin = *uuid;
uuid_data->is_fake = is_fake;
if (packed) {
G_STATIC_ASSERT_EXPR(sizeof(uuid_data->str) >= (sizeof(*uuid) * 2 + 1));
nm_utils_bin2hexstr_full(uuid, sizeof(*uuid), '\0', FALSE, uuid_data->str);
} else {
G_STATIC_ASSERT_EXPR(sizeof(uuid_data->str) >= 37);
nm_uuid_unparse(uuid, uuid_data->str);
}
return uuid_data;
}
/*****************************************************************************/
static const UuidData *
_machine_id_get(gboolean allow_fake)
{
static const UuidData *volatile p_uuid_data;
const UuidData *d;
again:
d = g_atomic_pointer_get(&p_uuid_data);
if (G_UNLIKELY(!d)) {
static gsize lock;
static UuidData uuid_data;
gs_free char *content = NULL;
gboolean is_fake = TRUE;
const char *fake_type = NULL;
NMUuid uuid;
/* Get the machine ID from /etc/machine-id; it's always in /etc no matter
* where our configured SYSCONFDIR is. Alternatively, it might be in
* LOCALSTATEDIR /lib/dbus/machine-id.
*/
if (nm_utils_file_get_contents(-1,
"/etc/machine-id",
100 * 1024,
0,
&content,
NULL,
NULL,
NULL)
|| nm_utils_file_get_contents(-1,
LOCALSTATEDIR "/lib/dbus/machine-id",
100 * 1024,
0,
&content,
NULL,
NULL,
NULL)) {
g_strstrip(content);
if (nm_utils_hexstr2bin_full(content,
FALSE,
FALSE,
FALSE,
NULL,
16,
(guint8 *) &uuid,
sizeof(uuid),
NULL)) {
if (!nm_uuid_is_null(&uuid)) {
/* an all-zero machine-id is not valid. */
is_fake = FALSE;
}
}
}
if (is_fake) {
const guint8 *seed_bin;
const NMUuid *hash_seed;
gsize seed_len;
if (!allow_fake) {
/* we don't allow generating (and memorizing) a fake key.
* Signal that no valid machine-id exists. */
return NULL;
}
if (nm_utils_host_id_get(&seed_bin, &seed_len)) {
static const NMUuid u =
NM_UUID_INIT(ab, 08, 5f, 06, b6, 29, 46, d1, a5, 53, 84, ee, ba, 56, 83, b6);
/* We have no valid machine-id but we have a valid secrey_key.
* Generate a fake machine ID by hashing the secret-key. The secret_key
* is commonly persisted, so it should be stable across reboots (despite
* having a broken system without proper machine-id).
*
* Note that we access the host-id here, which is based on secret_key.
* Also not that the secret_key may be generated based on the machine-id,
* so we have to be careful that they don't depend on each other (and
* no infinite recursion happens. This is done correctly, because the secret-key
* will call _machine_id_get(FALSE), so it won't allow accessing a fake
* machine-id, thus avoiding the problem. */
fake_type = "secret-key";
hash_seed = &u;
} else {
static const NMUuid u =
NM_UUID_INIT(7f, f0, c8, f5, 53, 99, 49, 01, ab, 63, 61, bf, 59, 4a, be, 8b);
/* the secret-key is not valid/persistent either. That happens when we fail
* to read/write the secret-key to disk. Fallback to boot-id. The boot-id
* itself may be fake and randomly generated ad-hoc, but that is as best
* as it gets. */
seed_bin = (const guint8 *) nm_utils_boot_id_bin();
seed_len = sizeof(NMUuid);
fake_type = "boot-id";
hash_seed = &u;
}
/* the fake machine-id is based on secret-key/boot-id, but we hash it
* again, so that they are not literally the same. */
nm_uuid_generate_from_string(&uuid,
(const char *) seed_bin,
seed_len,
NM_UUID_TYPE_VERSION5,
hash_seed);
}
if (!g_once_init_enter(&lock))
goto again;
d = _uuid_data_init(&uuid_data, TRUE, is_fake, &uuid);
g_atomic_pointer_set(&p_uuid_data, d);
g_once_init_leave(&lock, 1);
if (is_fake) {
nm_log_err(LOGD_CORE,
"/etc/machine-id: no valid machine-id. Use fake one based on %s: %s",
fake_type,
d->str);
} else
nm_log_dbg(LOGD_CORE, "/etc/machine-id: %s", d->str);
}
return d;
}
const char *
nm_utils_machine_id_str(void)
{
return _machine_id_get(TRUE)->str;
}
const NMUuid *
nm_utils_machine_id_bin(void)
{
return &_machine_id_get(TRUE)->bin;
}
gboolean
nm_utils_machine_id_is_fake(void)
{
return _machine_id_get(TRUE)->is_fake;
}
/*****************************************************************************/
/* prefix for version2 secret key. The secret key is hashed with /etc/machine-id. */
#define SECRET_KEY_V2_PREFIX "nm-v2:"
#define SECRET_KEY_FILE NMSTATEDIR "/secret_key"
static gboolean
_host_id_read_timestamp(gboolean use_secret_key_file,
const guint8 *host_id,
gsize host_id_len,
gint64 *out_timestamp_ns)
{
struct stat st;
gint64 now;
guint64 v;
if (use_secret_key_file && stat(SECRET_KEY_FILE, &st) == 0) {
/* don't check for overflow or timestamps in the future. We get whatever
* (bogus) date is on the file. */
*out_timestamp_ns = nm_utils_timespec_to_nsec(&st.st_mtim);
return TRUE;
}
/* generate a fake timestamp based on the host-id.
*
* This really should never happen under normal circumstances. We already
* are in a code path, where the system has a problem (unable to get good randomness
* and/or can't access the secret_key). In such a scenario, a fake timestamp is the
* least of our problems.
*
* At least, generate something sensible so we don't have to worry about the
* timestamp. It is wrong to worry about using a fake timestamp (which is tied to
* the secret_key) if we are unable to access the secret_key file in the first place.
*
* Pick a timestamp from the past two years, by using a generated timespan
* by hashing the host-id. Yes, this timestamp counts back from @now, and is
* thus not stable across restarts. But apparently neither is the host-id
* nor the secret_key itself. */
#define EPOCH_TWO_YEARS (G_GINT64_CONSTANT(2 * 365 * 24 * 3600) * NM_UTILS_NSEC_PER_SEC)
v = c_siphash_hash(NM_HASH_SEED_16_U64(1156657133u), host_id, host_id_len);
now = time(NULL);
*out_timestamp_ns =
NM_MAX((gint64) 1,
(now * NM_UTILS_NSEC_PER_SEC) - ((gint64) (v % ((guint64) (EPOCH_TWO_YEARS)))));
return FALSE;
}
static const guint8 *
_host_id_hash_v2(const guint8 *seed_arr,
gsize seed_len,
guint8 *out_digest /* 32 bytes (NM_UTILS_CHECKSUM_LENGTH_SHA256) */)
{
nm_auto_free_checksum GChecksum *sum = g_checksum_new(G_CHECKSUM_SHA256);
const UuidData *machine_id_data;
char slen[100];
/* The following snippet generates the same (binary) host-id:
(
stat -c '%s' /var/lib/NetworkManager/secret_key | tr -d '\n';
echo -n ' ';
cat /var/lib/NetworkManager/secret_key;
cat /etc/machine-id | tr -d '\n' | sed -n 's/[a-f0-9-]/\0/pg'
) \
| sha256sum \
| awk '{print $1}' \
| xxd -r -p
*/
nm_sprintf_buf(slen, "%" G_GSIZE_FORMAT " ", seed_len);
g_checksum_update(sum, (const guchar *) slen, strlen(slen));
g_checksum_update(sum, (const guchar *) seed_arr, seed_len);
machine_id_data = _machine_id_get(FALSE);
if (machine_id_data && !machine_id_data->is_fake)
g_checksum_update(sum, (const guchar *) machine_id_data->str, strlen(machine_id_data->str));
nm_utils_checksum_get_digest_len(sum, out_digest, NM_UTILS_CHECKSUM_LENGTH_SHA256);
return out_digest;
}
static gboolean
_host_id_read(guint8 **out_host_id, gsize *out_host_id_len)
{
#define SECRET_KEY_LEN 32u
guint8 sha256_digest[NM_UTILS_CHECKSUM_LENGTH_SHA256];
nm_auto_clear_secret_ptr NMSecretPtr file_content = {0};
const guint8 *secret_arr;
gsize secret_len;
GError *error = NULL;
gboolean success;
if (!nm_utils_file_get_contents(-1,
SECRET_KEY_FILE,
10 * 1024,
NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET,
&file_content.str,
&file_content.len,
NULL,
&error)) {
if (!nm_utils_error_is_notfound(error)) {
nm_log_warn(LOGD_CORE,
"secret-key: failure reading secret key in \"%s\": %s (generate new key)",
SECRET_KEY_FILE,
error->message);
}
g_clear_error(&error);
} else if (file_content.len >= NM_STRLEN(SECRET_KEY_V2_PREFIX) + SECRET_KEY_LEN
&& memcmp(file_content.bin, SECRET_KEY_V2_PREFIX, NM_STRLEN(SECRET_KEY_V2_PREFIX))
== 0) {
/* for this type of secret key, we require a prefix followed at least SECRET_KEY_LEN (32) bytes. We
* (also) do that, because older versions of NetworkManager wrote exactly 32 bytes without
* prefix, so we won't wrongly interpret such legacy keys as v2 (if they accidentally have
* a SECRET_KEY_V2_PREFIX prefix, they'll still have the wrong size).
*
* Note that below we generate the random seed in base64 encoding. But that is only done
* to write an ASCII file. There is no base64 decoding and the ASCII is hashed as-is.
* We would accept any binary data just as well (provided a suitable prefix and at least
* 32 bytes).
*
* Note that when hashing the v2 content, we also hash the prefix. There is no strong reason,
* except that it seems simpler not to distinguish between the v2 prefix and the content.
* It's all just part of the seed. */
secret_arr = _host_id_hash_v2(file_content.bin, file_content.len, sha256_digest);
secret_len = NM_UTILS_CHECKSUM_LENGTH_SHA256;
success = TRUE;
nm_log_dbg(LOGD_CORE,
"secret-key: v2 secret key loaded from \"%s\" (%zu bytes)",
SECRET_KEY_FILE,
file_content.len);
goto out;
} else if (file_content.len >= 16) {
secret_arr = file_content.bin;
secret_len = file_content.len;
success = TRUE;
nm_log_dbg(LOGD_CORE,
"secret-key: v1 secret key loaded from \"%s\" (%zu bytes)",
SECRET_KEY_FILE,
file_content.len);
goto out;
} else {
/* the secret key is borked. Log a warning, but proceed below to generate
* a new one. */
nm_log_warn(LOGD_CORE,
"secret-key: too short secret key in \"%s\" (generate new key)",
SECRET_KEY_FILE);
}
/* generate and persist new key */
{
#define SECRET_KEY_LEN_BASE64 ((((SECRET_KEY_LEN / 3) + 1) * 4) + 4)
guint8 rnd_buf[SECRET_KEY_LEN];
guint8 new_content[NM_STRLEN(SECRET_KEY_V2_PREFIX) + SECRET_KEY_LEN_BASE64];
int base64_state = 0;
int base64_save = 0;
gsize len;
nm_random_get_bytes(rnd_buf, sizeof(rnd_buf));
/* Our key is really binary data. But since we anyway generate a random seed
* (with 32 random bytes), don't write it in binary, but instead create
* an pure ASCII (base64) representation. Note that the ASCII will still be taken
* as-is (no base64 decoding is done). The sole purpose is to write a ASCII file
* instead of a binary. The content is gibberish either way. */
memcpy(new_content, SECRET_KEY_V2_PREFIX, NM_STRLEN(SECRET_KEY_V2_PREFIX));
len = NM_STRLEN(SECRET_KEY_V2_PREFIX);
len += g_base64_encode_step(rnd_buf,
sizeof(rnd_buf),
FALSE,
(char *) &new_content[len],
&base64_state,
&base64_save);
len +=
g_base64_encode_close(FALSE, (char *) &new_content[len], &base64_state, &base64_save);
nm_assert(len <= sizeof(new_content));
secret_arr = _host_id_hash_v2(new_content, len, sha256_digest);
secret_len = NM_UTILS_CHECKSUM_LENGTH_SHA256;
success = TRUE;
if (nm_utils_get_testing()) {
/* for test code, we don't write the generated secret-key to disk. */
} else if (!nm_utils_file_set_contents(SECRET_KEY_FILE,
(const char *) new_content,
len,
0600,
NULL,
NULL,
NULL,
&error)) {
nm_log_warn(
LOGD_CORE,
"secret-key: failure to persist secret key in \"%s\" (%s) (use non-persistent key)",
SECRET_KEY_FILE,
error->message);
g_clear_error(&error);
success = FALSE;
} else
nm_log_dbg(LOGD_CORE,
"secret-key: persist new v2 secret key to \"%s\" (%zu bytes)",
SECRET_KEY_FILE,
len);
nm_explicit_bzero(rnd_buf, sizeof(rnd_buf));
nm_explicit_bzero(new_content, sizeof(new_content));
}
out:
*out_host_id_len = secret_len;
*out_host_id = nm_memdup(secret_arr, secret_len);
return success;
}
typedef struct {
guint8 *host_id;
gsize host_id_len;
/* The timestamp (in nsec since the Epoch) returned by nm_utils_host_id_get_timestamp_nsec().
* It is associated with the host (and the host-id). We currently use this for the LLT DUID
* generation for IPv6. Instead of persisting the timestamp separately to disk, we re-use the
* file timestamp of the secret_key file. */
gint64 timestamp_nsec;
bool is_good : 1;
bool timestamp_is_good : 1;
} HostIdData;
static const HostIdData *volatile host_id_static;
static const HostIdData *
_host_id_get(void)
{
const HostIdData *host_id;
again:
host_id = g_atomic_pointer_get(&host_id_static);
if (G_UNLIKELY(!host_id)) {
static HostIdData host_id_data;
static gsize init_value = 0;
if (!g_once_init_enter(&init_value))
goto again;
host_id_data.is_good = _host_id_read(&host_id_data.host_id, &host_id_data.host_id_len);
host_id_data.timestamp_is_good = _host_id_read_timestamp(host_id_data.is_good,
host_id_data.host_id,
host_id_data.host_id_len,
&host_id_data.timestamp_nsec);
if (!host_id_data.timestamp_is_good && host_id_data.is_good)
nm_log_warn(LOGD_CORE, "secret-key: failure reading host timestamp (use fake one)");
host_id = &host_id_data;
g_atomic_pointer_set(&host_id_static, host_id);
g_once_init_leave(&init_value, 1);
}
return host_id;
}
/**
* nm_utils_host_id_get:
* @out_host_id: (out) (transfer none): the binary host key
* @out_host_id_len: the length of the host key.
*
* This returns a per-host key that depends on /var/lib/NetworkManage/secret_key
* and (depending on the version) on /etc/machine-id. If /var/lib/NetworkManage/secret_key
* does not exist, it will be generated and persisted for next boot.
*
* Returns: %TRUE, if the host key is "good". Note that this function
* will always succeed to return a host-key, and that this key
* won't change during the run of the program (no matter what).
* A %FALSE return possibly means, that the secret_key is not persisted
* to disk, and/or that it was generated with bad randomness.
*/
gboolean
nm_utils_host_id_get(const guint8 **out_host_id, gsize *out_host_id_len)
{
const HostIdData *host_id;
host_id = _host_id_get();
*out_host_id = host_id->host_id;
*out_host_id_len = host_id->host_id_len;
return host_id->is_good;
}
gint64
nm_utils_host_id_get_timestamp_nsec(void)
{
return _host_id_get()->timestamp_nsec;
}
static GArray *nmtst_host_id_stack = NULL;
static GMutex nmtst_host_id_lock;
const HostIdData *nmtst_host_id_static_0 = NULL;
void
nmtst_utils_host_id_push(const guint8 *host_id,
gssize host_id_len,
gboolean is_good,
const gint64 *p_timestamp_nsec)
{
NM_G_MUTEX_LOCKED(&nmtst_host_id_lock);
gs_free char *str1_to_free = NULL;
HostIdData *h;
g_assert(host_id_len >= -1);
if (host_id_len < 0)
host_id_len = host_id ? strlen((const char *) host_id) : 0;
nm_log_dbg(LOGD_CORE,
"nmtst: host-id push: \"%s\" (%zu), is-good=%d, timestamp=%" G_GINT64_FORMAT "%s",
nm_utils_buf_utf8safe_escape(host_id,
host_id_len,
NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
&str1_to_free),
(gsize) host_id_len,
!!is_good,
p_timestamp_nsec ? *p_timestamp_nsec : 0,
p_timestamp_nsec ? "" : " (not-good)");
if (!nmtst_host_id_stack) {
nmtst_host_id_stack = g_array_new(FALSE, FALSE, sizeof(HostIdData));
nmtst_host_id_static_0 = g_atomic_pointer_get(&host_id_static);
}
h = nm_g_array_append_new(nmtst_host_id_stack, HostIdData);
*h = (HostIdData) {
.host_id = nm_memdup(host_id, host_id_len),
.host_id_len = host_id_len,
.timestamp_nsec = p_timestamp_nsec ? *p_timestamp_nsec : 0,
.is_good = is_good,
.timestamp_is_good = !!p_timestamp_nsec,
};
g_atomic_pointer_set(&host_id_static, h);
}
void
nmtst_utils_host_id_pop(void)
{
NM_G_MUTEX_LOCKED(&nmtst_host_id_lock);
HostIdData *h;
g_assert(nmtst_host_id_stack);
g_assert(nmtst_host_id_stack->len > 0);
nm_log_dbg(LOGD_CORE, "nmtst: host-id pop");
h = &nm_g_array_index(nmtst_host_id_stack, HostIdData, nmtst_host_id_stack->len - 1);
g_free((char *) h->host_id);
g_array_set_size(nmtst_host_id_stack, nmtst_host_id_stack->len - 1u);
if (!g_atomic_pointer_compare_and_exchange(
&host_id_static,
h,
nmtst_host_id_stack->len == 0u ? nmtst_host_id_static_0
: &nm_g_array_last(nmtst_host_id_stack, HostIdData)))
g_assert_not_reached();
}
/*****************************************************************************/
static const UuidData *
_boot_id_get(void)
{
static const UuidData *volatile p_boot_id;
const UuidData *d;
again:
d = g_atomic_pointer_get(&p_boot_id);
if (G_UNLIKELY(!d)) {
static gsize lock;
static UuidData boot_id;
gs_free char *contents = NULL;
NMUuid uuid;
gboolean is_fake = FALSE;
nm_utils_file_get_contents(-1,
"/proc/sys/kernel/random/boot_id",
0,
NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE,
&contents,
NULL,
NULL,
NULL);
if (!contents || !nm_uuid_parse(nm_strstrip(contents), &uuid)) {
/* generate a random UUID instead. */
is_fake = TRUE;
nm_uuid_generate_random(&uuid);
}
if (!g_once_init_enter(&lock))
goto again;
d = _uuid_data_init(&boot_id, FALSE, is_fake, &uuid);
g_atomic_pointer_set(&p_boot_id, d);
g_once_init_leave(&lock, 1);
}
return d;
}
const char *
nm_utils_boot_id_str(void)
{
return _boot_id_get()->str;
}
const NMUuid *
nm_utils_boot_id_bin(void)
{
return &_boot_id_get()->bin;
}
/*****************************************************************************/
const char *
nm_utils_proc_cmdline(void)
{
static const char *volatile proc_cmdline_cached = NULL;
const char *proc_cmdline;
again:
proc_cmdline = g_atomic_pointer_get(&proc_cmdline_cached);
if (G_UNLIKELY(!proc_cmdline)) {
gs_free char *str = NULL;
/* /run/NetworkManager/proc-cmdline can be used to overrule /proc/cmdline. */
if (!g_file_get_contents(NMRUNDIR "/proc-cmdline", &str, NULL, NULL))
g_file_get_contents("/proc/cmdline", &str, NULL, NULL);
str = nm_str_realloc(str);
proc_cmdline = str ?: "";
if (!g_atomic_pointer_compare_and_exchange(&proc_cmdline_cached, NULL, proc_cmdline))
goto again;
g_steal_pointer(&str);
}
return proc_cmdline;
}
const char *const *
nm_utils_proc_cmdline_split(void)
{
static const char *const *volatile proc_cmdline_cached = NULL;
const char *const *proc_cmdline;
again:
proc_cmdline = g_atomic_pointer_get(&proc_cmdline_cached);
if (G_UNLIKELY(!proc_cmdline)) {
gs_strfreev char **split = NULL;
split = nm_utils_strsplit_quoted(nm_utils_proc_cmdline());
if (!g_atomic_pointer_compare_and_exchange(&proc_cmdline_cached, NULL, (gpointer) split))
goto again;
proc_cmdline = (const char *const *) g_steal_pointer(&split);
}
return proc_cmdline;
}
/*****************************************************************************/
/**
* nm_utils_arp_type_detect_from_hwaddrlen:
* @hwaddr_len: the length of the hardware address in bytes.
*
* Detects the arp-type based on the length of the MAC address.
* On success, this returns a (positive) value in uint16_t range,
* like ARPHRD_ETHER or ARPHRD_INFINIBAND.
*
* On failure, returns a negative error code.
*
* Returns: the arp-type or negative value on error. */
int
nm_utils_arp_type_detect_from_hwaddrlen(gsize hwaddr_len)
{
switch (hwaddr_len) {
case ETH_ALEN:
return ARPHRD_ETHER;
case INFINIBAND_ALEN:
return ARPHRD_INFINIBAND;
default:
/* Note: if you ever support anything but ethernet and infiniband,
* make sure to look at all callers. They assert that it's one of
* these two. */
return -EINVAL;
}
}
gboolean
nm_utils_arp_type_validate_hwaddr(int arp_type, const guint8 *hwaddr, gsize hwaddr_len)
{
if (!hwaddr)
return FALSE;
if (arp_type == ARPHRD_ETHER) {
G_STATIC_ASSERT(ARPHRD_ETHER >= 0 && ARPHRD_ETHER <= 0xFF);
if (hwaddr_len != ETH_ALEN)
return FALSE;
} else if (arp_type == ARPHRD_INFINIBAND) {
G_STATIC_ASSERT(ARPHRD_INFINIBAND >= 0 && ARPHRD_INFINIBAND <= 0xFF);
if (hwaddr_len != INFINIBAND_ALEN)
return FALSE;
} else
return FALSE;
nm_assert(arp_type == nm_utils_arp_type_detect_from_hwaddrlen(hwaddr_len));
return TRUE;
}
gboolean
nm_utils_arp_type_get_hwaddr_relevant_part(int arp_type, const guint8 **hwaddr, gsize *hwaddr_len)
{
g_return_val_if_fail(hwaddr && hwaddr_len
&& nm_utils_arp_type_validate_hwaddr(arp_type, *hwaddr, *hwaddr_len),
FALSE);
/* for infiniband, we only consider the last 8 bytes. */
if (arp_type == ARPHRD_INFINIBAND) {
*hwaddr += (INFINIBAND_ALEN - 8);
*hwaddr_len = 8;
}
return TRUE;
}
/*****************************************************************************/
/* Returns the "u" (universal/local) bit value for a Modified EUI-64 */
static gboolean
get_gre_eui64_u_bit(guint32 addr)
{
static const struct {
guint32 mask;
guint32 result;
} items[] = {
{0xff000000}, {0x7f000000}, /* IPv4 loopback */
{0xf0000000}, {0xe0000000}, /* IPv4 multicast */
{0xffffff00}, {0xe0000000}, /* IPv4 local multicast */
{0xffffffff}, {INADDR_BROADCAST}, /* limited broadcast */
{0xff000000}, {0x00000000}, /* zero net */
{0xff000000}, {0x0a000000}, /* private 10 (RFC3330) */
{0xfff00000}, {0xac100000}, /* private 172 */
{0xffff0000}, {0xc0a80000}, /* private 192 */
{0xffff0000}, {0xa9fe0000}, /* IPv4 link-local */
{0xffffff00}, {0xc0586300}, /* anycast 6-to-4 */
{0xffffff00}, {0xc0000200}, /* test 192 */
{0xfffe0000}, {0xc6120000}, /* test 198 */
};
guint i;
for (i = 0; i < G_N_ELEMENTS(items); i++) {
if ((addr & htonl(items[i].mask)) == htonl(items[i].result))
return 0x00; /* "local" scope */
}
return 0x02; /* "universal" scope */
}
/**
* nm_utils_get_ipv6_interface_identifier:
* @link_type: the hardware link type
* @hwaddr: the hardware address of the interface
* @hwaddr_len: the length (in bytes) of @hwaddr
* @dev_id: the device identifier, if any
* @out_iid: on success, filled with the interface identifier; on failure
* zeroed out
*
* Constructs an interface identifier in "Modified EUI-64" format which is
* suitable for constructing IPv6 addresses. Note that the identifier is
* not obscured in any way (eg, RFC3041).
*
* Returns: %TRUE if the interface identifier could be constructed, %FALSE if
* if could not be constructed.
*/
gboolean
nm_utils_get_ipv6_interface_identifier(NMLinkType link_type,
const guint8 *hwaddr,
guint hwaddr_len,
guint dev_id,
NMUtilsIPv6IfaceId *out_iid)
{
guint32 addr;
g_return_val_if_fail(hwaddr != NULL, FALSE);
g_return_val_if_fail(hwaddr_len > 0, FALSE);
g_return_val_if_fail(out_iid != NULL, FALSE);
out_iid->id = 0;
switch (link_type) {
case NM_LINK_TYPE_INFINIBAND:
/* Use the port GUID per http://tools.ietf.org/html/rfc4391#section-8,
* making sure to set the 'u' bit to 1. The GUID is the lower 64 bits
* of the IPoIB interface's hardware address.
*/
if (hwaddr_len == INFINIBAND_ALEN) {
memcpy(out_iid->id_u8, hwaddr + INFINIBAND_ALEN - 8, 8);
out_iid->id_u8[0] |= 0x02;
return TRUE;
}
break;
case NM_LINK_TYPE_GRE:
/* Hardware address is the network-endian IPv4 address */
if (hwaddr_len == 4) {
addr = unaligned_read_ne32(hwaddr);
out_iid->id_u8[0] = get_gre_eui64_u_bit(addr);
out_iid->id_u8[1] = 0x00;
out_iid->id_u8[2] = 0x5E;
out_iid->id_u8[3] = 0xFE;
memcpy(out_iid->id_u8 + 4, &addr, 4);
return TRUE;
}
break;
case NM_LINK_TYPE_6LOWPAN:
/* The hardware address is already 64-bit. This is the case for
* IEEE 802.15.4 networks. */
if (hwaddr_len == sizeof(out_iid->id_u8)) {
memcpy(out_iid->id_u8, hwaddr, sizeof(out_iid->id_u8));
return TRUE;
}
break;
default:
if (hwaddr_len == ETH_ALEN) {
/* Translate 48-bit MAC address to a 64-bit Modified EUI-64. See
* http://tools.ietf.org/html/rfc4291#appendix-A and the Linux
* kernel's net/ipv6/addrconf.c::ipv6_generate_eui64() function.
*/
out_iid->id_u8[0] = hwaddr[0];
out_iid->id_u8[1] = hwaddr[1];
out_iid->id_u8[2] = hwaddr[2];
if (dev_id) {
out_iid->id_u8[3] = (dev_id >> 8) & 0xff;
out_iid->id_u8[4] = dev_id & 0xff;
} else {
out_iid->id_u8[0] ^= 0x02;
out_iid->id_u8[3] = 0xff;
out_iid->id_u8[4] = 0xfe;
}
out_iid->id_u8[5] = hwaddr[3];
out_iid->id_u8[6] = hwaddr[4];
out_iid->id_u8[7] = hwaddr[5];
return TRUE;
}
break;
}
return FALSE;
}
/*****************************************************************************/
char *
nm_utils_stable_id_random(void)
{
char buf[15];
nm_random_get_bytes(buf, sizeof(buf));
return g_base64_encode((guchar *) buf, sizeof(buf));
}
char *
nm_utils_stable_id_generated_complete(const char *stable_id_generated)
{
nm_auto_free_checksum GChecksum *sum = NULL;
guint8 buf[NM_UTILS_CHECKSUM_LENGTH_SHA1];
char *base64;
/* for NM_UTILS_STABLE_TYPE_GENERATED we generate a possibly long string
* by doing text-substitutions in nm_utils_stable_id_parse().
*
* Let's shorten the (possibly) long stable_id to something more compact. */
g_return_val_if_fail(stable_id_generated, NULL);
sum = g_checksum_new(G_CHECKSUM_SHA1);
g_checksum_update(sum, (guchar *) stable_id_generated, strlen(stable_id_generated));
nm_utils_checksum_get_digest(sum, buf);
/* we don't care to use the sha1 sum in common hex representation.
* Use instead base64, it's 27 chars (stripping the padding) vs.
* 40. */
base64 = g_base64_encode((guchar *) buf, sizeof(buf));
nm_assert(strlen(base64) == 28);
nm_assert(base64[27] == '=');
base64[27] = '\0';
return base64;
}
static void
_stable_id_append(NMStrBuf *str, const char *substitution)
{
if (!substitution) {
/* Would have been nicer to append "=NIL;" to differentiate between
* empty and NULL.
*
* Can't do that now, as it would change behavior. */
substitution = "";
}
nm_str_buf_append_printf(str, "=%zu{%s}", strlen(substitution), substitution);
}
NMUtilsStableType
nm_utils_stable_id_parse(const char *stable_id,
const char *deviceid,
const char *hwaddr,
const char *bootid,
const char *uuid,
GBytes *ssid,
char **out_generated)
{
nm_auto_str_buf NMStrBuf str = NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_232, FALSE);
gsize i;
gsize idx_start;
g_return_val_if_fail(out_generated, NM_UTILS_STABLE_TYPE_RANDOM);
if (!stable_id) {
*out_generated = NULL;
return NM_UTILS_STABLE_TYPE_UUID;
}
if (nm_streq(stable_id, "default${CONNECTION}")) {
/* This changed behavior in 1.44. Explicitly setting "default${CONNECTION}"
* the same as the built-in default that we get by not configuring
* the property. */
*out_generated = NULL;
return NM_UTILS_STABLE_TYPE_UUID;
}
/* the stable-id allows for some dynamic by performing text-substitutions
* of ${...} patterns.
*
* At first, it looks a bit like bash parameter substitution.
* In contrast however, the process is unambiguous so that the resulting
* effective id differs if:
* - the original, untranslated stable-id differs
* - or any of the substitution differs.
*
* The reason for that is, for example if you specify "${CONNECTION}" in the
* stable-id, then the resulting ID should be always(!) unique for this connection.
* There should be no way another connection could specify any stable-id that results
* in the same addresses to be generated (aside hash collisions).
*
*
* For example: say you have a connection with UUID
* "123e4567-e89b-12d3-a456-426655440000" which happens also to be
* the current boot-id.
* Then:
* (1) connection.stable-id = <NULL>
* (2) connection.stable-id = "123e4567-e89b-12d3-a456-426655440000"
* (3) connection.stable-id = "${CONNECTION}"
* (3) connection.stable-id = "${BOOT}"
* will all generate different addresses, although in one way or the
* other, they all mangle the uuid "123e4567-e89b-12d3-a456-426655440000".
*
* For example, with stable-id="${FOO}${BAR}" the substitutions
* - FOO="ab", BAR="c"
* - FOO="a", BAR="bc"
* should give a different effective id.
*
* For example, with FOO="x" and BAR="x", the stable-ids
* - "${FOO}${BAR}"
* - "${BAR}${FOO}"
* should give a different effective id.
*/
idx_start = 0;
for (i = 0; stable_id[i];) {
if (stable_id[i] != '$') {
i++;
continue;
}
#define CHECK_PREFIX(prefix) \
({ \
gboolean _match = FALSE; \
\
if (NM_STR_HAS_PREFIX(&stable_id[i], "" prefix "")) { \
_match = TRUE; \
i += NM_STRLEN(prefix); \
nm_str_buf_append_len(&str, &(stable_id)[idx_start], i - idx_start); \
idx_start = i; \
} \
_match; \
})
if (CHECK_PREFIX("${CONNECTION}"))
_stable_id_append(&str, uuid);
else if (CHECK_PREFIX("${BOOT}"))
_stable_id_append(&str, bootid);
else if (CHECK_PREFIX("${DEVICE}"))
_stable_id_append(&str, deviceid);
else if (CHECK_PREFIX("${MAC}"))
_stable_id_append(&str, hwaddr);
else if (CHECK_PREFIX("${NETWORK_SSID}")) {
gs_free char *value_free = NULL;
gs_free char *s = NULL;
const char *value;
const char *type_id;
if (ssid) {
type_id = "s:";
value = nm_utils_buf_utf8safe_escape_bytes(ssid,
NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
&value_free);
} else {
/* If we have no SSID, we fallback to the connection's UUID.
*
* Give a separate prefix (@type_id), so that an SSID and a UUID
* fallback never result in the same output. */
type_id = "c:";
value = uuid ?: "";
}
s = g_strjoin("", type_id, value, NULL);
_stable_id_append(&str, s);
} else if (g_str_has_prefix(&stable_id[i], "${RANDOM}")) {
/* RANDOM makes not so much sense for cloned-mac-address
* as the result is similar to specifying "cloned-mac-address=random".
* It makes however sense for RFC 7217 Stable Privacy IPv6 addresses
* where this is effectively the only way to generate a different
* (random) host identifier for each connect.
*
* With RANDOM, the user can switch the lifetime of the
* generated cloned-mac-address and IPv6 host identifier
* by toggling only the stable-id property of the connection.
* With RANDOM being the most short-lived, ~non-stable~ variant.
*/
*out_generated = NULL;
return NM_UTILS_STABLE_TYPE_RANDOM;
} else {
/* The text following the '$' is not recognized as valid
* substitution pattern. Treat it verbatim. */
i++;
/* Note that using unrecognized substitution patterns might
* yield different results with future versions. Avoid that,
* by not using '$' (except for actual substitutions) or escape
* it as "$$" (which is guaranteed to be treated verbatim
* in future). */
if (stable_id[i] == '$')
i++;
}
}
#undef CHECK_PREFIX
if (str.len == 0) {
*out_generated = NULL;
return NM_UTILS_STABLE_TYPE_STABLE_ID;
}
if (idx_start < i)
nm_str_buf_append_len(&str, &stable_id[idx_start], i - idx_start);
*out_generated = nm_str_buf_finalize(&str, NULL);
return NM_UTILS_STABLE_TYPE_GENERATED;
}
NMUtilsStableType
nm_utils_stable_id_parse_network_ssid(GBytes *ssid,
const char *uuid,
gboolean complete,
char **out_stable_id)
{
NMUtilsStableType stable_type;
stable_type =
nm_utils_stable_id_parse("${NETWORK_SSID}", NULL, NULL, NULL, uuid, ssid, out_stable_id);
nm_assert(stable_type == NM_UTILS_STABLE_TYPE_GENERATED);
nm_assert(!out_stable_id || nm_str_not_empty(*out_stable_id));
if (complete && out_stable_id) {
gs_free char *ss = g_steal_pointer(out_stable_id);
*out_stable_id = nm_utils_stable_id_generated_complete(ss);
}
return stable_type;
}
/*****************************************************************************/
static gboolean
_is_reserved_ipv6_iid(const guint8 *iid)
{
/* https://tools.ietf.org/html/rfc5453 */
/* https://www.iana.org/assignments/ipv6-interface-ids/ipv6-interface-ids.xml */
/* 0000:0000:0000:0000 (Subnet-Router Anycast [RFC4291]) */
if (memcmp(iid, &nm_ip_addr_zero.addr6.s6_addr[8], 8) == 0)
return TRUE;
/* 0200:5EFF:FE00:0000 - 0200:5EFF:FE00:5212 (Reserved IPv6 Interface Identifiers corresponding to the IANA Ethernet Block [RFC4291])
* 0200:5EFF:FE00:5213 (Proxy Mobile IPv6 [RFC6543])
* 0200:5EFF:FE00:5214 - 0200:5EFF:FEFF:FFFF (Reserved IPv6 Interface Identifiers corresponding to the IANA Ethernet Block [RFC4291]) */
if (memcmp(iid, (const guint8[]) {0x02, 0x00, 0x5E, 0xFF, 0xFE}, 5) == 0)
return TRUE;
/* FDFF:FFFF:FFFF:FF80 - FDFF:FFFF:FFFF:FFFF (Reserved Subnet Anycast Addresses [RFC2526]) */
if (memcmp(iid, (const guint8[]) {0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, 7) == 0) {
if (iid[7] & 0x80)
return TRUE;
}
return FALSE;
}
void
nm_utils_ipv6_addr_set_stable_privacy_with_host_id(NMUtilsStableType stable_type,
struct in6_addr *addr,
const char *ifname,
const char *network_id,
guint32 dad_counter,
const guint8 *host_id,
gsize host_id_len)
{
nm_auto_free_checksum GChecksum *sum = NULL;
guint8 digest[NM_UTILS_CHECKSUM_LENGTH_SHA256];
guint32 tmp[2];
nm_assert(host_id_len);
nm_assert(network_id);
sum = g_checksum_new(G_CHECKSUM_SHA256);
host_id_len = NM_MIN(host_id_len, G_MAXUINT32);
if (stable_type != NM_UTILS_STABLE_TYPE_UUID) {
guint8 stable_type_uint8;
nm_assert(stable_type < (NMUtilsStableType) 255);
stable_type_uint8 = (guint8) stable_type;
/* Preferably, we would always like to include the stable-type,
* but for backward compatibility reasons, we cannot for UUID.
*
* That is no real problem and it is still impossible to
* force a collision here, because of how the remaining
* fields are hashed. That is, as we also hash @host_id_len
* and the terminating '\0' of @network_id, it is unambiguously
* possible to revert the process and deduce the @stable_type.
*/
g_checksum_update(sum, &stable_type_uint8, sizeof(stable_type_uint8));
}
g_checksum_update(sum, addr->s6_addr, 8);
g_checksum_update(sum, (const guchar *) ifname, strlen(ifname) + 1);
g_checksum_update(sum, (const guchar *) network_id, strlen(network_id) + 1);
tmp[0] = htonl(dad_counter);
tmp[1] = htonl(host_id_len);
g_checksum_update(sum, (const guchar *) tmp, sizeof(tmp));
g_checksum_update(sum, (const guchar *) host_id, host_id_len);
nm_utils_checksum_get_digest(sum, digest);
while (_is_reserved_ipv6_iid(digest)) {
g_checksum_reset(sum);
tmp[0] = htonl(++dad_counter);
g_checksum_update(sum, digest, sizeof(digest));
g_checksum_update(sum, (const guchar *) &tmp[0], sizeof(tmp[0]));
nm_utils_checksum_get_digest(sum, digest);
}
memcpy(addr->s6_addr + 8, &digest[0], 8);
}
void
nm_utils_ipv6_addr_set_stable_privacy(NMUtilsStableType stable_type,
struct in6_addr *addr,
const char *ifname,
const char *network_id,
guint32 dad_counter)
{
const guint8 *host_id;
gsize host_id_len;
nm_utils_host_id_get(&host_id, &host_id_len);
nm_utils_ipv6_addr_set_stable_privacy_with_host_id(stable_type,
addr,
ifname,
network_id,
dad_counter,
host_id,
host_id_len);
}
/**
* nm_utils_ipv6_addr_set_stable_privacy:
*
* Extend the address prefix with an interface identifier using the
* RFC 7217 Stable Privacy mechanism.
*
* Returns: %TRUE on success, %FALSE if the address could not be generated.
*/
gboolean
nm_utils_ipv6_addr_set_stable_privacy_may_fail(NMUtilsStableType stable_type,
struct in6_addr *addr,
const char *ifname,
const char *network_id,
guint32 dad_counter,
GError **error)
{
g_return_val_if_fail(network_id, FALSE);
if (dad_counter >= NM_STABLE_PRIVACY_RFC7217_IDGEN_RETRIES) {
g_set_error_literal(error,
NM_UTILS_ERROR,
NM_UTILS_ERROR_UNKNOWN,
"Too many DAD collisions");
return FALSE;
}
nm_utils_ipv6_addr_set_stable_privacy(stable_type, addr, ifname, network_id, dad_counter);
return TRUE;
}
/*****************************************************************************/
static void
_hw_addr_eth_complete(struct ether_addr *addr,
const char *current_mac_address,
const char *generate_mac_address_mask)
{
struct ether_addr mask;
struct ether_addr oui;
struct ether_addr *ouis;
gsize ouis_len;
guint i;
/* the second LSB of the first octet means
* "globally unique, OUI enforced, BIA (burned-in-address)"
* vs. "locally-administered". By default, set it to
* generate locally-administered addresses.
*
* Maybe be overwritten by a mask below. */
addr->ether_addr_octet[0] |= 2;
if (!generate_mac_address_mask || !*generate_mac_address_mask)
goto out;
if (!_nm_utils_generate_mac_address_mask_parse(generate_mac_address_mask,
&mask,
&ouis,
&ouis_len,
NULL))
goto out;
nm_assert((ouis == NULL) ^ (ouis_len != 0));
if (ouis) {
oui = ouis[nm_random_u64_range(0, ouis_len)];
g_free(ouis);
} else {
if (!nm_utils_hwaddr_aton(current_mac_address, &oui, ETH_ALEN))
goto out;
}
for (i = 0; i < ETH_ALEN; i++) {
const guint8 a = addr->ether_addr_octet[i];
const guint8 o = oui.ether_addr_octet[i];
const guint8 m = mask.ether_addr_octet[i];
addr->ether_addr_octet[i] = (a & ~m) | (o & m);
}
out:
/* The LSB of the first octet must always be cleared,
* it means Unicast vs. Multicast */
addr->ether_addr_octet[0] &= ~1;
}
char *
nm_utils_hw_addr_gen_random_eth(const char *current_mac_address,
const char *generate_mac_address_mask)
{
struct ether_addr bin_addr;
nm_random_get_bytes(&bin_addr, ETH_ALEN);
_hw_addr_eth_complete(&bin_addr, current_mac_address, generate_mac_address_mask);
return nm_utils_hwaddr_ntoa(&bin_addr, ETH_ALEN);
}
static char *
_hw_addr_gen_stable_eth(NMUtilsStableType stable_type,
const char *stable_id,
const guint8 *host_id,
gsize host_id_len,
const char *ifname,
const char *current_mac_address,
const char *generate_mac_address_mask)
{
nm_auto_free_checksum GChecksum *sum = NULL;
guint32 tmp;
guint8 digest[NM_UTILS_CHECKSUM_LENGTH_SHA256];
struct ether_addr bin_addr;
guint8 stable_type_uint8;
nm_assert(stable_id);
nm_assert(host_id);
sum = g_checksum_new(G_CHECKSUM_SHA256);
host_id_len = NM_MIN(host_id_len, G_MAXUINT32);
nm_assert(stable_type < (NMUtilsStableType) 255);
stable_type_uint8 = stable_type;
g_checksum_update(sum, (const guchar *) &stable_type_uint8, sizeof(stable_type_uint8));
tmp = htonl((guint32) host_id_len);
g_checksum_update(sum, (const guchar *) &tmp, sizeof(tmp));
g_checksum_update(sum, (const guchar *) host_id, host_id_len);
g_checksum_update(sum, (const guchar *) (ifname ?: ""), ifname ? (strlen(ifname) + 1) : 1);
g_checksum_update(sum, (const guchar *) stable_id, strlen(stable_id) + 1);
nm_utils_checksum_get_digest(sum, digest);
memcpy(&bin_addr, digest, ETH_ALEN);
_hw_addr_eth_complete(&bin_addr, current_mac_address, generate_mac_address_mask);
return nm_utils_hwaddr_ntoa(&bin_addr, ETH_ALEN);
}
char *
nm_utils_hw_addr_gen_stable_eth_impl(NMUtilsStableType stable_type,
const char *stable_id,
const guint8 *host_id,
gsize host_id_len,
const char *ifname,
const char *current_mac_address,
const char *generate_mac_address_mask)
{
return _hw_addr_gen_stable_eth(stable_type,
stable_id,
host_id,
host_id_len,
ifname,
current_mac_address,
generate_mac_address_mask);
}
char *
nm_utils_hw_addr_gen_stable_eth(NMUtilsStableType stable_type,
const char *stable_id,
const char *ifname,
const char *current_mac_address,
const char *generate_mac_address_mask)
{
const guint8 *host_id;
gsize host_id_len;
g_return_val_if_fail(stable_id, NULL);
nm_utils_host_id_get(&host_id, &host_id_len);
return _hw_addr_gen_stable_eth(stable_type,
stable_id,
host_id,
host_id_len,
ifname,
current_mac_address,
generate_mac_address_mask);
}
/*****************************************************************************/
GBytes *
nm_utils_dhcp_client_id_mac(int arp_type, const guint8 *hwaddr, gsize hwaddr_len)
{
guint8 *client_id_buf;
const guint8 hwaddr_type = arp_type;
if (!nm_utils_arp_type_get_hwaddr_relevant_part(arp_type, &hwaddr, &hwaddr_len))
g_return_val_if_reached(NULL);
client_id_buf = g_malloc(hwaddr_len + 1);
client_id_buf[0] = hwaddr_type;
memcpy(&client_id_buf[1], hwaddr, hwaddr_len);
return g_bytes_new_take(client_id_buf, hwaddr_len + 1);
}
#define HASH_KEY \
NM_HASH_SEED_16(0x80, \
0x11, \
0x8c, \
0xc2, \
0xfe, \
0x4a, \
0x03, \
0xee, \
0x3e, \
0xd6, \
0x0c, \
0x6f, \
0x36, \
0x39, \
0x14, \
0x09)
/**
* nm_utils_create_dhcp_iaid:
* @legacy_unstable_byteorder: legacy behavior is to generate a u32 iaid which
* is endianness dependent. This is to preserve backward compatibility.
* For non-legacy behavior, the returned integer is in stable endianness,
* and corresponds to legacy behavior on little endian systems.
* @interface_id: the seed for hashing when generating the ID. Usually,
* this is the interface name.
* @interface_id_len: length of @interface_id
*
* This corresponds to systemd's dhcp_identifier_set_iaid() for generating
* a IAID for the interface.
*
* Returns: the IAID in host byte order. */
guint32
nm_utils_create_dhcp_iaid(gboolean legacy_unstable_byteorder,
const guint8 *interface_id,
gsize interface_id_len)
{
guint64 u64;
guint32 u32;
u64 = c_siphash_hash(HASH_KEY, interface_id, interface_id_len);
u32 = (u64 & 0xffffffffu) ^ (u64 >> 32);
if (legacy_unstable_byteorder) {
/* legacy systemd code dhcp_identifier_set_iaid() generates the iaid
* dependent on the host endianness. Since this function returns the IAID
* in native-byte order, we need to account for that.
*
* On little endian systems, we want the legacy-behavior is identical to
* the endianness-agnostic behavior. So, we need to swap the bytes on
* big-endian systems.
*
* (https://github.com/systemd/systemd/pull/10614). */
return htole32(u32);
} else {
/* we return the value as-is, in native byte order. */
return u32;
}
}
GBytes *
nm_utils_dhcp_client_id_duid(guint32 iaid, const guint8 *duid, gsize duid_len)
{
struct _nm_packed {
guint8 type;
guint32 iaid;
guint8 duid[];
} *client_id;
gsize total_size;
/* the @duid must include the 16 bit duid-type and the data (of max 128 bytes). */
g_return_val_if_fail(duid_len > 2 && duid_len < 128 + 2, NULL);
g_return_val_if_fail(duid, NULL);
total_size = sizeof(*client_id) + duid_len;
client_id = g_malloc(total_size);
client_id->type = 255;
unaligned_write_be32(&client_id->iaid, iaid);
memcpy(client_id->duid, duid, duid_len);
return g_bytes_new_take(client_id, total_size);
}
/**
* nm_utils_dhcp_client_id_systemd_node_specific_full:
* @iaid: the IAID (identity association identifier) in native byte order
* @machine_id: the binary identifier for the machine. It is hashed
* into the DUID. It commonly is /etc/machine-id (parsed in binary as NMUuid).
* @machine_id_len: the length of the @machine_id.
*
* Systemd's sd_dhcp_client generates a default client ID (type 255, node-specific,
* RFC 4361) if no explicit client-id is set. This function duplicates that
* implementation and exposes it as (internal) API.
*
* Returns: a %GBytes of generated client-id. This function cannot fail.
*/
GBytes *
nm_utils_dhcp_client_id_systemd_node_specific_full(guint32 iaid,
const guint8 *machine_id,
gsize machine_id_len)
{
const guint16 DUID_TYPE_EN = 2;
const guint32 SYSTEMD_PEN = 43793;
struct _nm_packed {
guint8 type;
guint32 iaid;
struct _nm_packed {
guint16 type;
union {
struct _nm_packed {
/* DUID_TYPE_EN */
guint32 pen;
uint8_t id[8];
} en;
};
} duid;
} *client_id;
guint64 u64;
g_return_val_if_fail(machine_id, NULL);
g_return_val_if_fail(machine_id_len > 0, NULL);
client_id = g_malloc(sizeof(*client_id));
client_id->type = 255;
unaligned_write_be32(&client_id->iaid, iaid);
unaligned_write_be16(&client_id->duid.type, DUID_TYPE_EN);
unaligned_write_be32(&client_id->duid.en.pen, SYSTEMD_PEN);
u64 = htole64(c_siphash_hash(HASH_KEY, machine_id, machine_id_len));
memcpy(client_id->duid.en.id, &u64, sizeof(client_id->duid.en.id));
G_STATIC_ASSERT_EXPR(sizeof(*client_id) == 19);
return g_bytes_new_take(client_id, 19);
}
GBytes *
nm_utils_dhcp_client_id_systemd_node_specific(guint32 iaid)
{
return nm_utils_dhcp_client_id_systemd_node_specific_full(
iaid,
(const guint8 *) nm_utils_machine_id_bin(),
sizeof(NMUuid));
}
/*****************************************************************************/
GBytes *
nm_utils_generate_duid_llt(int arp_type, const guint8 *hwaddr, gsize hwaddr_len, gint64 time)
{
guint8 *arr;
const guint16 duid_type = htons(1);
const guint16 hw_type = htons(arp_type);
const guint32 duid_time = htonl(NM_MAX(0, time - NM_UTILS_EPOCH_DATETIME_200001010000));
if (!nm_utils_arp_type_get_hwaddr_relevant_part(arp_type, &hwaddr, &hwaddr_len))
nm_assert_not_reached();
arr = g_new(guint8, (2u + 2u + 4u) + hwaddr_len);
memcpy(&arr[0], &duid_type, 2);
memcpy(&arr[2], &hw_type, 2);
memcpy(&arr[4], &duid_time, 4);
memcpy(&arr[8], hwaddr, hwaddr_len);
return g_bytes_new_take(arr, (2u + 2u + 4u) + hwaddr_len);
}
GBytes *
nm_utils_generate_duid_ll(int arp_type, const guint8 *hwaddr, gsize hwaddr_len)
{
guint8 *arr;
const guint16 duid_type = htons(3);
const guint16 hw_type = htons(arp_type);
if (!nm_utils_arp_type_get_hwaddr_relevant_part(arp_type, &hwaddr, &hwaddr_len))
nm_assert_not_reached();
arr = g_new(guint8, (2u + 2u) + hwaddr_len);
memcpy(&arr[0], &duid_type, 2);
memcpy(&arr[2], &hw_type, 2);
memcpy(&arr[4], hwaddr, hwaddr_len);
return g_bytes_new_take(arr, (2u + 2u) + hwaddr_len);
}
GBytes *
nm_utils_generate_duid_uuid(const NMUuid *uuid)
{
const guint16 duid_type = htons(4);
guint8 *duid_buffer;
nm_assert(uuid);
/* Generate a DHCP Unique Identifier for DHCPv6 using the
* DUID-UUID method (see RFC 6355 section 4). Format is:
*
* u16: type (DUID-UUID = 4)
* u8[16]: UUID bytes
*/
G_STATIC_ASSERT_EXPR(sizeof(duid_type) == 2);
G_STATIC_ASSERT_EXPR(sizeof(*uuid) == 16);
duid_buffer = g_malloc(18);
memcpy(&duid_buffer[0], &duid_type, 2);
memcpy(&duid_buffer[2], uuid, 16);
return g_bytes_new_take(duid_buffer, 18);
}
GBytes *
nm_utils_generate_duid_from_machine_id(void)
{
static GBytes *volatile global_duid = NULL;
GBytes *p;
again:
p = g_atomic_pointer_get(&global_duid);
if (G_UNLIKELY(!p)) {
nm_auto_free_checksum GChecksum *sum = NULL;
const NMUuid *machine_id;
union {
guint8 sha256[NM_UTILS_CHECKSUM_LENGTH_SHA256];
NMUuid uuid;
} digest;
machine_id = nm_utils_machine_id_bin();
/* Hash the machine ID so it's not leaked to the network.
*
* Optimally, we would choose an use case specific seed, but for historic
* reasons we didn't. */
sum = g_checksum_new(G_CHECKSUM_SHA256);
g_checksum_update(sum, (const guchar *) machine_id, sizeof(*machine_id));
nm_utils_checksum_get_digest(sum, digest.sha256);
G_STATIC_ASSERT_EXPR(sizeof(digest.sha256) > sizeof(digest.uuid));
p = nm_utils_generate_duid_uuid(&digest.uuid);
if (!g_atomic_pointer_compare_and_exchange(&global_duid, NULL, p)) {
g_bytes_unref(p);
goto again;
}
}
return g_bytes_ref(p);
}
/*****************************************************************************/
/**
* nm_utils_setpgid:
* @unused: unused
*
* This can be passed as a child setup function to the g_spawn*() family
* of functions, to ensure that the child is in its own process group
* (and thus, in some situations, will not be killed when NetworkManager
* is killed).
*/
void
nm_utils_setpgid(gpointer unused G_GNUC_UNUSED)
{
pid_t pid;
pid = getpid();
setpgid(pid, pid);
}
/**
* nm_utils_g_value_set_strv:
* @value: a #GValue, initialized to store a #G_TYPE_STRV
* @strings: a #GPtrArray of strings. %NULL values are not
* allowed.
*
* Converts @strings to a #GStrv and stores it in @value.
*/
void
nm_utils_g_value_set_strv(GValue *value, GPtrArray *strings)
{
char **strv;
guint i;
strv = g_new(char *, strings->len + 1);
for (i = 0; i < strings->len; i++) {
nm_assert(strings->pdata[i]);
strv[i] = g_strdup(strings->pdata[i]);
}
strv[i] = NULL;
g_value_take_boxed(value, strv);
}
/*****************************************************************************/
const char *
nm_utils_dnsmasq_status_to_string(int status, char *dest, gsize size)
{
const char *msg;
nm_utils_to_string_buffer_init(&dest, &size);
if (status == 0)
msg = "Success";
else if (status == 1)
msg = "Configuration problem";
else if (status == 2)
msg = "Network access problem (address in use, permissions)";
else if (status == 3)
msg = "Filesystem problem (missing file/directory, permissions)";
else if (status == 4)
msg = "Memory allocation failure";
else if (status == 5)
msg = "Other problem";
else if (status >= 11) {
g_snprintf(dest, size, "Lease script failed with error %d", status - 10);
return dest;
} else
msg = "Unknown problem";
g_snprintf(dest, size, "%s (%d)", msg, status);
return dest;
}
/**
* nm_utils_get_reverse_dns_domains_ip_4:
* @addr: IP address in network order
* @plen: prefix length
* @domains: array for results
*
* Creates reverse DNS domains for the given address and prefix length, and
* append them to @domains.
*/
void
nm_utils_get_reverse_dns_domains_ip_4(guint32 addr, guint8 plen, GPtrArray *domains)
{
guint32 ip, ip2, mask;
guchar *p;
guint octets;
guint i;
gsize len0, len;
char *str, *s;
g_return_if_fail(domains);
g_return_if_fail(plen <= 32);
if (!plen)
return;
octets = (plen - 1) / 8 + 1;
ip = ntohl(addr);
mask = 0xFFFFFFFF << (32 - plen);
ip &= mask;
ip2 = ip;
len0 = NM_STRLEN("in-addr.arpa") + (4 * octets) + 1;
while ((ip2 & mask) == ip) {
addr = htonl(ip2);
p = (guchar *) &addr;
len = len0;
str = s = g_malloc(len);
for (i = octets; i > 0; i--)
nm_strbuf_append(&s, &len, "%u.", p[i - 1] & 0xff);
nm_strbuf_append_str(&s, &len, "in-addr.arpa");
g_ptr_array_add(domains, str);
ip2 += 1 << ((32 - plen) & ~7);
}
}
/**
* nm_utils_get_reverse_dns_domains_ip_6:
* @addr: IPv6 address
* @plen: prefix length
* @domains: array for results
*
* Creates reverse DNS domains for the given address and prefix length, and
* append them to @domains.
*/
void
nm_utils_get_reverse_dns_domains_ip_6(const struct in6_addr *ip, guint8 plen, GPtrArray *domains)
{
struct in6_addr addr;
guint nibbles, bits, entries;
int i, j;
gsize len0, len;
char *str, *s;
g_return_if_fail(domains);
g_return_if_fail(plen <= 128);
if (!plen)
return;
memcpy(&addr, ip, sizeof(struct in6_addr));
nm_ip6_addr_clear_host_address(&addr, NULL, plen);
/* Number of nibbles to include in domains */
nibbles = (plen - 1) / 4 + 1;
/* Prefix length in nibble */
bits = plen - ((plen - 1) / 4 * 4);
/* Number of domains */
entries = 1 << (4 - bits);
len0 = NM_STRLEN("ip6.arpa") + (2 * nibbles) + 1;
#define N_SHIFT(x) ((x) % 2 ? 0 : 4)
for (i = 0; i < entries; i++) {
len = len0;
str = s = g_malloc(len);
for (j = nibbles - 1; j >= 0; j--)
nm_strbuf_append(&s, &len, "%x.", (addr.s6_addr[j / 2] >> N_SHIFT(j)) & 0xf);
nm_strbuf_append_str(&s, &len, "ip6.arpa");
g_ptr_array_add(domains, str);
addr.s6_addr[(nibbles - 1) / 2] += 1 << N_SHIFT(nibbles - 1);
}
#undef N_SHIFT
}
struct plugin_info {
char *path;
struct stat st;
};
static int
read_device_factory_paths_sort_fcn(gconstpointer a, gconstpointer b)
{
const struct plugin_info *da = a;
const struct plugin_info *db = b;
time_t ta, tb;
ta = NM_MAX(da->st.st_mtime, da->st.st_ctime);
tb = NM_MAX(db->st.st_mtime, db->st.st_ctime);
if (ta < tb)
return 1;
if (ta > tb)
return -1;
return 0;
}
gboolean
nm_utils_validate_plugin(const char *path, struct stat *st, GError **error)
{
g_return_val_if_fail(path, FALSE);
g_return_val_if_fail(st, FALSE);
g_return_val_if_fail(!error || !*error, FALSE);
if (!S_ISREG(st->st_mode)) {
g_set_error_literal(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "not a regular file");
return FALSE;
}
if (st->st_uid != 0) {
g_set_error_literal(error,
NM_UTILS_ERROR,
NM_UTILS_ERROR_UNKNOWN,
"file has invalid owner (should be root)");
return FALSE;
}
if (st->st_mode & (S_IWGRP | S_IWOTH | S_ISUID)) {
g_set_error_literal(error,
NM_UTILS_ERROR,
NM_UTILS_ERROR_UNKNOWN,
"file has invalid permissions");
return FALSE;
}
return TRUE;
}
char **
nm_utils_read_plugin_paths(const char *dirname, const char *prefix)
{
GDir *dir;
GError *error = NULL;
const char *item;
GArray *paths;
char **result;
guint i;
g_return_val_if_fail(dirname, NULL);
g_return_val_if_fail(prefix, NULL);
dir = g_dir_open(dirname, 0, &error);
if (!dir) {
nm_log_warn(LOGD_CORE,
"device plugin: failed to open directory %s: %s",
dirname,
error->message);
g_clear_error(&error);
return NULL;
}
paths = g_array_new(FALSE, FALSE, sizeof(struct plugin_info));
while ((item = g_dir_read_name(dir))) {
int errsv;
struct plugin_info data;
if (!g_str_has_prefix(item, prefix))
continue;
if (!g_str_has_suffix(item, ".so"))
continue;
data.path = g_build_filename(dirname, item, NULL);
if (stat(data.path, &data.st) != 0) {
errsv = errno;
nm_log_warn(LOGD_CORE,
"plugin: skip invalid file %s (error during stat: %s)",
data.path,
nm_strerror_native(errsv));
goto skip;
}
if (!nm_utils_validate_plugin(data.path, &data.st, &error)) {
nm_log_warn(LOGD_CORE, "plugin: skip invalid file %s: %s", data.path, error->message);
g_clear_error(&error);
goto skip;
}
g_array_append_val(paths, data);
continue;
skip:
g_free(data.path);
}
g_dir_close(dir);
/* sort filenames by modification time. */
g_array_sort(paths, read_device_factory_paths_sort_fcn);
result = g_new(char *, paths->len + 1);
for (i = 0; i < paths->len; i++)
result[i] = nm_g_array_index(paths, struct plugin_info, i).path;
result[i] = NULL;
g_array_free(paths, TRUE);
return result;
}
char *
nm_utils_format_con_diff_for_audit(GHashTable *diff)
{
GHashTable *setting_diff;
char *setting_name, *prop_name;
GHashTableIter iter, iter2;
GString *str;
str = g_string_sized_new(32);
g_hash_table_iter_init(&iter, diff);
while (g_hash_table_iter_next(&iter, (gpointer *) &setting_name, (gpointer *) &setting_diff)) {
if (!setting_diff)
continue;
g_hash_table_iter_init(&iter2, setting_diff);
while (g_hash_table_iter_next(&iter2, (gpointer *) &prop_name, NULL))
g_string_append_printf(str, "%s.%s,", setting_name, prop_name);
}
if (str->len)
str->str[str->len - 1] = '\0';
return g_string_free(str, FALSE);
}
const char *
nm_utils_parse_dns_domain(const char *domain, gboolean *is_routing)
{
g_return_val_if_fail(domain, NULL);
g_return_val_if_fail(domain[0], NULL);
if (domain[0] == '~') {
domain++;
NM_SET_OUT(is_routing, TRUE);
} else
NM_SET_OUT(is_routing, FALSE);
return domain;
}
/*****************************************************************************/
static guint32
get_max_rate_ht_20(int mcs)
{
switch (mcs) {
case 0:
return 6500000;
case 1:
case 8:
return 13000000;
case 2:
case 16:
return 19500000;
case 3:
case 9:
case 24:
return 26000000;
case 4:
case 10:
case 17:
return 39000000;
case 5:
case 11:
case 25:
return 52000000;
case 6:
case 18:
return 58500000;
case 7:
return 65000000;
case 12:
case 19:
case 26:
return 78000000;
case 13:
case 27:
return 104000000;
case 14:
case 20:
return 117000000;
case 15:
return 130000000;
case 21:
case 28:
return 156000000;
case 22:
return 175500000;
case 23:
return 195000000;
case 29:
return 208000000;
case 30:
return 234000000;
case 31:
return 260000000;
}
return 0;
}
static guint32
get_max_rate_ht_40(int mcs)
{
switch (mcs) {
case 0:
return 13500000;
case 1:
case 8:
return 27000000;
case 2:
return 40500000;
case 3:
case 9:
case 24:
return 54000000;
case 4:
case 10:
case 17:
return 81000000;
case 5:
case 11:
case 25:
return 108000000;
case 6:
case 18:
return 121500000;
case 7:
return 135000000;
case 12:
case 19:
case 26:
return 162000000;
case 13:
case 27:
return 216000000;
case 14:
case 20:
return 243000000;
case 15:
return 270000000;
case 16:
return 40500000;
case 21:
case 28:
return 324000000;
case 22:
return 364500000;
case 23:
return 405000000;
case 29:
return 432000000;
case 30:
return 486000000;
case 31:
return 540000000;
}
return 0;
}
static guint32
get_max_rate_vht_80_ss1(int mcs)
{
switch (mcs) {
case 0:
return 29300000;
case 1:
return 58500000;
case 2:
return 87800000;
case 3:
return 117000000;
case 4:
return 175500000;
case 5:
return 234000000;
case 6:
return 263300000;
case 7:
return 292500000;
case 8:
return 351000000;
case 9:
return 390000000;
}
return 0;
}
static guint32
get_max_rate_vht_80_ss2(int mcs)
{
switch (mcs) {
case 0:
return 58500000;
case 1:
return 117000000;
case 2:
return 175500000;
case 3:
return 234000000;
case 4:
return 351000000;
case 5:
return 468000000;
case 6:
return 526500000;
case 7:
return 585000000;
case 8:
return 702000000;
case 9:
return 780000000;
}
return 0;
}
static guint32
get_max_rate_vht_80_ss3(int mcs)
{
switch (mcs) {
case 0:
return 87800000;
case 1:
return 175500000;
case 2:
return 263300000;
case 3:
return 351000000;
case 4:
return 526500000;
case 5:
return 702000000;
case 6:
return 0; /* invalid */
case 7:
return 877500000;
case 8:
return 1053000000;
case 9:
return 1170000000;
}
return 0;
}
static guint32
get_max_rate_vht_160_ss1(int mcs)
{
switch (mcs) {
case 0:
return 58500000;
case 1:
return 117000000;
case 2:
return 175500000;
case 3:
return 234000000;
case 4:
return 351000000;
case 5:
return 468000000;
case 6:
return 526500000;
case 7:
return 585000000;
case 8:
return 702000000;
case 9:
return 780000000;
}
return 0;
}
static guint32
get_max_rate_vht_160_ss2(int mcs)
{
switch (mcs) {
case 0:
return 117000000;
case 1:
return 234000000;
case 2:
return 351000000;
case 3:
return 468000000;
case 4:
return 702000000;
case 5:
return 936000000;
case 6:
return 1053000000;
case 7:
return 1170000000;
case 8:
return 1404000000;
case 9:
return 1560000000;
}
return 0;
}
static guint32
get_max_rate_vht_160_ss3(int mcs)
{
switch (mcs) {
case 0:
return 175500000;
case 1:
return 351000000;
case 2:
return 526500000;
case 3:
return 702000000;
case 4:
return 1053000000;
case 5:
return 1404000000;
case 6:
return 1579500000;
case 7:
return 1755000000;
case 8:
return 2106000000;
case 9:
return 0; /* invalid */
}
return 0;
}
static gboolean
get_max_rate_ht(const guint8 *bytes, guint len, guint32 *out_maxrate)
{
guint32 i;
guint8 ht_cap_info;
const guint8 *supported_mcs_set;
guint32 rate;
/* http://standards.ieee.org/getieee802/download/802.11-2012.pdf
* https://mrncciew.com/2014/10/19/cwap-ht-capabilities-ie/
*/
if (len != 26)
return FALSE;
ht_cap_info = bytes[0];
supported_mcs_set = &bytes[3];
*out_maxrate = 0;
/* Find the maximum supported mcs rate */
for (i = 0; i <= 76; i++) {
const unsigned mcs_octet = i / 8;
const unsigned MCS_RATE_BIT = 1 << i % 8;
if (supported_mcs_set[mcs_octet] & MCS_RATE_BIT) {
/* Check for 40Mhz wide channel support */
if (ht_cap_info & (1 << 1))
rate = get_max_rate_ht_40(i);
else
rate = get_max_rate_ht_20(i);
if (rate > *out_maxrate)
*out_maxrate = rate;
}
}
return TRUE;
}
static gboolean
get_max_rate_vht(const guint8 *bytes, guint len, guint32 *out_maxrate)
{
guint32 mcs, m;
guint8 vht_cap, tx_map;
/* https://tda802dot11.blogspot.it/2014/10/vht-capabilities-element-vht.html
* http://chimera.labs.oreilly.com/books/1234000001739/ch03.html#management_frames */
if (len != 12)
return FALSE;
vht_cap = bytes[0];
tx_map = bytes[8];
/* Check for mcs rates 8 and 9 support */
if (tx_map & 0x2a)
mcs = 9;
else if (tx_map & 0x15)
mcs = 8;
else
mcs = 7;
/* Check for 160Mhz wide channel support and
* spatial stream support */
if (vht_cap & (1 << 2)) {
if (tx_map & 0x30)
m = get_max_rate_vht_160_ss3(mcs);
else if (tx_map & 0x0C)
m = get_max_rate_vht_160_ss2(mcs);
else
m = get_max_rate_vht_160_ss1(mcs);
} else {
if (tx_map & 0x30)
m = get_max_rate_vht_80_ss3(mcs);
else if (tx_map & 0x0C)
m = get_max_rate_vht_80_ss2(mcs);
else
m = get_max_rate_vht_80_ss1(mcs);
}
*out_maxrate = m;
return TRUE;
}
static gboolean
get_bandwidth_ht(const guint8 *bytes, guint len, guint32 *out_bandwidth)
{
guint8 ht_op_flag_group;
/* http://standards.ieee.org/getieee802/download/802.11-2012.pdf
* https://mrncciew.com/2014/11/04/cwap-ht-operations-ie/
* IEEE std 802.11-2020 section 9.4.2.56
*/
if (len != 22)
return FALSE;
ht_op_flag_group = bytes[1];
/* Check bit for 20Mhz or 40Mhz */
if (ht_op_flag_group & (1 << 2))
*out_bandwidth = 40;
else
*out_bandwidth = 20;
return TRUE;
}
static gboolean
get_bandwidth_vht(const guint8 *bytes, guint len, guint32 *out_bandwidth)
{
guint8 sta_channel_width;
guint8 ccfs0;
guint8 ccfs1;
/* http://chimera.labs.oreilly.com/books/1234000001739/ch03.html#management_frames
* https://community.arubanetworks.com/community-home/librarydocuments/viewdocument?DocumentKey=799aad1b-d9c4-421a-a492-a111e8680d34&CommunityKey=39a6bdf4-2376-46f9-853a-49420d2d0caa&tab=librarydocuments
* IEEE Std 802.11-2020 section 9.4.2.158
*/
if (len < 3)
return FALSE;
sta_channel_width = bytes[0];
ccfs0 = bytes[1];
ccfs1 = bytes[2];
switch (sta_channel_width) {
case 0:
/* we rely on HT Operation IE value*/
return FALSE;
case 1:
if (ccfs1 == 0)
*out_bandwidth = 80;
else if (abs(ccfs1 - ccfs0) == 8)
*out_bandwidth = 160;
else if (abs(ccfs1 - ccfs0) > 16)
/* we are considering 80+80 as 160 */
*out_bandwidth = 160;
else
/* falling back to 80 MHz */
*out_bandwidth = 80;
break;
case 2:
/* deprecated */
*out_bandwidth = 160;
break;
case 3:
/* deprecated */
*out_bandwidth = 160;
break;
default:
return FALSE;
}
return TRUE;
}
/* Management Frame Information Element IDs, ieee80211_eid */
#define WLAN_EID_HT_CAPABILITY 45
#define WLAN_EID_HT_OPERATION 61
#define WLAN_EID_VHT_CAPABILITY 191
#define WLAN_EID_VHT_OPERATION 192
#define WLAN_EID_VENDOR_SPECIFIC 221
void
nm_wifi_utils_parse_ies(const guint8 *bytes,
gsize len,
guint32 *out_max_rate,
guint32 *out_bandwidth,
gboolean *out_metered,
gboolean *out_owe_transition_mode)
{
guint8 id, elem_len;
guint32 m;
NM_SET_OUT(out_max_rate, 0);
NM_SET_OUT(out_bandwidth, 0);
NM_SET_OUT(out_metered, FALSE);
NM_SET_OUT(out_owe_transition_mode, FALSE);
while (len) {
if (len < 2)
break;
id = *bytes++;
elem_len = *bytes++;
len -= 2;
if (elem_len > len)
break;
switch (id) {
case WLAN_EID_HT_CAPABILITY:
if (out_max_rate) {
if (get_max_rate_ht(bytes, elem_len, &m))
*out_max_rate = NM_MAX(*out_max_rate, m);
}
break;
case WLAN_EID_HT_OPERATION:
if (out_bandwidth)
get_bandwidth_ht(bytes, elem_len, out_bandwidth);
break;
case WLAN_EID_VHT_CAPABILITY:
if (out_max_rate) {
if (get_max_rate_vht(bytes, elem_len, &m))
*out_max_rate = NM_MAX(*out_max_rate, m);
}
break;
case WLAN_EID_VHT_OPERATION:
if (out_bandwidth)
get_bandwidth_vht(bytes, elem_len, out_bandwidth);
break;
case WLAN_EID_VENDOR_SPECIFIC:
if (len == 8 && bytes[0] == 0x00 /* OUI: Microsoft */
&& bytes[1] == 0x50 && bytes[2] == 0xf2
&& bytes[3] == 0x11) /* OUI type: Network cost */
{
/* https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nct/ */
NM_SET_OUT(out_metered, (bytes[4] > 1)); /* Cost level > 1 */
}
if (elem_len >= 10 && bytes[0] == 0x50 /* OUI: WiFi Alliance */
&& bytes[1] == 0x6f && bytes[2] == 0x9a
&& bytes[3] == 0x1c) /* OUI type: OWE Transition Mode */
NM_SET_OUT(out_owe_transition_mode, TRUE);
break;
}
len -= elem_len;
bytes += elem_len;
}
}
/*****************************************************************************/
guint8
nm_wifi_utils_level_to_quality(int val)
{
if (val < 0) {
/* Assume dBm already; rough conversion: best = -40, worst = -100 */
val = abs(CLAMP(val, -100, -40) + 40); /* normalize to 0 */
val = 100 - (int) ((100.0 * (double) val) / 60.0);
} else if (val > 110 && val < 256) {
/* assume old-style WEXT 8-bit unsigned signal level */
val -= 256; /* subtract 256 to convert to dBm */
val = abs(CLAMP(val, -100, -40) + 40); /* normalize to 0 */
val = 100 - (int) ((100.0 * (double) val) / 60.0);
} else {
/* Assume signal is a "quality" percentage */
}
return CLAMP(val, 0, 100);
}
/*****************************************************************************/
NM_UTILS_LOOKUP_STR_DEFINE(nm_activation_type_to_string,
NMActivationType,
NM_UTILS_LOOKUP_DEFAULT_WARN("(unknown)"),
NM_UTILS_LOOKUP_STR_ITEM(NM_ACTIVATION_TYPE_MANAGED, "managed"),
NM_UTILS_LOOKUP_STR_ITEM(NM_ACTIVATION_TYPE_ASSUME, "assume"),
NM_UTILS_LOOKUP_STR_ITEM(NM_ACTIVATION_TYPE_EXTERNAL, "external"), );
/*****************************************************************************/
typedef struct {
GPid pid;
GTask *task;
gulong cancellable_id;
GSource *child_watch_source;
GSource *timeout_source;
int child_stdin;
int child_stdout;
int child_stderr;
gboolean binary_output;
GSource *input_source;
GSource *output_source;
GSource *error_source;
NMStrBuf in_buffer;
NMStrBuf out_buffer;
NMStrBuf err_buffer;
gsize out_buffer_offset;
} HelperInfo;
#define _NMLOG2_PREFIX_NAME "nm-daemon-helper"
#define _NMLOG2_DOMAIN LOGD_CORE
#define _NMLOG2(level, info, ...) \
G_STMT_START \
{ \
if (nm_logging_enabled((level), (_NMLOG2_DOMAIN))) { \
HelperInfo *_info = (info); \
\
_nm_log((level), \
(_NMLOG2_DOMAIN), \
0, \
NULL, \
NULL, \
_NMLOG2_PREFIX_NAME "[" NM_HASH_OBFUSCATE_PTR_FMT \
",%d]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
NM_HASH_OBFUSCATE_PTR(_info), \
_info->pid _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
} \
} \
G_STMT_END
static void
helper_info_free(gpointer data)
{
HelperInfo *info = data;
nm_clear_g_source_inst(&info->child_watch_source);
nm_clear_g_source_inst(&info->timeout_source);
g_object_unref(info->task);
nm_str_buf_destroy(&info->in_buffer);
nm_str_buf_destroy(&info->out_buffer);
nm_str_buf_destroy(&info->err_buffer);
nm_clear_g_source_inst(&info->input_source);
nm_clear_g_source_inst(&info->output_source);
nm_clear_g_source_inst(&info->error_source);
nm_clear_fd(&info->child_stdout);
nm_clear_fd(&info->child_stdin);
nm_clear_fd(&info->child_stderr);
if (info->pid != -1) {
nm_assert(info->pid > 1);
nm_utils_kill_child_async(info->pid, SIGKILL, LOGD_CORE, "nm-daemon-helper", 0, NULL, NULL);
}
g_free(info);
}
static void
helper_complete(HelperInfo *info, GError *error)
{
if (error) {
if (info->err_buffer.len > 0) {
_LOG2T(info, "stderr: %s", nm_str_buf_get_str(&info->err_buffer));
}
nm_clear_g_cancellable_disconnect(g_task_get_cancellable(info->task),
&info->cancellable_id);
g_task_return_error(info->task, error);
helper_info_free(info);
return;
}
if (info->input_source || info->output_source || info->pid != -1) {
/* Wait that pipes are closed and process has terminated */
return;
}
nm_clear_g_cancellable_disconnect(g_task_get_cancellable(info->task), &info->cancellable_id);
if (info->binary_output) {
g_task_return_pointer(
info->task,
g_bytes_new(nm_str_buf_get_str_unsafe(&info->in_buffer), info->in_buffer.len),
(GDestroyNotify) (g_bytes_unref));
} else {
g_task_return_pointer(info->task,
nm_str_buf_finalize(&info->in_buffer, NULL) ?: g_new0(char, 1),
g_free);
}
helper_info_free(info);
}
static gboolean
helper_can_write(int fd, GIOCondition condition, gpointer user_data)
{
HelperInfo *info = user_data;
gssize n_written;
int errsv;
if (NM_FLAGS_HAS(condition, G_IO_ERR)) {
errsv = EIO;
goto out_error;
} else if (NM_FLAGS_HAS(condition, G_IO_HUP)) {
errsv = EPIPE;
goto out_error;
}
n_written = write(info->child_stdin,
&((nm_str_buf_get_str_unsafe(&info->out_buffer))[info->out_buffer_offset]),
info->out_buffer.len - info->out_buffer_offset);
errsv = errno;
if (n_written < 0 && errsv != EAGAIN)
goto out_error;
if (n_written > 0) {
if ((gsize) n_written >= (info->out_buffer.len - info->out_buffer_offset)) {
nm_assert((gsize) n_written == (info->out_buffer.len - info->out_buffer_offset));
nm_clear_g_source_inst(&info->output_source);
nm_close(info->child_stdin);
info->child_stdin = -1;
return G_SOURCE_CONTINUE;
}
info->out_buffer_offset += (gsize) n_written;
}
return G_SOURCE_CONTINUE;
out_error:
nm_clear_g_source_inst(&info->output_source);
helper_complete(info,
g_error_new(NM_UTILS_ERROR,
NM_UTILS_ERROR_UNKNOWN,
"error writing to helper: %d (%s)",
errsv,
nm_strerror_native(errsv)));
return G_SOURCE_CONTINUE;
}
static gboolean
helper_have_data(int fd, GIOCondition condition, gpointer user_data)
{
HelperInfo *info = user_data;
gssize n_read;
GError *error = NULL;
n_read = nm_utils_fd_read(fd, &info->in_buffer);
_LOG2T(info, "read returns %ld", (long) n_read);
if (n_read > 0)
return G_SOURCE_CONTINUE;
nm_clear_g_source_inst(&info->input_source);
nm_clear_fd(&info->child_stdout);
_LOG2T(info, "stdout closed");
if (n_read < 0) {
error = g_error_new(NM_UTILS_ERROR,
NM_UTILS_ERROR_UNKNOWN,
"read from process returned %d (%s)",
(int) -n_read,
nm_strerror_native((int) -n_read));
}
helper_complete(info, error);
return G_SOURCE_CONTINUE;
}
static gboolean
helper_have_err_data(int fd, GIOCondition condition, gpointer user_data)
{
HelperInfo *info = user_data;
gssize n_read;
n_read = nm_utils_fd_read(fd, &info->err_buffer);
if (n_read > 0)
return G_SOURCE_CONTINUE;
nm_clear_g_source_inst(&info->error_source);
nm_clear_fd(&info->child_stderr);
return G_SOURCE_CONTINUE;
}
static void
helper_child_terminated(GPid pid, int status, gpointer user_data)
{
HelperInfo *info = user_data;
GError *error = NULL;
gs_free char *status_desc = NULL;
_LOG2D(info, "process %s", (status_desc = nm_utils_get_process_exit_status_desc(status)));
info->pid = -1;
nm_clear_g_source_inst(&info->child_watch_source);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
if (!status_desc)
status_desc = nm_utils_get_process_exit_status_desc(status);
error =
g_error_new(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "helper process %s", status_desc);
}
helper_complete(info, error);
}
static gboolean
helper_timeout(gpointer user_data)
{
HelperInfo *info = user_data;
nm_clear_g_source_inst(&info->timeout_source);
helper_complete(info, g_error_new_literal(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "timed out"));
return G_SOURCE_CONTINUE;
}
static void
helper_cancelled(GObject *object, gpointer user_data)
{
HelperInfo *info = user_data;
GError *error = NULL;
nm_clear_g_signal_handler(g_task_get_cancellable(info->task), &info->cancellable_id);
nm_utils_error_set_cancelled(&error, FALSE, NULL);
helper_complete(info, error);
}
void
nm_utils_spawn_helper(const char *const *args,
gboolean binary_output,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer cb_data)
{
gs_free_error GError *error = NULL;
gs_free char *commands = NULL;
HelperInfo *info;
const char *const *arg;
GMainContext *context;
gsize n;
nm_assert(args && args[0]);
info = g_new(HelperInfo, 1);
*info = (HelperInfo) {
.task = nm_g_task_new(NULL, cancellable, nm_utils_spawn_helper, callback, cb_data),
.binary_output = binary_output,
};
/* Store if the caller requested binary output so that we can check later
* that the right result function is called. */
g_task_set_task_data(info->task, GINT_TO_POINTER(binary_output), NULL);
if (!g_spawn_async_with_pipes("/",
(char **) NM_MAKE_STRV(LIBEXECDIR "/nm-daemon-helper"),
(char **) NM_MAKE_STRV(),
G_SPAWN_CLOEXEC_PIPES | G_SPAWN_DO_NOT_REAP_CHILD,
NULL,
NULL,
&info->pid,
&info->child_stdin,
&info->child_stdout,
&info->child_stderr,
&error)) {
info->child_stdin = -1;
info->child_stdout = -1;
info->child_stderr = -1;
info->pid = -1;
g_task_return_error(info->task,
g_error_new(NM_UTILS_ERROR,
NM_UTILS_ERROR_UNKNOWN,
"error spawning nm-helper: %s",
error->message));
helper_info_free(info);
return;
}
_LOG2D(info, "spawned process with args: %s", (commands = g_strjoinv(" ", (char **) args)));
context = g_task_get_context(info->task);
/* The async function makes a lukewarm attempt to honor the current thread default
* context. However, it later uses nm_utils_kill_child_async() which always uses
* g_main_context_default(). For now, the function really can only be used with the
* main context. */
nm_assert(context == g_main_context_default());
/* We are using a GChildWatchSource in combination with kill()/waitpid()
* (where helper_info_free() clears the source and calls
* nm_utils_kill_child_async()). That leads to races where glib might have
* already reaped the process and our waitpid() call fails with:
*
* <error> [TIMESTAMP] kill child process 'nm-daemon-helper' (PID): failed due to unexpected return value -1 by waitpid (No child processes, 10) after sending SIGKILL (9)
*
* This is a bug in glib, addressed by [1]. Maybe there should be a
* workaround here, and not using the child watcher?
*
* [1] https://gitlab.gnome.org/GNOME/glib/-/merge_requests/3353
*/
info->child_watch_source = nm_g_child_watch_source_new(info->pid,
G_PRIORITY_DEFAULT,
helper_child_terminated,
info,
NULL);
g_source_attach(info->child_watch_source, context);
info->timeout_source =
nm_g_timeout_source_new_seconds(20, G_PRIORITY_DEFAULT, helper_timeout, info, NULL);
g_source_attach(info->timeout_source, context);
nm_io_fcntl_setfl_update_nonblock(info->child_stdin);
nm_io_fcntl_setfl_update_nonblock(info->child_stdout);
nm_io_fcntl_setfl_update_nonblock(info->child_stderr);
/* Watch process stdin */
for (n = 1, arg = args; *arg; arg++)
n += strlen(*arg) + 1u;
info->out_buffer = NM_STR_BUF_INIT(n, TRUE);
for (arg = args; *arg; arg++) {
nm_str_buf_append(&info->out_buffer, *arg);
nm_str_buf_append_c(&info->out_buffer, '\0');
}
info->output_source = nm_g_unix_fd_source_new(info->child_stdin,
G_IO_OUT | G_IO_ERR | G_IO_HUP,
G_PRIORITY_DEFAULT,
helper_can_write,
info,
NULL);
g_source_attach(info->output_source, context);
/* Watch process stdout */
info->in_buffer = NM_STR_BUF_INIT(0, FALSE);
info->input_source = nm_g_unix_fd_source_new(info->child_stdout,
G_IO_IN | G_IO_ERR | G_IO_HUP,
G_PRIORITY_DEFAULT,
helper_have_data,
info,
NULL);
g_source_attach(info->input_source, context);
/* Watch process stderr */
info->err_buffer = NM_STR_BUF_INIT(0, FALSE);
info->error_source = nm_g_unix_fd_source_new(info->child_stderr,
G_IO_IN | G_IO_ERR | G_IO_HUP,
G_PRIORITY_DEFAULT,
helper_have_err_data,
info,
NULL);
g_source_attach(info->error_source, context);
if (cancellable) {
gulong signal_id;
signal_id = g_cancellable_connect(cancellable, G_CALLBACK(helper_cancelled), info, NULL);
if (signal_id == 0) {
/* the request is already cancelled. Return. */
return;
}
info->cancellable_id = signal_id;
}
}
char *
nm_utils_spawn_helper_finish_string(GAsyncResult *result, GError **error)
{
GTask *task = G_TASK(result);
nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper));
/* Check binary_output */
nm_assert(GPOINTER_TO_INT(g_task_get_task_data(task)) == FALSE);
return g_task_propagate_pointer(task, error);
}
GBytes *
nm_utils_spawn_helper_finish_binary(GAsyncResult *result, GError **error)
{
GTask *task = G_TASK(result);
nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper));
/* Check binary_output */
nm_assert(GPOINTER_TO_INT(g_task_get_task_data(task)) == TRUE);
return g_task_propagate_pointer(task, error);
}
/*****************************************************************************/
/**
* nm_utils_get_nm_uid:
*
* Checks what EUID NetworkManager is running as.
* Saves the EUID so it can be reused without making many syscalls.
*/
uid_t
nm_utils_get_nm_uid(void)
{
static int u_static = -1;
int u;
again:
if ((u = g_atomic_int_get(&u_static)) == -1) {
uid_t u2;
u2 = geteuid();
u = u2;
nm_assert(u == u2);
nm_assert(u >= 0);
if (!g_atomic_int_compare_and_exchange(&u_static, -1, u))
goto again;
}
return u;
}
/**
* nm_utils_get_nm_gid:
*
* Checks what EGID NetworkManager is running as.
* Saves the EGID so it can be reused without making many syscalls.
*/
gid_t
nm_utils_get_nm_gid(void)
{
static int g_static = -1;
int g;
again:
if ((g = g_atomic_int_get(&g_static)) == -1) {
gid_t g2;
g2 = getegid();
g = g2;
nm_assert(g == g2);
nm_assert(g >= 0);
if (!g_atomic_int_compare_and_exchange(&g_static, -1, g))
goto again;
}
return g;
}
/*****************************************************************************/
/**
* nm_utils_shorten_hostname:
* @hostname: the input hostname
* @shortened: (out) (transfer full): on return, the shortened hostname
*
* Checks whether the input hostname is valid. If not, tries to shorten it
* to HOST_NAME_MAX (64) or to the first dot, whatever comes earlier.
* The new hostname is returned in @shortened.
*
* Returns: %TRUE if the input hostname was already valid or if was shortened
* successfully; %FALSE otherwise
*/
gboolean
nm_utils_shorten_hostname(const char *hostname, char **shortened)
{
gs_free char *s = NULL;
const char *dot;
gsize l;
nm_assert(hostname);
nm_assert(shortened);
if (nm_hostname_is_valid(hostname, FALSE)) {
*shortened = NULL;
return TRUE;
}
dot = strchr(hostname, '.');
if (dot)
l = (dot - hostname);
else
l = strlen(hostname);
l = NM_MIN(l, (gsize) NM_HOST_NAME_MAX);
s = g_strndup(hostname, l);
if (!nm_hostname_is_valid(s, FALSE)) {
*shortened = NULL;
return FALSE;
}
*shortened = g_steal_pointer(&s);
return TRUE;
}
/**
* nm_utils_connection_supported:
* @connection: the connection
* @error: on return, the reason why the connection in not supported
*
* Returns whether the given connection is supported by this version
* of NetworkManager.
*/
gboolean
nm_utils_connection_supported(NMConnection *connection, GError **error)
{
const char *type;
const char *feature = NULL;
g_return_val_if_fail(connection, FALSE);
g_return_val_if_fail(!error || !*error, FALSE);
type = nm_connection_get_connection_type(connection);
if (!WITH_TEAMDCTL) {
NMSettingConnection *s_con;
if (nm_streq0(type, NM_SETTING_TEAM_SETTING_NAME)) {
feature = "team";
goto out_disabled;
}
/* Match team ports */
if ((s_con = nm_connection_get_setting_connection(connection))
&& nm_streq0(nm_setting_connection_get_port_type(s_con),
NM_SETTING_TEAM_SETTING_NAME)) {
feature = "team";
goto out_disabled;
}
}
if (!WITH_OPENVSWITCH) {
if (NM_IN_STRSET(type,
NM_SETTING_OVS_BRIDGE_SETTING_NAME,
NM_SETTING_OVS_PORT_SETTING_NAME,
NM_SETTING_OVS_INTERFACE_SETTING_NAME)) {
feature = "Open vSwitch";
goto out_disabled;
}
/* Match OVS system interfaces */
if (nm_connection_get_setting_ovs_interface(connection)) {
feature = "Open vSwitch";
goto out_disabled;
}
}
if (!WITH_WIFI
&& NM_IN_STRSET(type,
NM_SETTING_WIRELESS_SETTING_NAME,
NM_SETTING_OLPC_MESH_SETTING_NAME,
NM_SETTING_WIFI_P2P_SETTING_NAME)) {
feature = "Wi-Fi";
goto out_disabled;
}
if (!WITH_WWAN
&& NM_IN_STRSET(type, NM_SETTING_GSM_SETTING_NAME, NM_SETTING_CDMA_SETTING_NAME)) {
feature = "WWAN";
goto out_disabled;
}
if (nm_streq0(type, NM_SETTING_WIMAX_SETTING_NAME)) {
feature = "WiMAX";
goto out_removed;
}
return TRUE;
out_disabled:
nm_assert(feature);
g_set_error(error,
NM_SETTINGS_ERROR,
NM_SETTINGS_ERROR_FEATURE_DISABLED,
"%s support is disabled in this build",
feature);
return FALSE;
out_removed:
nm_assert(feature);
g_set_error(error,
NM_SETTINGS_ERROR,
NM_SETTINGS_ERROR_FEATURE_REMOVED,
"%s is no longer supported",
feature);
return FALSE;
}
/*****************************************************************************/
/**
* nm_rate_limit_check():
* @rate_limit: the NMRateLimit instance
* @window_sec: the time window in seconds, between 1 and 864000 (ten days)
* @burst: the number of max allowed event occurrences in the given time
* window
*
* The function rate limits an event. Call it multiple times with the
* same @window_sec, and @burst values.
*
* Returns: TRUE if the event is allowed, FALSE if it is rate-limited
*/
gboolean
nm_rate_limit_check(NMRateLimit *rate_limit, gint32 window_sec, gint32 burst)
{
gint64 now;
gint64 old_ts_msec;
gint64 window_msec;
gint64 capacity;
gint64 elapsed;
nm_assert(window_sec >= 1 && window_sec <= 864000);
nm_assert(burst >= 1);
/* This implements a simple token bucket algorithm. For each millisecond,
* refill "burst" tokens. Thus, during a full time window we
* refill (window_msec * burst) tokens. Each event consumes @window_msec
* tokens. */
window_msec = (gint64) window_sec * NM_UTILS_MSEC_PER_SEC;
capacity = window_msec * (gint64) burst;
old_ts_msec = rate_limit->ts_msec;
now = nm_utils_get_monotonic_timestamp_msec();
rate_limit->ts_msec = now;
elapsed = now - old_ts_msec;
if (old_ts_msec == 0 || elapsed > window_msec) {
/* On the first call, or in case a whole window passed, (re)start with
* a full budget */
rate_limit->tokens = capacity;
} else {
rate_limit->tokens += elapsed * (gint64) burst;
rate_limit->tokens = NM_MIN(rate_limit->tokens, capacity);
}
/* Consume the tokens */
if (rate_limit->tokens >= window_msec) {
rate_limit->tokens -= window_msec;
return TRUE;
}
return FALSE;
}
const char *
nm_utils_get_connection_first_permissions_user(NMConnection *connection)
{
NMSettingConnection *s_con;
s_con = nm_connection_get_setting_connection(connection);
nm_assert(s_con);
return _nm_setting_connection_get_first_permissions_user(s_con);
}
/*****************************************************************************/
const char **
nm_utils_get_connection_private_files_paths(NMConnection *connection)
{
GPtrArray *files;
gs_free NMSetting **settings = NULL;
guint num_settings;
guint i;
files = g_ptr_array_new();
settings = nm_connection_get_settings(connection, &num_settings);
for (i = 0; i < num_settings; i++) {
_nm_setting_get_private_files(settings[i], files);
}
g_ptr_array_add(files, NULL);
return (const char **) g_ptr_array_free(files, files->len == 1);
}
typedef struct _ReadInfo ReadInfo;
typedef struct {
char *path;
ReadInfo *read_info;
} FileInfo;
struct _ReadInfo {
GTask *task;
GHashTable *table;
GPtrArray *file_infos; /* of FileInfo */
GError *first_error;
guint num_pending;
};
static void
read_file_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
FileInfo *file_info = user_data;
ReadInfo *read_info = file_info->read_info;
gs_unref_bytes GBytes *output = NULL;
gs_free_error GError *error = NULL;
output = nm_utils_spawn_helper_finish_binary(result, &error);
nm_assert(read_info->num_pending > 0);
read_info->num_pending--;
if (nm_utils_error_is_cancelled(error)) {
/* nop */
} else if (error) {
nm_log_dbg(LOGD_CORE,
"read-private-files: failed to read file '%s': %s",
file_info->path,
error->message);
if (!read_info->first_error) {
/* @error just says "helper process exited with status X".
* Return a more human-friendly one. */
read_info->first_error = g_error_new(NM_UTILS_ERROR,
NM_UTILS_ERROR_UNKNOWN,
"error reading file '%s'",
file_info->path);
}
} else {
nm_log_dbg(LOGD_SUPPLICANT,
"read-private-files: successfully read file '%s'",
file_info->path);
/* Store the file contents in the hash table */
if (!read_info->table) {
read_info->table = g_hash_table_new_full(nm_str_hash,
g_str_equal,
g_free,
(GDestroyNotify) g_bytes_unref);
}
g_hash_table_insert(read_info->table,
g_steal_pointer(&file_info->path),
g_steal_pointer(&output));
}
g_clear_pointer(&file_info->path, g_free);
/* If all operations are completed, return */
if (read_info->num_pending == 0) {
if (read_info->first_error) {
g_task_return_error(read_info->task, g_steal_pointer(&read_info->first_error));
} else {
g_task_return_pointer(read_info->task,
g_steal_pointer(&read_info->table),
(GDestroyNotify) g_hash_table_unref);
}
if (read_info->table)
g_hash_table_unref(read_info->table);
if (read_info->file_infos)
g_ptr_array_unref(read_info->file_infos);
g_object_unref(read_info->task);
g_free(read_info);
}
}
/**
* nm_utils_read_private_files:
* @paths: array of file paths to be read
* @user: name of the user to impersonate when reading the files
* @cancellable: cancellable to cancel the operation
* @callback: callback to invoke on completion
* @cb_data: data for @callback
*
* Reads the given list of files @paths on behalf of user @user. Invokes
* @callback asynchronously on completion. The callback must use
* nm_utils_read_private_files_finish() to obtain the result.
*/
void
nm_utils_read_private_files(const char *const *paths,
const char *user,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer cb_data)
{
ReadInfo *read_info;
FileInfo *file_info;
guint i;
g_return_if_fail(paths && paths[0]);
g_return_if_fail(cancellable);
g_return_if_fail(callback);
g_return_if_fail(cb_data);
read_info = g_new(ReadInfo, 1);
*read_info = (ReadInfo) {
.task = nm_g_task_new(NULL, cancellable, nm_utils_read_private_files, callback, cb_data),
.file_infos = g_ptr_array_new_with_free_func(g_free),
};
for (i = 0; paths[i]; i++) {
file_info = g_new(FileInfo, 1);
*file_info = (FileInfo) {
.path = g_strdup(paths[i]),
.read_info = read_info,
};
g_ptr_array_add(read_info->file_infos, file_info);
read_info->num_pending++;
nm_utils_spawn_helper(NM_MAKE_STRV("read-file-as-user", user, paths[i]),
TRUE,
cancellable,
read_file_helper_cb,
file_info);
}
}
/**
* nm_utils_read_private_files_finish:
* @result: the GAsyncResult
* @error: on return, the error
*
* Returns the files read by nm_utils_read_private_files(). The return value
* is a hash table {char * -> GBytes *}. Free it with g_hash_table_unref().
*/
GHashTable *
nm_utils_read_private_files_finish(GAsyncResult *result, GError **error)
{
GTask *task = G_TASK(result);
nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_read_private_files));
return g_task_propagate_pointer(task, error);
}