/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2004 - 2018 Red Hat, Inc.
* Copyright (C) 2005 - 2008 Novell, Inc.
*/
#include "nm-default.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 "nm-std-aux/unaligned.h"
#include "nm-glib-aux/nm-random-utils.h"
#include "nm-glib-aux/nm-io-utils.h"
#include "nm-glib-aux/nm-secret-utils.h"
#include "nm-glib-aux/nm-time-utils.h"
#include "nm-utils.h"
#include "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);
}
/*****************************************************************************/
static double
_exp10(guint16 ex)
{
double v;
if (ex == 0)
return 1.0;
v = _exp10(ex / 2);
v = v * v;
if (ex % 2)
v *= 10;
return v;
}
/*
* nm_utils_exp10:
* @ex: the exponent
*
* Returns: 10^ex, or pow(10, ex), or exp10(ex).
*/
double
nm_utils_exp10(gint16 ex)
{
if (ex >= 0)
return _exp10(ex);
return 1.0 / _exp10(-((gint32) ex));
}
/*****************************************************************************/
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);
}
static const char *
_trunk_first_line(char *str)
{
char *s;
s = strchr(str, '\n');
if (s)
s[0] = '\0';
return str;
}
int
nm_utils_modprobe(GError **error, gboolean suppress_error_logging, const char *arg1, ...)
{
gs_unref_ptrarray GPtrArray *argv = NULL;
int exit_status;
gs_free char * _log_str = NULL;
#define ARGV_TO_STR(argv) \
(_log_str ? _log_str : (_log_str = g_strjoinv(" ", (char **) argv->pdata)))
GError * local = NULL;
va_list ap;
NMLogLevel llevel = suppress_error_logging ? LOGL_DEBUG : LOGL_ERR;
gs_free char *std_out = NULL, *std_err = NULL;
g_return_val_if_fail(!error || !*error, -1);
g_return_val_if_fail(arg1, -1);
/* construct the argument list */
argv = g_ptr_array_sized_new(4);
g_ptr_array_add(argv, "/sbin/modprobe");
g_ptr_array_add(argv, "--use-blacklist");
g_ptr_array_add(argv, (char *) arg1);
va_start(ap, arg1);
while ((arg1 = va_arg(ap, const char *)))
g_ptr_array_add(argv, (char *) arg1);
va_end(ap);
g_ptr_array_add(argv, NULL);
nm_log_dbg(LOGD_CORE, "modprobe: '%s'", ARGV_TO_STR(argv));
if (!g_spawn_sync(NULL,
(char **) argv->pdata,
NULL,
0,
NULL,
NULL,
&std_out,
&std_err,
&exit_status,
&local)) {
nm_log(llevel,
LOGD_CORE,
NULL,
NULL,
"modprobe: '%s' failed: %s",
ARGV_TO_STR(argv),
local->message);
g_propagate_error(error, local);
return -1;
} else if (exit_status != 0) {
nm_log(llevel,
LOGD_CORE,
NULL,
NULL,
"modprobe: '%s' exited with error %d%s%s%s%s%s%s",
ARGV_TO_STR(argv),
exit_status,
std_out && *std_out ? " (" : "",
std_out && *std_out ? _trunk_first_line(std_out) : "",
std_out && *std_out ? ")" : "",
std_err && *std_err ? " (" : "",
std_err && *std_err ? _trunk_first_line(std_err) : "",
std_err && *std_err ? ")" : "");
}
return exit_status;
}
/*****************************************************************************/
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;
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 = (gint64) sleep_duration_msec * (guint64) 1000L;
return x < G_MAXULONG ? (gulong) x : G_MAXULONG;
}
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);
}
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;
}
/*****************************************************************************/
char *
nm_utils_new_vlan_name(const char *parent_iface, guint32 vlan_id)
{
guint id_len;
gsize parent_len;
char *ifname;
g_return_val_if_fail(parent_iface && *parent_iface, NULL);
if (vlan_id < 10)
id_len = 2;
else if (vlan_id < 100)
id_len = 3;
else if (vlan_id < 1000)
id_len = 4;
else {
g_return_val_if_fail(vlan_id < 4095, NULL);
id_len = 5;
}
ifname = g_new(char, IFNAMSIZ);
parent_len = strlen(parent_iface);
parent_len = MIN(parent_len, IFNAMSIZ - 1 - id_len);
memcpy(ifname, parent_iface, parent_len);
g_snprintf(&ifname[parent_len], IFNAMSIZ - parent_len, ".%u", vlan_id);
return ifname;
}
/* nm_utils_new_infiniband_name:
* @name: the output-buffer where the value will be written. Must be
* not %NULL and point to a string buffer of at least IFNAMSIZ bytes.
* @parent_name: the parent interface name
* @p_key: the partition key.
*
* Returns: the infiniband name will be written to @name and @name
* is returned.
*/
const char *
nm_utils_new_infiniband_name(char *name, const char *parent_name, int p_key)
{
g_return_val_if_fail(name, NULL);
g_return_val_if_fail(parent_name && parent_name[0], NULL);
g_return_val_if_fail(strlen(parent_name) < IFNAMSIZ, NULL);
/* technically, p_key of 0x0000 and 0x8000 is not allowed either. But we don't
* want to assert against that in nm_utils_new_infiniband_name(). So be more
* resilient here, and accept those. */
g_return_val_if_fail(p_key >= 0 && p_key <= 0xffff, NULL);
/* If parent+suffix is too long, kernel would just truncate
* the name. We do the same. See ipoib_vlan_add(). */
g_snprintf(name, IFNAMSIZ, "%s.%04x", parent_name, p_key);
return name;
}
/*****************************************************************************/
/**
* 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 {
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);
}
#define IPV6_PROPERTY_DIR "/proc/sys/net/ipv6/conf/"
#define IPV4_PROPERTY_DIR "/proc/sys/net/ipv4/conf/"
G_STATIC_ASSERT(sizeof(IPV4_PROPERTY_DIR) == sizeof(IPV6_PROPERTY_DIR));
G_STATIC_ASSERT(NM_STRLEN(IPV6_PROPERTY_DIR) + IFNAMSIZ + 60
== NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE);
/**
* nm_utils_sysctl_ip_conf_path:
* @addr_family: either AF_INET or AF_INET6.
* @buf: the output buffer where to write the path. It
* must be at least NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE bytes
* long.
* @ifname: an interface name
* @property: a property name
*
* Returns: the path to IPv6 property @property on @ifname. Note that
* this returns the input argument @buf.
*/
const char *
nm_utils_sysctl_ip_conf_path(int addr_family, char *buf, const char *ifname, const char *property)
{
int len;
nm_assert(buf);
nm_assert_addr_family(addr_family);
g_assert(nm_utils_ifname_valid_kernel(ifname, NULL));
property = NM_ASSERT_VALID_PATH_COMPONENT(property);
len = g_snprintf(buf,
NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE,
"%s%s/%s",
addr_family == AF_INET6 ? IPV6_PROPERTY_DIR : IPV4_PROPERTY_DIR,
ifname,
property);
g_assert(len < NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE - 1);
return buf;
}
gboolean
nm_utils_sysctl_ip_conf_is_path(int addr_family,
const char *path,
const char *ifname,
const char *property)
{
g_return_val_if_fail(path, FALSE);
NM_ASSERT_VALID_PATH_COMPONENT(property);
g_assert(!ifname || nm_utils_ifname_valid_kernel(ifname, NULL));
if (addr_family == AF_INET) {
if (!g_str_has_prefix(path, IPV4_PROPERTY_DIR))
return FALSE;
path += NM_STRLEN(IPV4_PROPERTY_DIR);
} else if (addr_family == AF_INET6) {
if (!g_str_has_prefix(path, IPV6_PROPERTY_DIR))
return FALSE;
path += NM_STRLEN(IPV6_PROPERTY_DIR);
} else
g_return_val_if_reached(FALSE);
if (ifname) {
if (!g_str_has_prefix(path, ifname))
return FALSE;
path += strlen(ifname);
if (path[0] != '/')
return FALSE;
path++;
} else {
const char *slash;
char buf[IFNAMSIZ];
gsize l;
slash = strchr(path, '/');
if (!slash)
return FALSE;
l = slash - path;
if (l >= IFNAMSIZ)
return FALSE;
memcpy(buf, path, l);
buf[l] = '\0';
if (!nm_utils_ifname_valid_kernel(buf, NULL))
return FALSE;
path = slash + 1;
}
if (!nm_streq(path, property))
return FALSE;
return TRUE;
}
gboolean
nm_utils_is_valid_path_component(const char *name)
{
const char *n;
if (name == NULL || name[0] == '\0')
return FALSE;
if (name[0] == '.') {
if (name[1] == '\0')
return FALSE;
if (name[1] == '.' && name[2] == '\0')
return FALSE;
}
n = name;
do {
if (*n == '/')
return FALSE;
} while (*(++n) != '\0');
return TRUE;
}
const char *
NM_ASSERT_VALID_PATH_COMPONENT(const char *name)
{
if (G_LIKELY(nm_utils_is_valid_path_component(name)))
return name;
nm_log_err(LOGD_CORE,
"Failed asserting path component: %s%s%s",
NM_PRINT_FMT_QUOTED(name, "\"", name, "\"", "(null)"));
g_error("FATAL: Failed asserting path component: %s%s%s",
NM_PRINT_FMT_QUOTED(name, "\"", name, "\"", "(null)"));
g_assert_not_reached();
}
/*****************************************************************************/
typedef struct {
NMUuid bin;
char
_nul_sentinel; /* just for safety, if somebody accidentally uses the binary in a string context. */
/* 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->_nul_sentinel = '\0';
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_utils_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_utils_uuid_is_null(&uuid)) {
/* an all-zero machine-id is not valid. */
is_fake = FALSE;
}
}
}
if (is_fake) {
const guint8 *seed_bin;
const char * 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)) {
/* 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 = "ab085f06-b629-46d1-a553-84eeba5683b6";
} else {
/* 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 = "7ff0c8f5-5399-4901-ab63-61bf594abe8b";
}
/* 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_utils_uuid_generate_from_string_bin(&uuid,
(const char *) seed_bin,
seed_len,
NM_UTILS_UUID_TYPE_VERSION5,
(gpointer) 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,
&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 *
_host_id_get(void)
{
static const HostIdData *volatile host_id_static;
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 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_utils_uuid_parse(nm_strstrip(contents), &uuid)) {
/* generate a random UUID instead. */
is_fake = TRUE;
_nm_utils_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;
}
/*****************************************************************************/
/**
* nm_utils_ipv6_addr_set_interface_identifier:
* @addr: output token encoded as %in6_addr
* @iid: %NMUtilsIPv6IfaceId interface identifier
*
* Converts the %NMUtilsIPv6IfaceId to an %in6_addr (suitable for use
* with Linux platform). This only copies the lower 8 bytes, ignoring
* the /64 network prefix which is expected to be all-zero for a valid
* token.
*/
void
nm_utils_ipv6_addr_set_interface_identifier(struct in6_addr *addr, const NMUtilsIPv6IfaceId iid)
{
memcpy(addr->s6_addr + 8, &iid.id_u8, 8);
}
/**
* nm_utils_ipv6_interface_identifier_get_from_addr:
* @iid: output %NMUtilsIPv6IfaceId interface identifier set from the token
* @addr: token encoded as %in6_addr
*
* Converts the %in6_addr encoded token (as used by Linux platform) to
* the interface identifier.
*/
void
nm_utils_ipv6_interface_identifier_get_from_addr(NMUtilsIPv6IfaceId * iid,
const struct in6_addr *addr)
{
memcpy(iid, addr->s6_addr + 8, 8);
}
/**
* nm_utils_ipv6_interface_identifier_get_from_token:
* @iid: output %NMUtilsIPv6IfaceId interface identifier set from the token
* @token: token encoded as string
*
* Converts the %in6_addr encoded token (as used in ip6 settings) to
* the interface identifier.
*
* Returns: %TRUE if the @token is a valid token, %FALSE otherwise
*/
gboolean
nm_utils_ipv6_interface_identifier_get_from_token(NMUtilsIPv6IfaceId *iid, const char *token)
{
struct in6_addr i6_token;
g_return_val_if_fail(token, FALSE);
if (!inet_pton(AF_INET6, token, &i6_token))
return FALSE;
if (!_nm_utils_inet6_is_token(&i6_token))
return FALSE;
nm_utils_ipv6_interface_identifier_get_from_addr(iid, &i6_token);
return TRUE;
}
/**
* nm_utils_inet6_interface_identifier_to_token:
* @iid: %NMUtilsIPv6IfaceId interface identifier
* @buf: the destination buffer of at least %NM_UTILS_INET_ADDRSTRLEN
* bytes.
*
* Converts the interface identifier to a string token.
*
* Returns: the input buffer filled with the id as string.
*/
const char *
nm_utils_inet6_interface_identifier_to_token(NMUtilsIPv6IfaceId iid,
char buf[static INET6_ADDRSTRLEN])
{
struct in6_addr i6_token = {.s6_addr = {
0,
}};
nm_assert(buf);
nm_utils_ipv6_addr_set_interface_identifier(&i6_token, iid);
return _nm_utils_inet6_ntop(&i6_token, buf);
}
/*****************************************************************************/
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;
}
static gboolean
_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,
GError ** error)
{
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);
return TRUE;
}
gboolean
nm_utils_ipv6_addr_set_stable_privacy_impl(NMUtilsStableType stable_type,
struct in6_addr * addr,
const char * ifname,
const char * network_id,
guint32 dad_counter,
guint8 * host_id,
gsize host_id_len,
GError ** error)
{
return _set_stable_privacy(stable_type,
addr,
ifname,
network_id,
dad_counter,
host_id,
host_id_len,
error);
}
#define RFC7217_IDGEN_RETRIES 3
/**
* 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(NMUtilsStableType stable_type,
struct in6_addr * addr,
const char * ifname,
const char * network_id,
guint32 dad_counter,
GError ** error)
{
const guint8 *host_id;
gsize host_id_len;
g_return_val_if_fail(network_id, FALSE);
if (dad_counter >= RFC7217_IDGEN_RETRIES) {
g_set_error_literal(error,
NM_UTILS_ERROR,
NM_UTILS_ERROR_UNKNOWN,
"Too many DAD collisions");
return FALSE;
}
nm_utils_host_id_get(&host_id, &host_id_len);
return _set_stable_privacy(stable_type,
addr,
ifname,
network_id,
dad_counter,
host_id,
host_id_len,
error);
}
/*****************************************************************************/
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);
}
/*****************************************************************************/
/**
* Takes a pair @timestamp and @duration, and returns the remaining duration based
* on the new timestamp @now.
*/
guint32
nm_utils_lifetime_rebase_relative_time_on_now(guint32 timestamp, guint32 duration, gint32 now)
{
gint64 t;
nm_assert(now >= 0);
if (duration == NM_PLATFORM_LIFETIME_PERMANENT)
return NM_PLATFORM_LIFETIME_PERMANENT;
if (timestamp == 0) {
/* if the @timestamp is zero, assume it was just left unset and that the relative
* @duration starts counting from @now. This is convenient to construct an address
* and print it in nm_platform_ip4_address_to_string().
*
* In general it does not make sense to set the @duration without anchoring at
* @timestamp because you don't know the absolute expiration time when looking
* at the address at a later moment. */
timestamp = now;
}
/* For timestamp > now, just accept it and calculate the expected(?) result. */
t = (gint64) timestamp + (gint64) duration - (gint64) now;
if (t <= 0)
return 0;
if (t >= NM_PLATFORM_LIFETIME_PERMANENT)
return NM_PLATFORM_LIFETIME_PERMANENT - 1;
return t;
}
guint32
nm_utils_lifetime_get(guint32 timestamp,
guint32 lifetime,
guint32 preferred,
gint32 now,
guint32 *out_preferred)
{
guint32 t_lifetime, t_preferred;
nm_assert(now >= 0);
if (timestamp == 0 && lifetime == 0) {
/* We treat lifetime==0 && timestamp==0 addresses as permanent addresses to allow easy
* creation of such addresses (without requiring to set the lifetime fields to
* NM_PLATFORM_LIFETIME_PERMANENT). The real lifetime==0 addresses (E.g. DHCP6 telling us
* to drop an address will have timestamp set.
*/
NM_SET_OUT(out_preferred, NM_PLATFORM_LIFETIME_PERMANENT);
g_return_val_if_fail(preferred == 0, NM_PLATFORM_LIFETIME_PERMANENT);
return NM_PLATFORM_LIFETIME_PERMANENT;
}
if (now <= 0)
now = nm_utils_get_monotonic_timestamp_sec();
t_lifetime = nm_utils_lifetime_rebase_relative_time_on_now(timestamp, lifetime, now);
if (!t_lifetime) {
NM_SET_OUT(out_preferred, 0);
return 0;
}
t_preferred = nm_utils_lifetime_rebase_relative_time_on_now(timestamp, preferred, now);
NM_SET_OUT(out_preferred, MIN(t_preferred, t_lifetime));
/* Assert that non-permanent addresses have a (positive) @timestamp. nm_utils_lifetime_rebase_relative_time_on_now()
* treats addresses with timestamp 0 as *now*. Addresses passed to _address_get_lifetime() always
* should have a valid @timestamp, otherwise on every re-sync, their lifetime will be extended anew.
*/
g_return_val_if_fail(timestamp != 0
|| (lifetime == NM_PLATFORM_LIFETIME_PERMANENT
&& preferred == NM_PLATFORM_LIFETIME_PERMANENT),
t_lifetime);
g_return_val_if_fail(t_preferred <= t_lifetime, t_lifetime);
return t_lifetime;
}
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_utils_strbuf_append(&s, &len, "%u.", p[i - 1] & 0xff);
nm_utils_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_utils_strbuf_append(&s, &len, "%x.", (addr.s6_addr[j / 2] >> N_SHIFT(j)) & 0xf);
nm_utils_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 */
NM_SET_OUT(out_metered, (bytes[7] > 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_ENUM2STR_DEFINE(nm_icmpv6_router_pref_to_string,
NMIcmpv6RouterPref,
NM_UTILS_ENUM2STR(NM_ICMPV6_ROUTER_PREF_LOW, "low"),
NM_UTILS_ENUM2STR(NM_ICMPV6_ROUTER_PREF_MEDIUM, "medium"),
NM_UTILS_ENUM2STR(NM_ICMPV6_ROUTER_PREF_HIGH, "high"),
NM_UTILS_ENUM2STR(NM_ICMPV6_ROUTER_PREF_INVALID, "invalid"), );
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"), );