/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2017 Red Hat, Inc.
*/
#include "nm-default.h"
#include "nm-hostname-manager.h"
#include <sys/stat.h>
#if HAVE_SELINUX
#include <selinux/selinux.h>
#include <selinux/label.h>
#endif
#include "nm-libnm-core-intern/nm-common-macros.h"
#include "nm-dbus-interface.h"
#include "nm-connection.h"
#include "nm-utils.h"
#include "nm-core-internal.h"
#include "NetworkManagerUtils.h"
/*****************************************************************************/
#define HOSTNAMED_SERVICE_NAME "org.freedesktop.hostname1"
#define HOSTNAMED_SERVICE_PATH "/org/freedesktop/hostname1"
#define HOSTNAMED_SERVICE_INTERFACE "org.freedesktop.hostname1"
#define HOSTNAME_FILE_DEFAULT "/etc/hostname"
#define HOSTNAME_FILE_UCASE_HOSTNAME "/etc/HOSTNAME"
#define HOSTNAME_FILE_GENTOO "/etc/conf.d/hostname"
#define CONF_DHCP SYSCONFDIR "/sysconfig/network/dhcp"
#if (defined(HOSTNAME_PERSIST_SUSE) + defined(HOSTNAME_PERSIST_SLACKWARE) \
+ defined(HOSTNAME_PERSIST_GENTOO)) \
> 1
#error "Can only define one of HOSTNAME_PERSIST_*"
#endif
#if defined(HOSTNAME_PERSIST_SUSE)
#define HOSTNAME_FILE HOSTNAME_FILE_UCASE_HOSTNAME
#elif defined(HOSTNAME_PERSIST_SLACKWARE)
#define HOSTNAME_FILE HOSTNAME_FILE_UCASE_HOSTNAME
#elif defined(HOSTNAME_PERSIST_GENTOO)
#define HOSTNAME_FILE HOSTNAME_FILE_GENTOO
#else
#define HOSTNAME_FILE HOSTNAME_FILE_DEFAULT
#endif
/*****************************************************************************/
NM_GOBJECT_PROPERTIES_DEFINE(NMHostnameManager, PROP_HOSTNAME, );
typedef struct {
char * current_hostname;
GFileMonitor *monitor;
GFileMonitor *dhcp_monitor;
gulong monitor_id;
gulong dhcp_monitor_id;
GDBusProxy * hostnamed_proxy;
} NMHostnameManagerPrivate;
struct _NMHostnameManager {
GObject parent;
NMHostnameManagerPrivate _priv;
};
struct _NMHostnameManagerClass {
GObjectClass parent;
};
G_DEFINE_TYPE(NMHostnameManager, nm_hostname_manager, G_TYPE_OBJECT);
#define NM_HOSTNAME_MANAGER_GET_PRIVATE(self) \
_NM_GET_PRIVATE(self, NMHostnameManager, NM_IS_HOSTNAME_MANAGER)
NM_DEFINE_SINGLETON_GETTER(NMHostnameManager, nm_hostname_manager_get, NM_TYPE_HOSTNAME_MANAGER);
/*****************************************************************************/
#define _NMLOG_DOMAIN LOGD_CORE
#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "hostname", __VA_ARGS__)
/*****************************************************************************/
#if defined(HOSTNAME_PERSIST_GENTOO)
static char *
read_hostname_gentoo(const char *path)
{
gs_free char * contents = NULL;
gs_strfreev char **all_lines = NULL;
const char * tmp;
guint i;
if (!g_file_get_contents(path, &contents, NULL, NULL))
return NULL;
all_lines = g_strsplit(contents, "\n", 0);
for (i = 0; all_lines[i]; i++) {
g_strstrip(all_lines[i]);
if (all_lines[i][0] == '#' || all_lines[i][0] == '\0')
continue;
if (g_str_has_prefix(all_lines[i], "hostname=")) {
tmp = &all_lines[i][NM_STRLEN("hostname=")];
return g_shell_unquote(tmp, NULL);
}
}
return NULL;
}
#endif
#if defined(HOSTNAME_PERSIST_SLACKWARE)
static char *
read_hostname_slackware(const char *path)
{
gs_free char * contents = NULL;
gs_strfreev char **all_lines = NULL;
guint i = 0;
if (!g_file_get_contents(path, &contents, NULL, NULL))
return NULL;
all_lines = g_strsplit(contents, "\n", 0);
for (i = 0; all_lines[i]; i++) {
g_strstrip(all_lines[i]);
if (all_lines[i][0] == '#' || all_lines[i][0] == '\0')
continue;
return g_shell_unquote(&all_lines[i][0], NULL);
}
return NULL;
}
#endif
#if defined(HOSTNAME_PERSIST_SUSE)
static gboolean
hostname_is_dynamic(void)
{
GIOChannel *channel;
char * str = NULL;
gboolean dynamic = FALSE;
channel = g_io_channel_new_file(CONF_DHCP, "r", NULL);
if (!channel)
return dynamic;
while (g_io_channel_read_line(channel, &str, NULL, NULL, NULL) != G_IO_STATUS_EOF) {
if (str) {
g_strstrip(str);
if (g_str_has_prefix(str, "DHCLIENT_SET_HOSTNAME="))
dynamic = strcmp(&str[NM_STRLEN("DHCLIENT_SET_HOSTNAME=")], "\"yes\"") == 0;
g_free(str);
}
}
g_io_channel_shutdown(channel, FALSE, NULL);
g_io_channel_unref(channel);
return dynamic;
}
#endif
/* Returns an allocated string which the caller owns and must eventually free */
char *
nm_hostname_manager_read_hostname(NMHostnameManager *self)
{
NMHostnameManagerPrivate *priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
char * hostname = NULL;
if (priv->hostnamed_proxy) {
hostname = g_strdup(priv->current_hostname);
goto out;
}
#if defined(HOSTNAME_PERSIST_SUSE)
if (priv->dhcp_monitor_id && hostname_is_dynamic())
return NULL;
#endif
#if defined(HOSTNAME_PERSIST_GENTOO)
hostname = read_hostname_gentoo(HOSTNAME_FILE);
#elif defined(HOSTNAME_PERSIST_SLACKWARE)
hostname = read_hostname_slackware(HOSTNAME_FILE);
#else
if (g_file_get_contents(HOSTNAME_FILE, &hostname, NULL, NULL))
g_strchomp(hostname);
#endif
out:
if (hostname && !hostname[0]) {
g_free(hostname);
return NULL;
}
return hostname;
}
/*****************************************************************************/
const char *
nm_hostname_manager_get_hostname(NMHostnameManager *self)
{
g_return_val_if_fail(NM_IS_HOSTNAME_MANAGER(self), NULL);
return NM_HOSTNAME_MANAGER_GET_PRIVATE(self)->current_hostname;
}
static void
_set_hostname_take(NMHostnameManager *self, char *hostname)
{
NMHostnameManagerPrivate *priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
_LOGI("hostname changed from %s%s%s to %s%s%s",
NM_PRINT_FMT_QUOTED(priv->current_hostname, "\"", priv->current_hostname, "\"", "(none)"),
NM_PRINT_FMT_QUOTED(hostname, "\"", hostname, "\"", "(none)"));
g_free(priv->current_hostname);
priv->current_hostname = hostname;
_notify(self, PROP_HOSTNAME);
}
static void
_set_hostname(NMHostnameManager *self, const char *hostname)
{
NMHostnameManagerPrivate *priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
hostname = nm_str_not_empty(hostname);
if (!nm_streq0(hostname, priv->current_hostname))
_set_hostname_take(self, g_strdup(hostname));
}
static void
_set_hostname_read(NMHostnameManager *self)
{
NMHostnameManagerPrivate *priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
char * hostname;
if (priv->hostnamed_proxy) {
/* read-hostname returns the current hostname with hostnamed. */
return;
}
hostname = nm_hostname_manager_read_hostname(self);
if (nm_streq0(hostname, priv->current_hostname)) {
g_free(hostname);
return;
}
_set_hostname_take(self, hostname);
}
/*****************************************************************************/
typedef struct {
char * hostname;
NMHostnameManagerSetHostnameCb cb;
gpointer user_data;
} SetHostnameInfo;
static void
set_transient_hostname_done(GObject *object, GAsyncResult *res, gpointer user_data)
{
GDBusProxy *proxy = G_DBUS_PROXY(object);
gs_free SetHostnameInfo *info = user_data;
gs_unref_variant GVariant *result = NULL;
gs_free_error GError *error = NULL;
result = g_dbus_proxy_call_finish(proxy, res, &error);
if (error) {
_LOGW("couldn't set the system hostname to '%s' using hostnamed: %s",
info->hostname,
error->message);
}
info->cb(info->hostname, !error, info->user_data);
g_free(info->hostname);
}
void
nm_hostname_manager_set_transient_hostname(NMHostnameManager * self,
const char * hostname,
NMHostnameManagerSetHostnameCb cb,
gpointer user_data)
{
NMHostnameManagerPrivate *priv;
SetHostnameInfo * info;
g_return_if_fail(NM_IS_HOSTNAME_MANAGER(self));
priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
if (!priv->hostnamed_proxy) {
cb(hostname, FALSE, user_data);
return;
}
info = g_new0(SetHostnameInfo, 1);
info->hostname = g_strdup(hostname);
info->cb = cb;
info->user_data = user_data;
g_dbus_proxy_call(priv->hostnamed_proxy,
"SetHostname",
g_variant_new("(sb)", hostname, FALSE),
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
set_transient_hostname_done,
info);
}
gboolean
nm_hostname_manager_get_transient_hostname(NMHostnameManager *self, char **hostname)
{
NMHostnameManagerPrivate *priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
GVariant * v_hostname;
if (!priv->hostnamed_proxy)
return FALSE;
v_hostname = g_dbus_proxy_get_cached_property(priv->hostnamed_proxy, "Hostname");
if (!v_hostname) {
_LOGT("transient hostname retrieval failed");
return FALSE;
}
*hostname = g_variant_dup_string(v_hostname, NULL);
g_variant_unref(v_hostname);
return TRUE;
}
gboolean
nm_hostname_manager_write_hostname(NMHostnameManager *self, const char *hostname)
{
NMHostnameManagerPrivate *priv;
char * hostname_eol;
gboolean ret;
gs_free_error GError *error = NULL;
const char * file = HOSTNAME_FILE;
gs_free char * link_path = NULL;
gs_unref_variant GVariant *var = NULL;
struct stat file_stat;
#if HAVE_SELINUX
gboolean fcon_was_set = FALSE;
char * fcon_prev = NULL;
#endif
g_return_val_if_fail(NM_IS_HOSTNAME_MANAGER(self), FALSE);
priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
if (priv->hostnamed_proxy) {
var = g_dbus_proxy_call_sync(priv->hostnamed_proxy,
"SetStaticHostname",
g_variant_new("(sb)", hostname, FALSE),
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
&error);
if (error)
_LOGW("could not set hostname: %s", error->message);
return !error;
}
/* If the hostname file is a symbolic link, follow it to find where the
* real file is located, otherwise g_file_set_contents will attempt to
* replace the link with a plain file.
*/
if (lstat(file, &file_stat) == 0 && S_ISLNK(file_stat.st_mode)
&& (link_path = nm_utils_read_link_absolute(file, NULL)))
file = link_path;
#if defined(HOSTNAME_PERSIST_GENTOO)
hostname_eol = g_strdup_printf("#Generated by NetworkManager\n"
"hostname=\"%s\"\n",
hostname);
#else
hostname_eol = g_strdup_printf("%s\n", hostname);
#endif
#if HAVE_SELINUX
/* Get default context for hostname file and set it for fscreate */
{
struct selabel_handle *handle;
handle = selabel_open(SELABEL_CTX_FILE, NULL, 0);
if (handle) {
mode_t st_mode = 0;
char * fcon = NULL;
if (stat(file, &file_stat) == 0)
st_mode = file_stat.st_mode;
if ((selabel_lookup(handle, &fcon, file, st_mode) == 0)
&& (getfscreatecon(&fcon_prev) == 0)) {
setfscreatecon(fcon);
fcon_was_set = TRUE;
}
selabel_close(handle);
freecon(fcon);
}
}
#endif
ret = g_file_set_contents(file, hostname_eol, -1, &error);
#if HAVE_SELINUX
/* Restore previous context and cleanup */
if (fcon_was_set)
setfscreatecon(fcon_prev);
if (fcon_prev)
freecon(fcon_prev);
#endif
g_free(hostname_eol);
if (!ret) {
_LOGW("could not save hostname to %s: %s", file, error->message);
return FALSE;
}
return TRUE;
}
gboolean
nm_hostname_manager_validate_hostname(const char *hostname)
{
const char *p;
gboolean dot = TRUE;
if (!hostname || !hostname[0])
return FALSE;
for (p = hostname; *p; p++) {
if (*p == '.') {
if (dot)
return FALSE;
dot = TRUE;
} else {
if (!g_ascii_isalnum(*p) && (*p != '-') && (*p != '_'))
return FALSE;
dot = FALSE;
}
}
if (dot)
return FALSE;
return (p - hostname <= HOST_NAME_MAX);
}
static void
hostname_file_changed_cb(GFileMonitor * monitor,
GFile * file,
GFile * other_file,
GFileMonitorEvent event_type,
gpointer user_data)
{
_set_hostname_read(user_data);
}
/*****************************************************************************/
static void
hostnamed_properties_changed(GDBusProxy *proxy,
GVariant * changed_properties,
char ** invalidated_properties,
gpointer user_data)
{
NMHostnameManager * self = user_data;
NMHostnameManagerPrivate *priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
GVariant * v_hostname;
v_hostname = g_dbus_proxy_get_cached_property(priv->hostnamed_proxy, "StaticHostname");
if (v_hostname) {
_set_hostname(self, g_variant_get_string(v_hostname, NULL));
g_variant_unref(v_hostname);
}
}
static void
setup_hostname_file_monitors(NMHostnameManager *self)
{
NMHostnameManagerPrivate *priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
GFileMonitor * monitor;
const char * path = HOSTNAME_FILE;
char * link_path = NULL;
struct stat file_stat;
GFile * file;
/* resolve the path to the hostname file if it is a symbolic link */
if (lstat(path, &file_stat) == 0 && S_ISLNK(file_stat.st_mode)
&& (link_path = nm_utils_read_link_absolute(path, NULL))) {
path = link_path;
if (lstat(link_path, &file_stat) == 0 && S_ISLNK(file_stat.st_mode)) {
_LOGW("only one level of symbolic link indirection is allowed when "
"monitoring " HOSTNAME_FILE);
}
}
/* monitor changes to hostname file */
file = g_file_new_for_path(path);
monitor = g_file_monitor_file(file, G_FILE_MONITOR_NONE, NULL, NULL);
g_object_unref(file);
g_free(link_path);
if (monitor) {
priv->monitor_id =
g_signal_connect(monitor, "changed", G_CALLBACK(hostname_file_changed_cb), self);
priv->monitor = monitor;
}
#if defined(HOSTNAME_PERSIST_SUSE)
/* monitor changes to dhcp file to know whether the hostname is valid */
file = g_file_new_for_path(CONF_DHCP);
monitor = g_file_monitor_file(file, G_FILE_MONITOR_NONE, NULL, NULL);
g_object_unref(file);
if (monitor) {
priv->dhcp_monitor_id =
g_signal_connect(monitor, "changed", G_CALLBACK(hostname_file_changed_cb), self);
priv->dhcp_monitor = monitor;
}
#endif
_set_hostname_read(self);
}
/*****************************************************************************/
static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
NMHostnameManager *self = NM_HOSTNAME_MANAGER(object);
switch (prop_id) {
case PROP_HOSTNAME:
g_value_set_string(value, nm_hostname_manager_get_hostname(self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
break;
}
}
/*****************************************************************************/
static void
nm_hostname_manager_init(NMHostnameManager *self)
{}
static void
constructed(GObject *object)
{
NMHostnameManager * self = NM_HOSTNAME_MANAGER(object);
NMHostnameManagerPrivate *priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
GDBusProxy * proxy;
GVariant * variant;
gs_free_error GError *error = NULL;
proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM,
0,
NULL,
HOSTNAMED_SERVICE_NAME,
HOSTNAMED_SERVICE_PATH,
HOSTNAMED_SERVICE_INTERFACE,
NULL,
&error);
if (proxy) {
variant = g_dbus_proxy_get_cached_property(proxy, "StaticHostname");
if (variant) {
_LOGI("hostname: using hostnamed");
priv->hostnamed_proxy = proxy;
g_signal_connect(proxy,
"g-properties-changed",
G_CALLBACK(hostnamed_properties_changed),
self);
hostnamed_properties_changed(proxy, NULL, NULL, self);
g_variant_unref(variant);
} else {
_LOGI("hostname: couldn't get property from hostnamed");
g_object_unref(proxy);
}
} else {
_LOGI("hostname: hostnamed not used as proxy creation failed with: %s", error->message);
g_clear_error(&error);
}
if (!priv->hostnamed_proxy)
setup_hostname_file_monitors(self);
G_OBJECT_CLASS(nm_hostname_manager_parent_class)->constructed(object);
}
static void
dispose(GObject *object)
{
NMHostnameManager * self = NM_HOSTNAME_MANAGER(object);
NMHostnameManagerPrivate *priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
if (priv->hostnamed_proxy) {
g_signal_handlers_disconnect_by_func(priv->hostnamed_proxy,
G_CALLBACK(hostnamed_properties_changed),
self);
g_clear_object(&priv->hostnamed_proxy);
}
if (priv->monitor) {
if (priv->monitor_id)
g_signal_handler_disconnect(priv->monitor, priv->monitor_id);
g_file_monitor_cancel(priv->monitor);
g_clear_object(&priv->monitor);
}
if (priv->dhcp_monitor) {
if (priv->dhcp_monitor_id)
g_signal_handler_disconnect(priv->dhcp_monitor, priv->dhcp_monitor_id);
g_file_monitor_cancel(priv->dhcp_monitor);
g_clear_object(&priv->dhcp_monitor);
}
nm_clear_g_free(&priv->current_hostname);
G_OBJECT_CLASS(nm_hostname_manager_parent_class)->dispose(object);
}
static void
nm_hostname_manager_class_init(NMHostnameManagerClass *class)
{
GObjectClass *object_class = G_OBJECT_CLASS(class);
object_class->constructed = constructed;
object_class->get_property = get_property;
object_class->dispose = dispose;
obj_properties[PROP_HOSTNAME] = g_param_spec_string(NM_HOSTNAME_MANAGER_HOSTNAME,
"",
"",
NULL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
}