/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2014 - 2016 Red Hat, Inc.
*/
#include "nm-default.h"
#include "nm-dhcp-listener.h"
#include <sys/socket.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include "nm-dhcp-helper-api.h"
#include "nm-dhcp-client.h"
#include "nm-dhcp-manager.h"
#include "nm-core-internal.h"
#include "nm-dbus-manager.h"
#include "NetworkManagerUtils.h"
#define PRIV_SOCK_PATH NMRUNDIR "/private-dhcp"
#define PRIV_SOCK_TAG "dhcp"
/*****************************************************************************/
const NMDhcpClientFactory *const _nm_dhcp_manager_factories[6] = {
/* the order here matters, as we will try the plugins in this order to find
* the first available plugin. */
#if WITH_DHCPCANON
&_nm_dhcp_client_factory_dhcpcanon,
#endif
#if WITH_DHCLIENT
&_nm_dhcp_client_factory_dhclient,
#endif
#if WITH_DHCPCD
&_nm_dhcp_client_factory_dhcpcd,
#endif
&_nm_dhcp_client_factory_internal,
&_nm_dhcp_client_factory_systemd,
&_nm_dhcp_client_factory_nettools,
};
/*****************************************************************************/
typedef struct {
NMDBusManager *dbus_mgr;
gulong new_conn_id;
gulong dis_conn_id;
GHashTable * connections;
} NMDhcpListenerPrivate;
struct _NMDhcpListener {
GObject parent;
NMDhcpListenerPrivate _priv;
};
struct _NMDhcpListenerClass {
GObjectClass parent;
};
enum { EVENT, LAST_SIGNAL };
static guint signals[LAST_SIGNAL] = {0};
G_DEFINE_TYPE(NMDhcpListener, nm_dhcp_listener, G_TYPE_OBJECT)
#define NM_DHCP_LISTENER_GET_PRIVATE(self) \
_NM_GET_PRIVATE(self, NMDhcpListener, NM_IS_DHCP_LISTENER)
NM_DEFINE_SINGLETON_GETTER(NMDhcpListener, nm_dhcp_listener_get, NM_TYPE_DHCP_LISTENER);
/*****************************************************************************/
#define _NMLOG_PREFIX_NAME "dhcp-listener"
#define _NMLOG_DOMAIN LOGD_DHCP
#define _NMLOG(level, ...) \
G_STMT_START \
{ \
const NMDhcpListener *_self = (self); \
char _prefix[64]; \
\
nm_log((level), \
(_NMLOG_DOMAIN), \
NULL, \
NULL, \
"%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
(_self != singleton_instance \
? nm_sprintf_buf(_prefix, "%s[%p]", _NMLOG_PREFIX_NAME, _self) \
: _NMLOG_PREFIX_NAME) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
} \
G_STMT_END
/*****************************************************************************/
static char *
get_option(GVariant *options, const char *key)
{
GVariant * value;
const guchar *bytes, *s;
gsize len;
char * converted, *d;
if (!g_variant_lookup(options, key, "@ay", &value))
return NULL;
bytes = g_variant_get_fixed_array(value, &len, 1);
/* Since the DHCP options come through environment variables, they should
* already be UTF-8 safe, but just make sure.
*/
converted = g_malloc(len + 1);
for (s = bytes, d = converted; s < bytes + len; s++, d++) {
/* Convert NULLs to spaces and non-ASCII characters to ? */
if (*s == '\0')
*d = ' ';
else if (*s > 127)
*d = '?';
else
*d = *s;
}
*d = '\0';
g_variant_unref(value);
return converted;
}
static void
_method_call_handle(NMDhcpListener *self, GVariant *parameters)
{
gs_free char * iface = NULL;
gs_free char * pid_str = NULL;
gs_free char * reason = NULL;
gs_unref_variant GVariant *options = NULL;
int pid;
gboolean handled = FALSE;
g_variant_get(parameters, "(@a{sv})", &options);
iface = get_option(options, "interface");
if (iface == NULL) {
_LOGW("dhcp-event: didn't have associated interface.");
return;
}
pid_str = get_option(options, "pid");
pid = _nm_utils_ascii_str_to_int64(pid_str, 10, 0, G_MAXINT32, -1);
if (pid == -1) {
_LOGW("dhcp-event: couldn't convert PID '%s' to an integer", pid_str ?: "(null)");
return;
}
reason = get_option(options, "reason");
if (reason == NULL) {
_LOGW("dhcp-event: (pid %d) DHCP event didn't have a reason", pid);
return;
}
g_signal_emit(self, signals[EVENT], 0, iface, pid, options, reason, &handled);
if (!handled) {
if (g_ascii_strcasecmp(reason, "RELEASE") == 0) {
/* Ignore event when the dhcp client gets killed and we receive its last message */
_LOGD("dhcp-event: (pid %d) unhandled RELEASE DHCP event for interface %s", pid, iface);
} else
_LOGW("dhcp-event: (pid %d) unhandled DHCP event for interface %s", pid, iface);
}
}
static void
_method_call(GDBusConnection * connection,
const char * sender,
const char * object_path,
const char * interface_name,
const char * method_name,
GVariant * parameters,
GDBusMethodInvocation *invocation,
gpointer user_data)
{
NMDhcpListener *self = NM_DHCP_LISTENER(user_data);
if (!nm_streq(interface_name, NM_DHCP_HELPER_SERVER_INTERFACE_NAME)
|| !nm_streq(method_name, NM_DHCP_HELPER_SERVER_METHOD_NOTIFY)) {
g_dbus_method_invocation_return_error(invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_UNKNOWN_METHOD,
"Unknown method %s",
method_name);
return;
}
_method_call_handle(self, parameters);
g_dbus_method_invocation_return_value(invocation, NULL);
}
static GDBusInterfaceInfo *const interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO(
NM_DHCP_HELPER_SERVER_INTERFACE_NAME,
.methods = NM_DEFINE_GDBUS_METHOD_INFOS(
NM_DEFINE_GDBUS_METHOD_INFO(NM_DHCP_HELPER_SERVER_METHOD_NOTIFY,
.in_args = NM_DEFINE_GDBUS_ARG_INFOS(
NM_DEFINE_GDBUS_ARG_INFO("data", "a{sv}"), ), ), ), );
static guint
_dbus_connection_register_object(NMDhcpListener *self, GDBusConnection *connection, GError **error)
{
static const GDBusInterfaceVTable interface_vtable = {
.method_call = _method_call,
};
return g_dbus_connection_register_object(
connection,
NM_DHCP_HELPER_SERVER_OBJECT_PATH,
interface_info,
NM_UNCONST_PTR(GDBusInterfaceVTable, &interface_vtable),
self,
NULL,
error);
}
static void
new_connection_cb(NMDBusManager * mgr,
GDBusConnection * connection,
GDBusObjectManager *manager,
NMDhcpListener * self)
{
NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(self);
guint registration_id;
GError * error = NULL;
/* it is important to register the object during the new-connection signal,
* as this avoids races with the connecting object. */
registration_id = _dbus_connection_register_object(self, connection, &error);
if (!registration_id) {
_LOGE("failure to register %s for connection %p: %s",
NM_DHCP_HELPER_SERVER_OBJECT_PATH,
connection,
error->message);
g_error_free(error);
return;
}
g_hash_table_insert(priv->connections, connection, GUINT_TO_POINTER(registration_id));
}
static void
dis_connection_cb(NMDBusManager *mgr, GDBusConnection *connection, NMDhcpListener *self)
{
NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(self);
guint id;
id = GPOINTER_TO_UINT(g_hash_table_lookup(priv->connections, connection));
if (id) {
g_dbus_connection_unregister_object(connection, id);
g_hash_table_remove(priv->connections, connection);
}
}
/*****************************************************************************/
static void
nm_dhcp_listener_init(NMDhcpListener *self)
{
NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(self);
/* Maps GDBusConnection :: signal-id */
priv->connections = g_hash_table_new(nm_direct_hash, NULL);
priv->dbus_mgr = g_object_ref(nm_dbus_manager_get());
/* Register the socket our DHCP clients will return lease info on */
nm_dbus_manager_private_server_register(priv->dbus_mgr, PRIV_SOCK_PATH, PRIV_SOCK_TAG);
priv->new_conn_id = g_signal_connect(priv->dbus_mgr,
NM_DBUS_MANAGER_PRIVATE_CONNECTION_NEW "::" PRIV_SOCK_TAG,
G_CALLBACK(new_connection_cb),
self);
priv->dis_conn_id =
g_signal_connect(priv->dbus_mgr,
NM_DBUS_MANAGER_PRIVATE_CONNECTION_DISCONNECTED "::" PRIV_SOCK_TAG,
G_CALLBACK(dis_connection_cb),
self);
}
static void
dispose(GObject *object)
{
NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(object);
nm_clear_g_signal_handler(priv->dbus_mgr, &priv->new_conn_id);
nm_clear_g_signal_handler(priv->dbus_mgr, &priv->dis_conn_id);
nm_clear_pointer(&priv->connections, g_hash_table_destroy);
g_clear_object(&priv->dbus_mgr);
G_OBJECT_CLASS(nm_dhcp_listener_parent_class)->dispose(object);
}
static void
nm_dhcp_listener_class_init(NMDhcpListenerClass *listener_class)
{
GObjectClass *object_class = G_OBJECT_CLASS(listener_class);
object_class->dispose = dispose;
signals[EVENT] = g_signal_new(NM_DHCP_LISTENER_EVENT,
G_OBJECT_CLASS_TYPE(object_class),
G_SIGNAL_RUN_LAST,
0,
g_signal_accumulator_true_handled,
NULL,
NULL,
G_TYPE_BOOLEAN, /* listeners return TRUE if handled */
4,
G_TYPE_STRING, /* iface */
G_TYPE_INT, /* pid */
G_TYPE_VARIANT, /* options */
G_TYPE_STRING); /* reason */
}