
While NetworkManager is of course (mostly) single threaded, our static functions really should be thread safe. "mostly" single threaded because we have GDBus's worker thread, we use a thread for writing non-blocking SR-IOV sysctl, in the past (or still?) we used a thread for async glibc resolver. Anyway, a low-level function like must be thread safe, when it uses global data. Granted, the initialize-once pattern with the flag and the int variable, is probably in many cases good enough. But it makes me unhappy, the thought of accessing static data without a synchronized operation.
5204 lines
170 KiB
C
5204 lines
170 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()
|
|
{
|
|
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()
|
|
{
|
|
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: (allow-none): 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;
|
|
/* 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 ": 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) (allow-none): 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;
|
|
/* 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 ": 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 %ld milliseconds for process to terminate "
|
|
"normally after sending %s...",
|
|
LOG_NAME_ARGS,
|
|
(long) MAX(wait_before_kill_msec, 0),
|
|
_kc_signal_to_string(sig));
|
|
was_waiting = TRUE;
|
|
}
|
|
|
|
sleep_time = MIN(wait_until - now, 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 = 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 = MIN(wait_until_sigkill - now, sleep_duration_usec);
|
|
}
|
|
|
|
if (!was_waiting) {
|
|
if (wait_until_sigkill != 0) {
|
|
nm_log_dbg(log_domain,
|
|
LOG_NAME_PROCESS_FMT
|
|
": waiting up to %ld milliseconds for process to disappear before "
|
|
"sending KILL signal after sending %s...",
|
|
LOG_NAME_ARGS,
|
|
(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 = 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 {
|
|
const char *interface_name;
|
|
const char *device_type;
|
|
const char *driver;
|
|
const char *driver_version;
|
|
const char *dhcp_plugin;
|
|
struct {
|
|
const char *value;
|
|
gboolean is_parsed;
|
|
guint len;
|
|
guint8 bin[_NM_UTILS_HWADDR_LEN_MAX];
|
|
} hwaddr;
|
|
struct {
|
|
const char *value;
|
|
gboolean is_parsed;
|
|
guint32 a;
|
|
guint32 b;
|
|
guint32 c;
|
|
} s390_subchannels;
|
|
} MatchDeviceData;
|
|
|
|
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, MatchDeviceData *match_data)
|
|
{
|
|
guint32 a, b, c;
|
|
|
|
if (G_UNLIKELY(!match_data->s390_subchannels.is_parsed)) {
|
|
match_data->s390_subchannels.is_parsed = TRUE;
|
|
|
|
if (!match_data->s390_subchannels.value
|
|
|| !match_device_s390_subchannels_parse(match_data->s390_subchannels.value,
|
|
&match_data->s390_subchannels.a,
|
|
&match_data->s390_subchannels.b,
|
|
&match_data->s390_subchannels.c)) {
|
|
match_data->s390_subchannels.value = NULL;
|
|
return FALSE;
|
|
}
|
|
} else if (!match_data->s390_subchannels.value)
|
|
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, MatchDeviceData *match_data)
|
|
{
|
|
if (G_UNLIKELY(!match_data->hwaddr.is_parsed)) {
|
|
match_data->hwaddr.is_parsed = TRUE;
|
|
|
|
if (match_data->hwaddr.value) {
|
|
gsize l;
|
|
|
|
if (!_nm_utils_hwaddr_aton(match_data->hwaddr.value,
|
|
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)
|
|
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, MatchDeviceData *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->interface_name) {
|
|
if (nm_streq(spec_str, match_data->interface_name))
|
|
return TRUE;
|
|
if (use_pattern && g_pattern_match_simple(spec_str, match_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->interface_name && nm_streq(spec_str, match_data->interface_name))
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
NMMatchSpecMatchType
|
|
nm_match_spec_device(const GSList *specs,
|
|
const char *interface_name,
|
|
const char *device_type,
|
|
const char *driver,
|
|
const char *driver_version,
|
|
const char *hwaddr,
|
|
const char *s390_subchannels,
|
|
const char *dhcp_plugin)
|
|
{
|
|
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;
|
|
MatchDeviceData match_data = {
|
|
.interface_name = interface_name,
|
|
.device_type = nm_str_not_empty(device_type),
|
|
.driver = nm_str_not_empty(driver),
|
|
.driver_version = nm_str_not_empty(driver_version),
|
|
.dhcp_plugin = nm_str_not_empty(dhcp_plugin),
|
|
.hwaddr =
|
|
{
|
|
.value = hwaddr,
|
|
},
|
|
.s390_subchannels =
|
|
{
|
|
.value = s390_subchannels,
|
|
},
|
|
};
|
|
|
|
nm_assert(!hwaddr || nm_utils_hwaddr_valid(hwaddr, -1));
|
|
|
|
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_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);
|
|
}
|
|
|
|
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 =
|
|
&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 =
|
|
&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 random timestamp from the past two years. Yes, this timestamp
|
|
* is 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 = nm_hash_siphash42(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];
|
|
|
|
/*
|
|
(stat -c '%s' /var/lib/NetworkManager/secret_key;
|
|
echo -n ' ';
|
|
cat /var/lib/NetworkManager/secret_key;
|
|
cat /etc/machine-id | tr -d '\n' | sed -n 's/[a-f0-9-]/\0/pg') | sha256sum
|
|
*/
|
|
|
|
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;
|
|
|
|
success = nm_utils_random_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;
|
|
|
|
if (!success)
|
|
nm_log_warn(LOGD_CORE,
|
|
"secret-key: failure to generate good random data for secret-key (use "
|
|
"non-persistent key)");
|
|
else 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,
|
|
&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;
|
|
gint64 timestamp_ns;
|
|
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_ns);
|
|
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_ns(void)
|
|
{
|
|
return _host_id_get()->timestamp_ns;
|
|
}
|
|
|
|
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 *timestamp_ns)
|
|
{
|
|
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,
|
|
timestamp_ns ? *timestamp_ns : 0,
|
|
timestamp_ns ? "" : " (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_ns = timestamp_ns ? *timestamp_ns : 0,
|
|
.is_good = is_good,
|
|
.timestamp_is_good = !!timestamp_ns,
|
|
};
|
|
|
|
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 = &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;
|
|
|
|
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.
|
|
*/
|
|
g_return_val_if_fail(hwaddr_len == INFINIBAND_ALEN, FALSE);
|
|
memcpy(out_iid->id_u8, hwaddr + INFINIBAND_ALEN - 8, 8);
|
|
out_iid->id_u8[0] |= 0x02;
|
|
return TRUE;
|
|
case NM_LINK_TYPE_GRE:
|
|
/* Hardware address is the network-endian IPv4 address */
|
|
g_return_val_if_fail(hwaddr_len == 4, FALSE);
|
|
addr = *(guint32 *) 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;
|
|
case NM_LINK_TYPE_6LOWPAN:
|
|
/* The hardware address is already 64-bit. This is the case for
|
|
* IEEE 802.15.4 networks. */
|
|
memcpy(out_iid->id_u8, hwaddr, sizeof(out_iid->id_u8));
|
|
return TRUE;
|
|
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_utils_random_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(GString *str, const char *substitution)
|
|
{
|
|
if (!substitution)
|
|
substitution = "";
|
|
g_string_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,
|
|
char **out_generated)
|
|
{
|
|
gsize i, idx_start;
|
|
GString *str = NULL;
|
|
|
|
g_return_val_if_fail(out_generated, NM_UTILS_STABLE_TYPE_RANDOM);
|
|
|
|
if (!stable_id) {
|
|
*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 subsitutions 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 (g_str_has_prefix(&stable_id[i], "" prefix "")) { \
|
|
_match = TRUE; \
|
|
if (!str) \
|
|
str = g_string_sized_new(256); \
|
|
i += NM_STRLEN(prefix); \
|
|
g_string_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 (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.
|
|
*/
|
|
if (str)
|
|
g_string_free(str, TRUE);
|
|
*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) {
|
|
*out_generated = NULL;
|
|
return NM_UTILS_STABLE_TYPE_STABLE_ID;
|
|
}
|
|
|
|
if (idx_start < i)
|
|
g_string_append_len(str, &stable_id[idx_start], i - idx_start);
|
|
*out_generated = g_string_free(str, FALSE);
|
|
return NM_UTILS_STABLE_TYPE_GENERATED;
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
|
|
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 = 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) {
|
|
/* g_random_int() is good enough here. It uses a static GRand instance
|
|
* that is seeded from /dev/urandom. */
|
|
oui = ouis[g_random_int() % 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_utils_random_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 = 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 \
|
|
((const guint8[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_utils_ip6_address_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 = MAX(da->st.st_mtime, da->st.st_ctime);
|
|
tb = 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] = 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;
|
|
case 7:
|
|
return 877500000;
|
|
case 8:
|
|
return 105300000;
|
|
case 9:
|
|
return 117000000;
|
|
}
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
|
|
/* Management Frame Information Element IDs, ieee80211_eid */
|
|
#define WLAN_EID_HT_CAPABILITY 45
|
|
#define WLAN_EID_VHT_CAPABILITY 191
|
|
#define WLAN_EID_VENDOR_SPECIFIC 221
|
|
|
|
void
|
|
nm_wifi_utils_parse_ies(const guint8 *bytes,
|
|
gsize len,
|
|
guint32 *out_max_rate,
|
|
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_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_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_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;
|
|
GSource *input_source;
|
|
GSource *output_source;
|
|
|
|
NMStrBuf in_buffer;
|
|
NMStrBuf out_buffer;
|
|
gsize out_buffer_offset;
|
|
} HelperInfo;
|
|
|
|
#define _NMLOG_PREFIX_NAME "helper"
|
|
#define _NMLOG_DOMAIN LOGD_CORE
|
|
#define _NMLOG2(level, info, ...) \
|
|
G_STMT_START \
|
|
{ \
|
|
if (nm_logging_enabled((level), (_NMLOG_DOMAIN))) { \
|
|
HelperInfo *_info = (info); \
|
|
\
|
|
_nm_log((level), \
|
|
(_NMLOG_DOMAIN), \
|
|
0, \
|
|
NULL, \
|
|
NULL, \
|
|
_NMLOG_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_clear_g_source_inst(&info->input_source);
|
|
nm_clear_g_source_inst(&info->output_source);
|
|
|
|
if (info->child_stdout != -1)
|
|
nm_close(info->child_stdout);
|
|
if (info->child_stdin != -1)
|
|
nm_close(info->child_stdin);
|
|
|
|
if (info->pid != -1) {
|
|
nm_assert(info->pid > 1);
|
|
nm_utils_kill_child_async(info->pid, SIGKILL, LOGD_CORE, _NMLOG_PREFIX_NAME, 0, NULL, NULL);
|
|
}
|
|
|
|
g_free(info);
|
|
}
|
|
|
|
static void
|
|
helper_complete(HelperInfo *info, GError *error)
|
|
{
|
|
if (error) {
|
|
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);
|
|
g_task_return_pointer(info->task, nm_str_buf_finalize(&info->in_buffer, NULL), 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_close(info->child_stdout);
|
|
info->child_stdout = -1;
|
|
|
|
_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 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,
|
|
GCancellable *cancellable,
|
|
GAsyncReadyCallback callback,
|
|
gpointer cb_data)
|
|
{
|
|
gs_free_error GError *error = NULL;
|
|
gs_free char *commands = NULL;
|
|
HelperInfo *info;
|
|
int fd_flags;
|
|
const char *const *arg;
|
|
|
|
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),
|
|
.child_stdin = -1,
|
|
.child_stdout = -1,
|
|
.pid = -1,
|
|
};
|
|
|
|
if (!g_spawn_async_with_pipes("/",
|
|
(char **) NM_MAKE_STRV(LIBEXECDIR "/nm-daemon-helper"),
|
|
(char **) NM_MAKE_STRV(),
|
|
G_SPAWN_DO_NOT_REAP_CHILD,
|
|
NULL,
|
|
NULL,
|
|
&info->pid,
|
|
&info->child_stdin,
|
|
&info->child_stdout,
|
|
NULL,
|
|
&error)) {
|
|
info->child_stdin = -1;
|
|
info->child_stdout = -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)));
|
|
|
|
info->child_watch_source = g_child_watch_source_new(info->pid);
|
|
g_source_set_callback(info->child_watch_source,
|
|
G_SOURCE_FUNC(helper_child_terminated),
|
|
info,
|
|
NULL);
|
|
g_source_attach(info->child_watch_source, g_main_context_get_thread_default());
|
|
|
|
info->timeout_source =
|
|
nm_g_timeout_source_new_seconds(20, G_PRIORITY_DEFAULT, helper_timeout, info, NULL);
|
|
g_source_attach(info->timeout_source, g_main_context_get_thread_default());
|
|
|
|
/* Set file descriptors as non-blocking */
|
|
fd_flags = fcntl(info->child_stdin, F_GETFD, 0);
|
|
fcntl(info->child_stdin, F_SETFL, fd_flags | O_NONBLOCK);
|
|
fd_flags = fcntl(info->child_stdout, F_GETFD, 0);
|
|
fcntl(info->child_stdout, F_SETFL, fd_flags | O_NONBLOCK);
|
|
|
|
/* Watch process stdin */
|
|
nm_str_buf_init(&info->out_buffer, 32, 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, g_main_context_get_thread_default());
|
|
|
|
/* Watch process stdout */
|
|
nm_str_buf_init(&info->in_buffer, NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, 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, g_main_context_get_thread_default());
|
|
|
|
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(GAsyncResult *result, GError **error)
|
|
{
|
|
GTask *task = G_TASK(result);
|
|
|
|
nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper));
|
|
|
|
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 = geteuid();
|
|
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;
|
|
}
|